Framework bootstrap vs child PersistentPreRunE¶
- Authors
- Matt Cockayne, Claude (claude-fable-5) (AI drafting assistant)
- Date
- 2026-06-12
- Status
- IMPLEMENTED (open questions resolved in review 2026-06-12; implemented 2026-06-13)
Summary¶
The framework's bootstrap (config loading, log-level setup, feature-flag resolution, telemetry collector construction, update check) runs in the root command's PersistentPreRunE. Cobra executes only the closest PersistentPreRunE in the command chain β so the moment a downstream tool defines a PersistentPreRunE on a subcommand, the root bootstrap is silently skipped for that subtree. The docs steer authors straight into this. This spec makes the framework bootstrap run regardless and warns authors when their hook would otherwise shadow it.
Finding addressed (from docs/development/reports/codebase-audit-2026-06-12.md Β§3.6):
child-persistentprerune-silently-disables-framework-setupβ Medium, missing-feature
Motivation¶
This is a silent, surprising footgun in the framework's core contract. A downstream author adds a subcommand-level PersistentPreRunE (a perfectly normal cobra pattern) and, with no error or warning, loses config loading, telemetry, and the update check for that subtree. The failure mode is invisible until something that depends on bootstrap (e.g. props.Config) is nil or stale at runtime.
Design decisions¶
D1 β Enable cobra.EnableTraverseRunHooks (resolved O1)¶
Set cobra.EnableTraverseRunHooks = true in NewCmdRoot. Cobra (v1.10.2, confirmed present) then runs all PersistentPreRunE hooks from root to leaf rather than only the closest, so the root bootstrap always runs and a downstream child hook runs after it (resolved O2: bootstrap always first). Verified during review that the flag is not currently set anywhere in the codebase β the bug is live and unmitigated.
This is chosen over moving bootstrap out of PersistentPreRunE (see Alternative considered) because it is a one-liner cobra fully supports, and crucially it keeps bootstrap in PersistentPreRunE where cobra has already resolved --debug/--config/--ci β avoiding the early-flag-resolution complexity the alternative would incur. The "process-global var" caveat is negligible for GTB: only the root command defines a persistent pre-run hook, so rootβleaf traversal changes nothing for the framework's own tree.
D2 β Bootstrap always runs; no opt-out (resolved O4)¶
Bootstrap runs for every command β there is no bootstrap-exempt annotation. That is the whole point of the fix (bootstrap must never be silently skipped); an "offline" subcommand simply doesn't use the parts of Props it doesn't need. Adding an opt-out would reintroduce the footgun.
D3 β Warn on shadowing; document the ordering¶
When constructing the command tree, detect a downstream PersistentPreRunE and emit a one-time debug log noting that it runs after the framework bootstrap (rootβleaf), so authors understand the order rather than being surprised. Update the docs/how-to that currently steer authors into the trap, documenting the guaranteed bootstrap-then-child ordering.
Alternative considered β bootstrap in Execute / outermost middleware¶
Run bootstrap from the Execute wrapper (or as the outermost registered middleware) before cobra dispatches to any PersistentPreRunE. More durable (no dependency on cobra hook resolution) and would unify with the sealed middleware registry β but a real refactor: bootstrap currently reads cobra-resolved flags during PersistentPreRunE, so this requires resolving the persistent flag set early and special-casing --help/completion so they don't trigger bootstrap. Rejected as disproportionate for a pre-1.0 framework; revisit only if the middleware unification becomes desirable on its own merits (see the deferred O3 note).
Open questions¶
O1, O2, O4 resolved during review (2026-06-12); O3 deferred.
- O1 β Approach. Resolved: Option A β
cobra.EnableTraverseRunHooks = true. Confirmed not currently set; cobra v1.10.2 supports it. (D1) - O2 β Ordering guarantee. Resolved: bootstrap always first, child hooks after (rootβleaf). (D1)
- O3 β Middleware unification (deferred). Whether bootstrap should ultimately be the outermost middleware (unifying with
command-composition-registration, 2026-05-30) rather than aPersistentPreRunEis a separate architectural question, not needed for this fix. Tracked as a future consideration, not a blocker. - O4 β Opt-out. Resolved: no opt-out β bootstrap always runs. (D2)
Verification plan¶
- Unit β a command tree with a child
PersistentPreRunEstill hasprops.Config/props.Collectorpopulated when the child runs. - Regression β the scenario the audit describes: define a subcommand hook, assert bootstrap still ran.
- Ordering β assert the documented hook order.
- E2E β a scaffolded tool with a custom subcommand hook resolves config correctly.
- Docs β fix the how-to that steers authors into the trap; document the guaranteed ordering.
Out of scope¶
- Rewriting the middleware registry (referenced, not replaced).
- Per-command bootstrap opt-out beyond what O4 resolves.
Related¶
- 2026-06-12 codebase audit Β§3.6
- Plan 3 β improvements Phase 16
- command composition registration spec
docs/concepts/command-middleware.md,docs/how-to/nested-subcommands.md