-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Description
My urfave/cli version is
v3.5.0 (latest as of 2025-10-30)
Checklist
- Are you running the latest v3 release? The list of releases is https://github.com/urfave/cli/releases. ✅
- Did you check the manual for your release? The v3 manual is https://cli.urfave.org/v3/getting-started/ ✅
- Did you perform a search about this problem? Here's the https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests about searching. ✅
Dependency Management
My project is using go modules.
Describe the bug
When the same flag (same flag object instance) is defined in both a root command's Flags array and a subcommand's Flags array, environment variables specified via
Sources: cli.EnvVars() are not read correctly when executing the subcommand. However, CLI flag values passed via command-line arguments work fine.
This appears to be a regression or unintended behavior in v3, as the flag duplication pattern was common in v2 for creating "global" flags.
Minimal reproduction code
package main
import (
"context"
"fmt"
"os"
"github.com/urfave/cli/v3"
)
func main() {
// Define flag once
flagTest := &cli.StringFlag{
Name: "test-flag",
Sources: cli.EnvVars("TEST_FLAG"),
Usage: "Test flag",
Required: true,
}
// Subcommand that uses the flag
subCmd := &cli.Command{
Name: "subcmd",
Flags: []cli.Flag{
flagTest, // Flag on subcommand
},
Action: func(ctx context.Context, cmd *cli.Command) error {
value := cmd.String(flagTest.Name)
fmt.Printf("Subcommand - Flag value: '%s'\n", value)
return nil
},
}
// Root command that ALSO has the same flag
rootCmd := &cli.Command{
Flags: []cli.Flag{
flagTest, // SAME flag on root
},
Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
value := cmd.String(flagTest.Name)
fmt.Printf("Root Before - Flag value: '%s'\n", value)
return ctx, nil
},
Commands: []*cli.Command{
subCmd,
},
}
if err := rootCmd.Run(context.Background(), os.Args); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}Steps to reproduce
- Save the code above as main.go
- Run with environment variable set:
TEST_FLAG="my-value" go run main.go subcmd
Observed behavior
Root Before - Flag value: ''
Subcommand - Flag value: ''
The environment variable TEST_FLAG is not read, and the flag value is empty.
However, if you pass the flag via CLI:
TEST_FLAG="my-value" go run main.go subcmd --test-flag="cli-value"
Output:
Root Before - Flag value: 'cli-value'
Subcommand - Flag value: 'cli-value'
The CLI flag does work correctly.
Expected behavior
The environment variable should be read and the flag value should be:
Root Before - Flag value: 'my-value'
Subcommand - Flag value: 'my-value'
This is the behavior when the flag is not duplicated on the root command (i.e., when flagTest is only in the subcommand's Flags array).
Additional context
Workaround: Remove the flag from the root command's Flags array if it's only used by subcommands. Only include flags on the root that are actually used by the root's
Before/After hooks.
This issue was discovered while migrating a large codebase from v2 to v3. In v2, it was common to define flags on both root and subcommands to make them "globally
available." The migration guide doesn't mention this breaking change.
Impact
This could affect many projects migrating from v2 to v3, as the duplication pattern was used for global flags. The issue is subtle because:
- CLI flags still work (masking the problem during development)
- Environment variables silently fail (causing issues in production/CI)
Questions
- Is this intended behavior?
- Should the migration guide warn about this?
- Is there a recommended pattern for "global" flags in v3 that work with both CLI args and env vars?
Run go version and paste its output here
go version go1.24.6 darwin/arm64
Run go env and paste its output here
GO111MODULE='on'
GOARCH='arm64'
GOOS='darwin'
GOVERSION='go1.24.6'