Repository: spf13/cobra (go)

Issue:
Feature request: Set a default completion function
In my experience, not many flags actually require completion of files.

Some have dynamic completions, but a great deal of them take free-form string parameters where no completion can be offered.
Unfortunately, the default completion for a flag with no completion function in Cobra is file completion.
This forces me to register a `cobra.NoFileCompletions` for each flag of that category.

As an example, completion for `docker run` has 56 flags where no meaningful FlagCompletionFunc exists.
In a first attempt, I registered 56 handlers with `cmd.RegisterFlagCompletionFunc("flag-name", cobra.NoFileCompletions)` just to silence their file completion.
Following a hint of @thaJeztah, I replaced them with a programmatically installed default handler, see [here](https://github.com/docker/cli/commit/06260e68f3686a0368284a8ce9b910e056bb523b#diff-50fe347eb4bdafebc190aa42b2caa34b089a538cd444b8a151a763372e93c0cf).

I would very much welcome a solution in Cobra, where I can set a default handler on command level, something like

```go
cmd.CompletionOptions.DefaultCompHandler = cobra.NoFileCompletions
```

With this feature, I'd only have to define the completion functions for flags that actually have meaningful completions.

/cc @thaJeztah

Pull request:
The default ShellCompDirective can be customized for a command and its subcommands
Closes #2209, #2221 

#2221 proposed a solution where the custom `DefaultShellCompDirective` only applied to a given command.

In https://github.com/spf13/cobra/pull/2221#discussion_r1923845125, alternative approaches were discussed.
We agreed on a recursive solution, where a change applies to a command and its subcommands.

To ease review, I open a new PR for this approach.

Patch:
diff --git a/.github/workflows/size-labeler.yml b/.github/workflows/size-labeler.yml
deleted file mode 100644
index 4c54d82..0000000
--- a/.github/workflows/size-labeler.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-# Reference: https://github.com/CodelyTV/pr-size-labeler
-
-name: size-labeler
-
-on: [pull_request_target]
-
-permissions:
-  contents: read
-
-jobs:
-  size-labeler:
-    permissions:
-      pull-requests: write  # for codelytv/pr-size-labeler to add labels & comment on PRs
-    runs-on: ubuntu-latest
-    name: Label the PR size
-    steps:
-      - uses: codelytv/pr-size-labeler@v1.8.1
-        with:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-          xs_label: 'size/XS'
-          xs_max_size: '10'
-          s_label: 'size/S'
-          s_max_size: '24'
-          m_label: 'size/M'
-          m_max_size: '99'
-          l_label: 'size/L'
-          l_max_size: '200'
-          xl_label: 'size/XL'
-          fail_if_xl: 'false'
-          message_if_xl: >
-            'This PR exceeds the recommended size of 200 lines.
-            Please make sure you are NOT addressing multiple issues with one PR.
-            Note this PR might be rejected due to its size.’
diff --git a/.golangci.yml b/.golangci.yml
index 2c8f480..6acf8ab 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -12,14 +12,20 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+version: "2"
+
 run:
-  deadline: 5m
+  timeout: 5m
+
+formatters:
+  enable:
+    - gofmt
+    - goimports
 
 linters:
-  disable-all: true
+  default: none
   enable:
     #- bodyclose
-    # - deadcode ! deprecated since v1.49.0; replaced by 'unused'
     #- depguard
     #- dogsled
     #- dupl
@@ -30,28 +36,24 @@ linters:
     - goconst
     - gocritic
     #- gocyclo
-    - gofmt
-    - goimports
-    #- gomnd
     #- goprintffuncname
     - gosec
-    - gosimple
     - govet
     - ineffassign
     #- lll
     - misspell
+    #- mnd
     #- nakedret
     #- noctx
     - nolintlint
     #- rowserrcheck
-    #- scopelint
     - staticcheck
-    #- structcheck ! deprecated since v1.49.0; replaced by 'unused'
-    - stylecheck
-    #- typecheck
     - unconvert
     #- unparam
     - unused
-    # - varcheck ! deprecated since v1.49.0; replaced by 'unused'
     #- whitespace
-  fast: false
+  exclusions:
+    presets:
+      - common-false-positives
+      - legacy
+      - std-error-handling
diff --git a/README.md b/README.md
index 7175715..b30e91b 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,20 @@ name a few. [This list](site/content/projects_using_cobra.md) contains a more ex
 [![Go Reference](https://pkg.go.dev/badge/github.com/spf13/cobra.svg)](https://pkg.go.dev/github.com/spf13/cobra)
 [![Go Report Card](https://goreportcard.com/badge/github.com/spf13/cobra)](https://goreportcard.com/report/github.com/spf13/cobra)
 [![Slack](https://img.shields.io/badge/Slack-cobra-brightgreen)](https://gophers.slack.com/archives/CD3LP1199)
+<hr>
+<div align="center" markdown="1">
+   <sup>Supported by:</sup>
+   <br>
+   <br>
+   <a href="https://www.warp.dev/cobra">
+      <img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/ab8dd143-b0fd-4904-bdc5-dd7ecac94eae">
+   </a>
+
+### [Warp, the AI terminal for devs](https://www.warp.dev/cobra)
+[Try Cobra in Warp today](https://www.warp.dev/cobra)<br>
+
+</div>
+<hr>
 
 # Overview
 
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..54e60c2
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,105 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+The `cobra` maintainers take security issues seriously and
+we appreciate your efforts to _**responsibly**_ disclose your findings.
+We will make every effort to swiftly respond and address concerns.
+
+To report a security vulnerability:
+
+1. **DO NOT** create a public GitHub issue for the vulnerability!
+2. **DO NOT** create a public GitHub Pull Request with a fix for the vulnerability!
+3. Send an email to `cobra-security@googlegroups.com`.
+4. Include the following details in your report:
+   - Description of the vulnerability
+   - Steps to reproduce
+   - Potential impact of the vulnerability (to your downstream project, to the Go ecosystem, etc.)
+   - Any potential mitigations you've already identified
+5. Allow up to 7 days for an initial response.
+   You should receive an acknowledgment of your report and an estimated timeline for a fix.
+6. (Optional) If you have a fix and would like to contribute your patch, please work
+   directly with the maintainers via `cobra-security@googlegroups.com` to
+   coordinate pushing the patch to GitHub, cutting a new release, and disclosing the change.
+
+## Response Process
+
+When a security vulnerability report is received, the `cobra` maintainers will:
+
+1. Confirm receipt of the vulnerability report within 7 days.
+2. Assess the report to determine if it constitutes a security vulnerability.
+3. If confirmed, assign the vulnerability a severity level and create a timeline for addressing it.
+4. Develop and test a fix.
+5. Patch the vulnerability and make a new GitHub release: the maintainers will coordinate disclosure with the reporter.
+6. Create a new GitHub Security Advisory to inform the broader Go ecosystem
+
+## Disclosure Policy
+
+The `cobra` maintainers follow a coordinated disclosure process:
+
+1. Security vulnerabilities will be addressed as quickly as possible.
+2. A CVE (Common Vulnerabilities and Exposures) identifier will be requested for significant vulnerabilities
+   that are within `cobra` itself.
+3. Once a fix is ready, the maintainers will:
+   - Release a new version containing the fix.
+   - Update the security advisory with details about the vulnerability.
+   - Credit the reporter (unless they wish to remain anonymous).
+   - Credit the fixer (unless they wish to remain anonymous, this may be the same as the reporter).
+   - Announce the vulnerability through appropriate channels
+     (GitHub Security Advisory, mailing lists, GitHub Releases, etc.)
+
+## Supported Versions
+
+Security fixes will typically only be released for the most recent major release.
+
+## Upstream Security Issues
+
+`cobra` generally will not accept vulnerability reports that originate in upstream
+dependencies. I.e., if there is a problem in Go code that `cobra` depends on,
+it is best to engage that project's maintainers and owners.
+
+This security policy primarily pertains only to `cobra` itself but if you believe you've
+identified a problem that originates in an upstream dependency and is being widely
+distributed by `cobra`, please follow the disclosure procedure above: the `cobra`
+maintainers will work with you to determine the severity and ecosystem impact.
+
+## Security Updates and CVEs
+
+Information about known security vulnerabilities and CVEs affecting `cobra` will
+be published as GitHub Security Advisories at
+https://github.com/spf13/cobra/security/advisories.
+
+All users are encouraged to watch the repository and upgrade promptly when
+security releases are published.
+
+## `cobra` Security Best Practices for Users
+
+When using `cobra` in your CLIs, the `cobra` maintainers recommend the following:
+
+1. Always use the latest version of `cobra`.
+2. [Use Go modules](https://go.dev/blog/using-go-modules) for dependency management.
+3. Always use the latest possible version of Go.
+
+## Security Best Practices for Contributors
+
+When contributing to `cobra`:
+
+1. Be mindful of security implications when adding new features or modifying existing ones.
+2. Be aware of `cobra`'s extremely large reach: it is used in nearly every Go CLI
+   (like Kubernetes, Docker, Prometheus, etc. etc.)
+3. Write tests that explicitly cover edge cases and potential issues.
+4. If you discover a security issue while working on `cobra`, please report it
+   following the process above rather than opening a public pull request or issue that
+   addresses the vulnerability.
+5. Take personal sec-ops seriously and secure your GitHub account: use [two-factor authentication](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa),
+   [sign your commits with a GPG or SSH key](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification),
+   etc.
+
+## Acknowledgments
+
+The `cobra` maintainers would like to thank all security researchers and
+community members who help keep cobra, its users, and the entire Go ecosystem secure through responsible disclosures!!
+
+---
+
+*This security policy is inspired by the [Open Web Application Security Project (OWASP)](https://owasp.org/) guidelines and security best practices.*
diff --git a/command.go b/command.go
index dbb2c29..4794e5e 100644
--- a/command.go
+++ b/command.go
@@ -1296,6 +1296,11 @@ Simply type ` + c.DisplayName() + ` help [path to command] for full details.`,
 					c.Printf("Unknown help topic %#q\n", args)
 					CheckErr(c.Root().Usage())
 				} else {
+					// FLow the context down to be used in help text
+					if cmd.ctx == nil {
+						cmd.ctx = c.ctx
+					}
+
 					cmd.InitDefaultHelpFlag()    // make possible 'help' flag to be shown
 					cmd.InitDefaultVersionFlag() // make possible 'version' flag to be shown
 					CheckErr(cmd.Help())
@@ -2020,7 +2025,7 @@ func defaultUsageFunc(w io.Writer, in interface{}) error {
 		fmt.Fprint(w, trimRightSpace(c.InheritedFlags().FlagUsages()))
 	}
 	if c.HasHelpSubCommands() {
-		fmt.Fprintf(w, "\n\nAdditional help topcis:")
+		fmt.Fprintf(w, "\n\nAdditional help topics:")
 		for _, subcmd := range c.Commands() {
 			if subcmd.IsAdditionalHelpTopicCommand() {
 				fmt.Fprintf(w, "\n  %s %s", rpad(subcmd.CommandPath(), subcmd.CommandPathPadding()), subcmd.Short)
diff --git a/completions.go b/completions.go
index a1752f7..d3607c2 100644
--- a/completions.go
+++ b/completions.go
@@ -115,6 +115,13 @@ type CompletionOptions struct {
 	DisableDescriptions bool
 	// HiddenDefaultCmd makes the default 'completion' command hidden
 	HiddenDefaultCmd bool
+	// DefaultShellCompDirective sets the ShellCompDirective that is returned
+	// if no special directive can be determined
+	DefaultShellCompDirective *ShellCompDirective
+}
+
+func (receiver *CompletionOptions) SetDefaultShellCompDirective(directive ShellCompDirective) {
+	receiver.DefaultShellCompDirective = &directive
 }
 
 // Completion is a string that can be used for completions
@@ -375,7 +382,7 @@ func (c *Command) getCompletions(args []string) (*Command, []Completion, ShellCo
 	// Error while attempting to parse flags
 	if flagErr != nil {
 		// If error type is flagCompError and we don't want flagCompletion we should ignore the error
-		if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) {
+		if _, ok := flagErr.(*flagCompError); !ok || flagCompletion {
 			return finalCmd, []Completion{}, ShellCompDirectiveDefault, flagErr
 		}
 	}
@@ -480,6 +487,14 @@ func (c *Command) getCompletions(args []string) (*Command, []Completion, ShellCo
 		}
 	} else {
 		directive = ShellCompDirectiveDefault
+		// check current and parent commands for a custom DefaultShellCompDirective
+		for cmd := finalCmd; cmd != nil; cmd = cmd.parent {
+			if cmd.CompletionOptions.DefaultShellCompDirective != nil {
+				directive = *cmd.CompletionOptions.DefaultShellCompDirective
+				break
+			}
+		}
+
 		if flag == nil {
 			foundLocalNonPersistentFlag := false
 			// If TraverseChildren is true on the root command we don't check for
@@ -773,7 +788,7 @@ See each sub-command's help for details on how to use the generated script.
 		// shell completion for it (prog __complete completion '')
 		subCmd, cmdArgs, err := c.Find(args)
 		if err != nil || subCmd.Name() != compCmdName &&
-			!(subCmd.Name() == ShellCompRequestCmd && len(cmdArgs) > 1 && cmdArgs[0] == compCmdName) {
+			(subCmd.Name() != ShellCompRequestCmd || len(cmdArgs) <= 1 || cmdArgs[0] != compCmdName) {
 			// The completion command is not being called or being completed so we remove it.
 			c.RemoveCommand(completionCmd)
 			return
diff --git a/doc/man_docs.go b/doc/man_docs.go
index 2138f24..560bc20 100644
--- a/doc/man_docs.go
+++ b/doc/man_docs.go
@@ -212,7 +212,7 @@ func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
 	manPrintOptions(buf, cmd)
 	if len(cmd.Example) > 0 {
 		buf.WriteString("# EXAMPLE\n")
-		buf.WriteString(fmt.Sprintf("```\n%s\n```\n", cmd.Example))
+		fmt.Fprintf(buf, "```\n%s\n```\n", cmd.Example)
 	}
 	if hasSeeAlso(cmd) {
 		buf.WriteString("# SEE ALSO\n")
@@ -240,7 +240,7 @@ func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
 		buf.WriteString(strings.Join(seealsos, ", ") + "\n")
 	}
 	if !cmd.DisableAutoGenTag {
-		buf.WriteString(fmt.Sprintf("# HISTORY\n%s Auto generated by spf13/cobra\n", header.Date.Format("2-Jan-2006")))
+		fmt.Fprintf(buf, "# HISTORY\n%s Auto generated by spf13/cobra\n", header.Date.Format("2-Jan-2006"))
 	}
 	return buf.Bytes()
 }
diff --git a/doc/md_docs.go b/doc/md_docs.go
index 1259222..6eae7cc 100644
--- a/doc/md_docs.go
+++ b/doc/md_docs.go
@@ -69,12 +69,12 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
 	}
 
 	if cmd.Runnable() {
-		buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine()))
+		fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.UseLine())
 	}
 
 	if len(cmd.Example) > 0 {
 		buf.WriteString("### Examples\n\n")
-		buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.Example))
+		fmt.Fprintf(buf, "```\n%s\n```\n\n", cmd.Example)
 	}
 
 	if err := printOptions(buf, cmd, name); err != nil {
@@ -87,7 +87,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
 			pname := parent.CommandPath()
 			link := pname + markdownExtension
 			link = strings.ReplaceAll(link, " ", "_")
-			buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short))
+			fmt.Fprintf(buf, "* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short)
 			cmd.VisitParents(func(c *cobra.Command) {
 				if c.DisableAutoGenTag {
 					cmd.DisableAutoGenTag = c.DisableAutoGenTag
@@ -105,7 +105,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
 			cname := name + " " + child.Name()
 			link := cname + markdownExtension
 			link = strings.ReplaceAll(link, " ", "_")
-			buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short))
+			fmt.Fprintf(buf, "* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short)
 		}
 		buf.WriteString("\n")
 	}
diff --git a/doc/rest_docs.go b/doc/rest_docs.go
index c33acc2..4901ca9 100644
--- a/doc/rest_docs.go
+++ b/doc/rest_docs.go
@@ -82,13 +82,13 @@ func GenReSTCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string, str
 	buf.WriteString("\n" + long + "\n\n")
 
 	if cmd.Runnable() {
-		buf.WriteString(fmt.Sprintf("::\n\n  %s\n\n", cmd.UseLine()))
+		fmt.Fprintf(buf, "::\n\n  %s\n\n", cmd.UseLine())
 	}
 
 	if len(cmd.Example) > 0 {
 		buf.WriteString("Examples\n")
 		buf.WriteString("~~~~~~~~\n\n")
-		buf.WriteString(fmt.Sprintf("::\n\n%s\n\n", indentString(cmd.Example, "  ")))
+		fmt.Fprintf(buf, "::\n\n%s\n\n", indentString(cmd.Example, "  "))
 	}
 
 	if err := printOptionsReST(buf, cmd, name); err != nil {
@@ -101,7 +101,7 @@ func GenReSTCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string, str
 			parent := cmd.Parent()
 			pname := parent.CommandPath()
 			ref = strings.ReplaceAll(pname, " ", "_")
-			buf.WriteString(fmt.Sprintf("* %s \t - %s\n", linkHandler(pname, ref), parent.Short))
+			fmt.Fprintf(buf, "* %s \t - %s\n", linkHandler(pname, ref), parent.Short)
 			cmd.VisitParents(func(c *cobra.Command) {
 				if c.DisableAutoGenTag {
 					cmd.DisableAutoGenTag = c.DisableAutoGenTag
@@ -118,7 +118,7 @@ func GenReSTCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string, str
 			}
 			cname := name + " " + child.Name()
 			ref = strings.ReplaceAll(cname, " ", "_")
-			buf.WriteString(fmt.Sprintf("* %s \t - %s\n", linkHandler(cname, ref), child.Short))
+			fmt.Fprintf(buf, "* %s \t - %s\n", linkHandler(cname, ref), child.Short)
 		}
 		buf.WriteString("\n")
 	}
diff --git a/doc/yaml_docs.go b/doc/yaml_docs.go
index 2b26d6e..3719717 100644
--- a/doc/yaml_docs.go
+++ b/doc/yaml_docs.go
@@ -153,7 +153,7 @@ func genFlagResult(flags *pflag.FlagSet) []cmdOption {
 		// Todo, when we mark a shorthand is deprecated, but specify an empty message.
 		// The flag.ShorthandDeprecated is empty as the shorthand is deprecated.
 		// Using len(flag.ShorthandDeprecated) > 0 can't handle this, others are ok.
-		if !(len(flag.ShorthandDeprecated) > 0) && len(flag.Shorthand) > 0 {
+		if len(flag.ShorthandDeprecated) == 0 && len(flag.Shorthand) > 0 {
 			opt := cmdOption{
 				flag.Name,
 				flag.Shorthand,
diff --git a/site/content/completions/_index.md b/site/content/completions/_index.md
index 8445850..0115910 100644
--- a/site/content/completions/_index.md
+++ b/site/content/completions/_index.md
@@ -305,6 +305,36 @@ $ helm status --output [tab][tab]
 json table yaml
 ```
 
+#### Change the default ShellCompDirective
+
+When no completion function is registered for a leaf command or for a flag, Cobra will
+automatically use `ShellCompDirectiveDefault`, which will invoke the shell's filename completion.
+This implies that when file completion does not apply to a leaf command or to a flag (the command
+or flag does not operate on a filename), turning off file completion requires you to register a
+completion function for that command/flag.
+For example:
+
+```go
+cmd.RegisterFlagCompletionFunc("flag-name", cobra.NoFileCompletions)
+```
+
+If you find that there are more situations where file completion should be turned off than
+when it is applicable, you can recursively change the default `ShellCompDirective` for a command
+and its subcommands to `ShellCompDirectiveNoFileComp`:
+
+```go
+cmd.CompletionOptions.SetDefaultShellCompDirective(ShellCompDirectiveNoFileComp)
+```
+
+If doing so, keep in mind that you should instead register a completion function for leaf commands or
+flags where file completion is applicable. For example:
+
+```go
+cmd.RegisterFlagCompletionFunc("flag-name", cobra.FixedCompletions(nil, ShellCompDirectiveDefault))
+```
+
+To change the default directive for the entire program, set the DefaultShellCompDirective on the root command.
+
 #### Debugging
 
 You can also easily debug your Go completion code for flags:


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2904bf0..f6a3111 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -47,7 +47,7 @@ jobs:
           check-latest: true
           cache: true
 
-      - uses: golangci/golangci-lint-action@v4.0.0
+      - uses: golangci/golangci-lint-action@v8.0.0
         with:
           version: latest
           args: --verbose
diff --git a/command_test.go b/command_test.go
index 7e7c341..a86e57f 100644
--- a/command_test.go
+++ b/command_test.go
@@ -2921,3 +2921,34 @@ func TestUnknownFlagShouldReturnSameErrorRegardlessOfArgPosition(t *testing.T) {
 		})
 	}
 }
+
+func TestHelpFuncExecuted(t *testing.T) {
+	helpText := "Long description"
+
+	// Create a context that will be unique, not just the background context
+	//nolint:staticcheck // We can safely use a basic type as key in tests.
+	executionCtx := context.WithValue(context.Background(), "testKey", "123")
+
+	child := &Command{Use: "child", Run: emptyRun}
+	child.SetHelpFunc(func(cmd *Command, args []string) {
+		_, err := cmd.OutOrStdout().Write([]byte(helpText))
+		if err != nil {
+			t.Error(err)
+		}
+
+		// Test for https://github.com/spf13/cobra/issues/2240
+		if cmd.Context() != executionCtx {
+			t.Error("Context doesn't equal the execution context")
+		}
+	})
+
+	rootCmd := &Command{Use: "root", Run: emptyRun}
+	rootCmd.AddCommand(child)
+
+	output, err := executeCommandWithContext(executionCtx, rootCmd, "help", "child")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+	}
+
+	checkStringContains(t, output, helpText)
+}
diff --git a/completions_test.go b/completions_test.go
index 89da3d5..ec7b907 100644
--- a/completions_test.go
+++ b/completions_test.go
@@ -1297,7 +1297,7 @@ func TestValidArgsFuncCmdContext(t *testing.T) {
 	}
 	rootCmd.AddCommand(childCmd)
 
-	//nolint:golint,staticcheck // We can safely use a basic type as key in tests.
+	//nolint:staticcheck // We can safely use a basic type as key in tests.
 	ctx := context.WithValue(context.Background(), "testKey", "123")
 
 	// Test completing an empty string on the childCmd
@@ -2899,7 +2899,7 @@ func TestCompletionFuncCompatibility(t *testing.T) {
 			var userComp func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
 
 			// check against new signature
-			var _ CompletionFunc = userComp
+			var _ CompletionFunc = userComp //nolint:staticcheck // LHS type is needed for this use case
 
 			// check Command accepts
 			cmd := Command{
@@ -2913,8 +2913,7 @@ func TestCompletionFuncCompatibility(t *testing.T) {
 			var userComp func(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective)
 
 			// check against new signature
-			var _ CompletionFunc = userComp
-
+			var _ CompletionFunc = userComp //nolint:staticcheck // LHS type is needed for this use case
 			// check Command accepts
 			cmd := Command{
 				ValidArgsFunction: userComp,
@@ -2927,8 +2926,8 @@ func TestCompletionFuncCompatibility(t *testing.T) {
 			var userComp CompletionFunc
 
 			// check helper against old signature
-			var _ func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) = userComp
-			var _ func(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) = userComp
+			var _ func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) = userComp     //nolint:staticcheck // LHS type is needed for this use case
+			var _ func(cmd *Command, args []string, toComplete string) ([]Completion, ShellCompDirective) = userComp //nolint:staticcheck // LHS type is needed for this use case
 
 			// check Command accepts
 			cmd := Command{
@@ -2967,7 +2966,7 @@ func TestCompletionFuncCompatibility(t *testing.T) {
 			var userComp UserCompletionTypeAliasHelper
 
 			// Here we are validating the existing type validates the CompletionFunc type
-			var _ CompletionFunc = userComp
+			var _ CompletionFunc = userComp //nolint:staticcheck // LHS type is needed for this use case
 
 			cmd := Command{
 				ValidArgsFunction: userComp,
@@ -4016,3 +4015,60 @@ func TestInitDefaultCompletionCmd(t *testing.T) {
 		})
 	}
 }
+
+func TestCustomDefaultShellCompDirective(t *testing.T) {
+	rootCmd := &Command{Use: "root", Run: emptyRun}
+	rootCmd.PersistentFlags().String("string", "", "test string flag")
+	// use ShellCompDirectiveNoFileComp instead of the default, which is ShellCompDirectiveDefault.
+	rootCmd.CompletionOptions.SetDefaultShellCompDirective(ShellCompDirectiveNoFileComp)
+
+	// child1 inherits the custom DefaultShellCompDirective.
+	childCmd1 := &Command{Use: "child1", Run: emptyRun}
+	// child2 resets the custom DefaultShellCompDirective to the default value.
+	childCmd2 := &Command{Use: "child2", Run: emptyRun}
+	childCmd2.CompletionOptions.SetDefaultShellCompDirective(ShellCompDirectiveDefault)
+
+	rootCmd.AddCommand(childCmd1, childCmd2)
+
+	testCases := []struct {
+		desc              string
+		args              []string
+		expectedDirective string
+	}{
+		{
+			"flag completion on root command with custom DefaultShellCompDirective",
+			[]string{"--string", ""},
+			"ShellCompDirectiveNoFileComp",
+		},
+		{
+			"flag completion on subcommand with inherited custom DefaultShellCompDirective",
+			[]string{"child1", "--string", ""},
+			"ShellCompDirectiveNoFileComp",
+		},
+		{
+			"flag completion on subcommand with reset DefaultShellCompDirective",
+			[]string{"child2", "--string", ""},
+			"ShellCompDirectiveDefault",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			args := []string{ShellCompNoDescRequestCmd}
+			args = append(args, tc.args...)
+
+			output, err := executeCommand(rootCmd, args...)
+
+			if err != nil {
+				t.Errorf("Unexpected error: %v", err)
+			}
+
+			outputWords := strings.Split(strings.TrimSpace(output), " ")
+			directive := outputWords[len(outputWords)-1]
+
+			if directive != tc.expectedDirective {
+				t.Errorf("expected: %q, got: %q", tc.expectedDirective, directive)
+			}
+		})
+	}
+}