Skip to content

Environment variables not read when flag is defined on both root and subcommand #2217

@peterdeme

Description

@peterdeme

My urfave/cli version is

v3.5.0 (latest as of 2025-10-30)

Checklist

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

  1. Save the code above as main.go
  2. 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:

  1. CLI flags still work (masking the problem during development)
  2. 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'

Metadata

Metadata

Assignees

Labels

area/v3relates to / is being considered for v3kind/documentationdescribes a documentation updatestatus/triagemaintainers still need to look into this

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions