Flag-to-config binding¶
- Authors
- Matt Cockayne, Claude (claude-fable-5) (AI drafting assistant)
- Date
- 2026-06-12
- Status
- IMPLEMENTED (open questions resolved in review 2026-06-12; delivered 2026-06-13)
Summary¶
GTB documents a configuration precedence of CLI flags > env vars > file config > embedded assets > defaults. The flags tier is not implemented: there is no viper.BindPFlag anywhere, no binding helper, and built-ins like --debug are special-cased in configureLogging rather than flowing through Config. An arbitrary downstream flag therefore never overrides config. This spec adds a real binding facility so the documented precedence holds, and reconciles two docs that currently disagree on whether flags beat env vars.
Finding addressed (from docs/development/reports/codebase-audit-2026-06-12.md Β§3.6):
flag-to-config-binding-unimplementedβ Medium, missing-feature
Motivation¶
Downstream authors reasonably expect mytool --port 9090 to override port: 8080 in config, because the docs promise flags win. Today it silently does not, unless they hand-wire BindPFlag themselves. The special-casing of --debug/--ci is evidence of the missing general mechanism. Beyond the bug, two docs contradict each other on the flag-vs-env ordering β implementation forces us to pick one and document it precisely.
Design decisions¶
D1 β WithBoundFlags option¶
Add a props/root option that registers a set of pflags (or a whole *pflag.FlagSet) to be BindPFlag-ed onto the config container during config load, so Config.Get* reflects flag overrides at the documented precedence. Shape (illustrative, to be finalised):
The explicit map is the precise default. Additionally offer an optional convention helper that derives keys from flag names (--server-port β server.port) for authors who want zero-boilerplate binding (resolved O1).
D2 β Bind during config load, after file/env; only changed flags¶
Binding happens at the point config is assembled (after file + env layers are established) so viper's precedence (BindPFlag sits above env) yields the documented result. Only flags the user explicitly set are bound (flag.Changed) (resolved O3): a flag left at its default must not override config β binding defaults is viper's classic clobber footgun, which flag.Changed filtering neutralises. (O2: viper's documented order already matches the desired precedence, so no custom precedence implementation is needed.)
D3 β Fold the special-cased built-ins through the same path¶
--debug, --ci are currently read directly. Where it simplifies without changing behaviour, route them through the binding so there is one mechanism, not two. (Keep --debug's immediate log-level effect; only the config-visibility changes.)
D4 β Reconcile the docs¶
Pick the canonical precedence (recommend: flags > env > file > embedded > defaults, matching viper's Set/BindPFlag/AutomaticEnv order) and make docs/concepts/config.md and any conflicting how-to agree. This is a required deliverable, not optional.
D5 β Bind per-command flags too (resolved O4)¶
Bind both root/persistent flags and per-command flags. A command's own flags bind into a command-scoped config view (the Config visible to that command's RunE), so e.g. mytool serve --port 9090 overrides server.port for the serve command. Implementation: bind a command's flag set when that command runs (in the bootstrap path, which β per the bootstrap-prerun spec β now runs for every command), filtering by flag.Changed as in D2. Persistent/root flags bind once at the root; per-command flags bind per dispatch.
Open questions¶
All resolved during review (2026-06-12).
- O1 β Binding API. Resolved: explicit
WithBoundFlags(map[string]*pflag.Flag)as the precise default, plus an optional convention helper that derives keys from flag names (--server-portβserver.port) for ergonomics. (D1) - O2 β viper precedence. Resolved: viper's documented order already places
BindPFlagaboveAutomaticEnv(flag > env > config), so no custom precedence is needed β the real subtlety is the default-clobber gotcha, handled by O3. - O3 β changed vs default flags. Resolved: bind only flags the user explicitly set (
flag.Changed). A flag at its default must not override config β this neutralises viper's default-clobber behaviour. (D2) - O4 β Scope. Resolved: bind both root/persistent and per-command flags (per-command into a command-scoped config view), not root-only. (D5)
Verification plan¶
- Unit β with a bound flag set explicitly,
Config.GetInt("server.port")returns the flag value; unset flag does not override config (O3). - Precedence β flag beats env beats file, proven with all three set to different values.
- Built-ins β
--debugstill sets the log level;--ci/configcibehaviour unchanged. - Docs β the two precedence docs now agree and match the implementation; add a how-to.
Out of scope¶
- Changing the env-var prefix mechanism (
EnvPrefixstays as-is).
Related¶
- 2026-06-12 codebase audit Β§3.6
- Plan 3 β improvements Phase 16
docs/concepts/config.md,docs/components/config.md- config env-prefix spec