You typed run -c config.yaml. The flag is registered. The error still says the config is missing. The tokens were on the command line. The root FlagSet never looked at them.
In this note, parse means stdlib flag / FlagSet consumption of argv until the first non-flag token (or --). It does not mean “scan the whole line for every - option.”
Wrong model: Parse scans argv for registered flags.
Actual model: Parse consumes a prefix of argv until a non-flag token.
run -c config.yaml
^^^
verb -> first non-flag -> root Parse stops
Root parse on the full argv leaves the verb in the middle, so everything after it is leftover args, not flags. Peel the verb, then parse the remainder offers only the tokens after the verb to the command FlagSet.
Invariant: flags that belong to a subcommand are bound only after the verb is stripped. Binding is a property of the post-verb parse path, not of “-c appeared in the typed line.”
The stop rule is the contract
Go’s flag package stops at the first non-flag argument by design. A subcommand verb is that argument. The moment Parse sees run, it stops. Later -c and the path become Args(). Your String("c", "", "config") still holds the empty default. The program is not lying when it says config is missing. It never bound the flag.
In this CLI grammar, globals before the verb belong to the root FlagSet; flags after the verb belong to the subcommand FlagSet. The library only knows which set you call Parse on. One parse of the full line does not cover both without that split.
If your CLI has no verb, root parse is fine. The failure mode starts when the grammar puts a word before the flags and the code still pretends argv is a flat option list.
Lab: root parse vs peel-then-parse
I ran go run ./lab/flag-parse-stops-at-verb.go in this repo on 31 August 2026. Argv after the binary name is shaped like run -c config.yaml. Stdlib flag only.
Naive root parse leaves -c empty:
root := flag.NewFlagSet("root", flag.ContinueOnError)
config := root.String("c", "", "config")
_ = root.Parse([]string{"run", "-c", "config.yaml"})
// *config == ""
// root.Args() == ["run", "-c", "config.yaml"]
Peel the verb, then parse the remainder:
cmd := flag.NewFlagSet("run", flag.ContinueOnError)
config := cmd.String("c", "", "config")
_ = cmd.Parse([]string{"-c", "config.yaml"})
// *config == "config.yaml"
Control: peel run with no -c → config stays empty. The fixture prints configSeen=false for the naive parse and true after peeling. That difference makes the stop rule observable.
What this fix does not buy
Peeling one verb does not design nested subcommands, mutual flag inheritance, or a replacement for cobra / spf13. It does not make unknown flags after -- into configs. It does not excuse a missing file once -c is bound.
Flags bind on the peeled remainder. Until then, shell history shows -c and the error still says the config is missing.