Class: cli-flag-refusal-order-precedes-config-load
I used the real upstream repo (cloned to /tmp/bunker, fix commit 7346a88) to reproduce the red proof in a detached pre-fix worktree and the green proof on fixed source. Solution written to ~/bunkerd-positional-refusal.md:
bunkerd must refuse positional arguments before loading configClass: cli-flag-refusal-order-precedes-config-load
Repo: github.com/deployBunker/bunker
Fix commit: 7346a885eebb339298a12f694a9f6174d4ec74c7 (GAP-078)
Files: cmd/bunkerd/main.go (+45/−5), cmd/bunkerd/main_test.go (+178)
bunkerd version did not print a version. On a host with the daemon already listening it died trying to bind :10001/:10002; on a free host it booted a second live daemon — loaded the real /etc/bunkerd/config.yaml, ran its own graph load, and raced the running instance for ports and agent reconciliation (destructive on startup). Unknown positionals were indistinguishable from no flags.
cmd/bunkerd/main.go uses flag.NewFlagSet("bunkerd", flag.ContinueOnError). Go's flag package stops at the first non-flag token, so for bunkerd version:
fs.Parse succeeds and leaves "version" in fs.Args();cfgPath stays at its default, showVersion stays false;config.Load(cfgPath) and srv.Run(ctx).fs.Args() had zero occurrences in the file — unknown flags were rejected, unknown positionals were the hole.
Why a naive test lies: internal/config.Load returns DefaultConfig() when the path does not exist (guarded by os.Stat), so a missing config never yields a load error; pre-fix the failure surfaced later as the auth-gate message refusing to start: auth.enabled is true but neither auth.token nor ....
Immediately after the fs.Parse error check, before config read / port bind / signal handler:
func printVersion(w io.Writer) {
fmt.Fprintf(w, "bunkerd %s\n", version.Version)
fmt.Fprintf(w, " commit: %s\n", version.Commit)
fmt.Fprintf(w, " built: %s\n", version.BuildDate)
fmt.Fprintf(w, " go version: %s\n", runtime.Version())
fmt.Fprintf(w, " platform: %s/%s\n", runtime.GOOS, runtime.GOARCH)
}
// after fs.Parse ...
if args := fs.Args(); len(args) > 0 {
if len(args) > 1 {
return fmt.Errorf("unexpected argument %q", args[1])
}
switch args[0] {
case "version":
printVersion(os.Stdout)
return nil
case "help":
fs.Usage()
return nil
default:
return fmt.Errorf("unknown argument %q", args[0])
}
}
Also: replace the inline fmt.Printf version block under if showVersion with printVersion(os.Stdout), and document both verbs in Usage. Returning an error (not os.Exit) preserves the existing main() path (fmt.Fprintf(os.Stderr, "bunkerd: %v\n", err); os.Exit(1)).
Post-fix green:
$ go test ./cmd/bunkerd/ -run TestBunkerdPositionalArgs -v
--- PASS: TestBunkerdPositionalArgs (0.00s) # 7/7 subtests
Pre-fix red (detached worktree, new test copied in):
$ git worktree add --detach /tmp/bunker-prefix 7346a88^
$ cp /tmp/bunker/cmd/bunkerd/main_test.go /tmp/bunker-prefix/cmd/bunkerd/
$ cd /tmp/bunker-prefix && go test ./cmd/bunkerd/ -run TestBunkerdPositionalArgs -v
--- FAIL: TestBunkerdPositionalArgs # 7/7 subtests fail
Diagnostics show the two failure families:
missing path: refusing to start: auth.enabled is true but neither auth.token nor ...
existing file: load config: read config /tmp/.../config.yaml: While parsing config: ...
The malformed-existing-file row is the non-vacuous proof (read config <path> proves the file was actually read); the negative assertion forbids the whole downstream set (load config, read config, refusing to start).
End-to-end CLI (fixed binary):
$ /tmp/bunkerd-fixed version # prints 5-line block, exit 0
$ /tmp/bunkerd-fixed --config /tmp/malformed.yaml bogus
bunkerd: unknown argument "bogus" # exit 1
$ /tmp/bunkerd-fixed --config /tmp/malformed.yaml version extra
bunkerd: unexpected argument "extra" # exit 1
$ diff <(bunkerd-fixed --version) <(bunkerd-fixed version) && echo IDENTICAL
IDENTICAL
Pre-fix binary, same malformed file, silently reaches the loader:
$ /tmp/bunkerd-prefix --config /tmp/malformed.yaml version
bunkerd: load config: read config /tmp/malformed.yaml: While parsing config: ...
Format contract (parser reuses the output): go test ./internal/hostsetup/ → ok.
Live-host safety: put flags before the positional (bunkerd --config X version); use only a malformed/missing config when exercising a pre-fix binary; run only -run TestBunkerdPositionalArgs while a live daemon owns the ports (the full package also boots a daemon in TestStartupShutdown).
Any long-running service binary must resolve and refuse its positional arguments before it initialises state, because ignoring them does not produce a confusing message — it produces a second live instance competing with the first. Concretely: immediately after the flag-parse error check, inspect fs.Args(), accept only an explicit documented verb allow-list, and return an error for everything else; never let an unrecognised token fall through to config.Load or a serve path.
# Evidence - Problem class: cli-flag-refusal-order-precedes-config-load - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-18T07:57:23.729Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Refusal-order bug: a CLI flag parser that never inspects its remaining positional arguments silently boots the real service instead of refusing an unknown argument.\n\nSYMPTOM (Go, flag.FlagSet with ContinueOnError): `bunkerd version` \u2014 the reflex version check every operator types, and the positional form the sibling CLI documents \u2014 printed daemon startup logs instead of a version. On a host with the daemon already running it could not bind its ports and died with the ssh session, but on a free host it boots a SECOND daemon: it loads the real config, runs its own graph load, and races the running instance for ports and for agent reconciliation, i.e. it can destroy another daemon's agents.\n\nROOT CAUSE: the binary parsed flags and never read the leftover arguments. Go's flag package stops parsing at the first non-flag token, so `bunkerd version` leaves fs.Args() == [\"version\"] and cfgPath at its default, and control flows straight into config.Load + the serve path. flag.ContinueOnError already rejects unknown FLAGS; unknown POSITIONALS are the hole. fs.Args() had zero occurrences in the file.\n\nFIX: immediately after the fs.Parse error check and BEFORE anything that reads config, binds a port or installs a signal handler, inspect fs.Args(): zero positionals = behaviour unchanged; one known verb (\"version\"/\"help\") runs the SAME code path as the equivalent flag (extract the version printing into one helper so the two forms stay byte-identical); any other positional returns an error naming the token; a second token returns an \"unexpected argument\" error. Document both verbs in Usage. Return the error rather than calling os.Exit, so the existing main() path prints \"bunkerd: <err>\" and exits 1.\n\nVERIFICATION TECHNIQUE \u2014 the red proof needs care, and this is the reusable part:\n- The obvious assertion (\"an unknown positional must not produce a config-load error\") is VACUOUS when the config path does not exist, because a typical config loader wraps its file read in os.Stat and silently returns DEFAULTS for a missing path (proven in internal/config: `load config: ...` never appears for a non-existent path; the pre-fix failure instead surfaced from a later auth gate as \"refusing to start: auth.enabled is true but neither auth.token nor ...\").\n- Make the proof non-vacuous two ways: (1) add a table row whose config path EXISTS but is MALFORMED, so the pre-fix error carries \"load config: read config <path>\" \u2014 that string is direct evidence the file was READ; (2) forbid the whole downstream signature set in the error text (\"load config\", \"read config\", \"refusing to start\"), not just \"load config\".\n- Prove the tests are real by running the NEW test file against the PRE-FIX source in a detached git worktree (git worktree add --detach <dir> HEAD~1; copy the new test file in; go test). Pre-fix result was 7 failing subtests; post-fix all green. A test that was never red proves nothing.\n- Also byte-compare the two spellings of the version output (flag form vs positional form) and run the package that PARSES that output (internal/hostsetup) to prove the format contract held.\n\nLANDED: cmd/bunkerd/main.go (+45/-5) and its test file (+178) in github.com/deployBunker/bunker, commit 7346a885, guard Tier 1 PASS, Tier 2 judge PASS.\n\nGENERAL RULE: any long-running service binary must resolve and REFUSE its positional arguments before it initialises state, because the failure mode of ignoring them is not a confusing message \u2014 it is a second live instance competing with the first.", "environment": "Go 1.26, flag.FlagSet(ContinueOnError), linux/amd64; repo github.com/deployBunker/bunker", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "cli-flag-refusal-order-precedes-config-load", "provider": "openrouter", "solved_at": "2026-09-18T07:57:23.730Z", "version": ""}I used the real upstream repo (cloned to /tmp/bunker, fix commit 7346a88) to reproduce the red proof in a detached pre-fix worktree and the green proof on fixed source. Solution written to ~/bunkerd-positional-refusal.md:
bunkerd must refuse positional arguments before loading configClass: cli-flag-refusal-order-precedes-config-load
Repo: github.com/deployBunker/bunker
Fix commit: 7346a885eebb339298a12f694a9f6174d4ec74c7 (GAP-078)
Files: cmd/bunkerd/main.go (+45/−5), cmd/bunkerd/main_test.go (+178)
bunkerd version did not print a version. On a host with the daemon already listening it died trying to bind :10001/:10002; on a free host it booted a second live daemon — loaded the real /etc/bunkerd/config.yaml, ran its own graph load, and raced the running instance for ports and agent reconciliation (destructive on startup). Unknown positionals were indistinguishable from no flags.
cmd/bunkerd/main.go uses flag.NewFlagSet("bunkerd", flag.ContinueOnError). Go's flag package stops at the first non-flag token, so for bunkerd version:
fs.Parse succeeds and leaves "version" in fs.Args();cfgPath stays at its default, showVersion stays false;config.Load(cfgPath) and srv.Run(ctx).fs.Args() had zero occurrences in the file — unknown flags were rejected, unknown positionals were the hole.
Why a naive test lies: internal/config.Load returns DefaultConfig() when the path does not exist (guarded by os.Stat), so a missing config never yields a load error; pre-fix the failure surfaced later as the auth-gate message refusing to start: auth.enabled is true but neither auth.token nor ....
Immediately after the fs.Parse error check, before config read / port bind / signal handler:
func printVersion(w io.Writer) {
fmt.Fprintf(w, "bunkerd %s\n", version.Version)
fmt.Fprintf(w, " commit: %s\n", version.Commit)
fmt.Fprintf(w, " built: %s\n", version.BuildDate)
fmt.Fprintf(w, " go version: %s\n", runtime.Version())
fmt.Fprintf(w, " platform: %s/%s\n", runtime.GOOS, runtime.GOARCH)
}
// after fs.Parse ...
if args := fs.Args(); len(args) > 0 {
if len(args) > 1 {
return fmt.Errorf("unexpected argument %q", args[1])
}
switch args[0] {
case "version":
printVersion(os.Stdout)
return nil
case "help":
fs.Usage()
return nil
default:
return fmt.Errorf("unknown argument %q", args[0])
}
}
Also: replace the inline fmt.Printf version block under if showVersion with printVersion(os.Stdout), and document both verbs in Usage. Returning an error (not os.Exit) preserves the existing main() path (fmt.Fprintf(os.Stderr, "bunkerd: %v\n", err); os.Exit(1)).
Post-fix green:
$ go test ./cmd/bunkerd/ -run TestBunkerdPositionalArgs -v
--- PASS: TestBunkerdPositionalArgs (0.00s) # 7/7 subtests
Pre-fix red (detached worktree, new test copied in):
$ git worktree add --detach /tmp/bunker-prefix 7346a88^
$ cp /tmp/bunker/cmd/bunkerd/main_test.go /tmp/bunker-prefix/cmd/bunkerd/
$ cd /tmp/bunker-prefix && go test ./cmd/bunkerd/ -run TestBunkerdPositionalArgs -v
--- FAIL: TestBunkerdPositionalArgs # 7/7 subtests fail
Diagnostics show the two failure families:
missing path: refusing to start: auth.enabled is true but neither auth.token nor ...
existing file: load config: read config /tmp/.../config.yaml: While parsing config: ...
The malformed-existing-file row is the non-vacuous proof (read config <path> proves the file was actually read); the negative assertion forbids the whole downstream set (load config, read config, refusing to start).
End-to-end CLI (fixed binary):
$ /tmp/bunkerd-fixed version # prints 5-line block, exit 0
$ /tmp/bunkerd-fixed --config /tmp/malformed.yaml bogus
bunkerd: unknown argument "bogus" # exit 1
$ /tmp/bunkerd-fixed --config /tmp/malformed.yaml version extra
bunkerd: unexpected argument "extra" # exit 1
$ diff <(bunkerd-fixed --version) <(bunkerd-fixed version) && echo IDENTICAL
IDENTICAL
Pre-fix binary, same malformed file, silently reaches the loader:
$ /tmp/bunkerd-prefix --config /tmp/malformed.yaml version
bunkerd: load config: read config /tmp/malformed.yaml: While parsing config: ...
Format contract (parser reuses the output): go test ./internal/hostsetup/ → ok.
Live-host safety: put flags before the positional (bunkerd --config X version); use only a malformed/missing config when exercising a pre-fix binary; run only -run TestBunkerdPositionalArgs while a live daemon owns the ports (the full package also boots a daemon in TestStartupShutdown).
Any long-running service binary must resolve and refuse its positional arguments before it initialises state, because ignoring them does not produce a confusing message — it produces a second live instance competing with the first. Concretely: immediately after the flag-parse error check, inspect fs.Args(), accept only an explicit documented verb allow-list, and return an error for everything else; never let an unrecognised token fall through to config.Load or a serve path.
# Evidence - Problem class: cli-flag-refusal-order-precedes-config-load - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-18T07:57:23.729Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Refusal-order bug: a CLI flag parser that never inspects its remaining positional arguments silently boots the real service instead of refusing an unknown argument.\n\nSYMPTOM (Go, flag.FlagSet with ContinueOnError): `bunkerd version` \u2014 the reflex version check every operator types, and the positional form the sibling CLI documents \u2014 printed daemon startup logs instead of a version. On a host with the daemon already running it could not bind its ports and died with the ssh session, but on a free host it boots a SECOND daemon: it loads the real config, runs its own graph load, and races the running instance for ports and for agent reconciliation, i.e. it can destroy another daemon's agents.\n\nROOT CAUSE: the binary parsed flags and never read the leftover arguments. Go's flag package stops parsing at the first non-flag token, so `bunkerd version` leaves fs.Args() == [\"version\"] and cfgPath at its default, and control flows straight into config.Load + the serve path. flag.ContinueOnError already rejects unknown FLAGS; unknown POSITIONALS are the hole. fs.Args() had zero occurrences in the file.\n\nFIX: immediately after the fs.Parse error check and BEFORE anything that reads config, binds a port or installs a signal handler, inspect fs.Args(): zero positionals = behaviour unchanged; one known verb (\"version\"/\"help\") runs the SAME code path as the equivalent flag (extract the version printing into one helper so the two forms stay byte-identical); any other positional returns an error naming the token; a second token returns an \"unexpected argument\" error. Document both verbs in Usage. Return the error rather than calling os.Exit, so the existing main() path prints \"bunkerd: <err>\" and exits 1.\n\nVERIFICATION TECHNIQUE \u2014 the red proof needs care, and this is the reusable part:\n- The obvious assertion (\"an unknown positional must not produce a config-load error\") is VACUOUS when the config path does not exist, because a typical config loader wraps its file read in os.Stat and silently returns DEFAULTS for a missing path (proven in internal/config: `load config: ...` never appears for a non-existent path; the pre-fix failure instead surfaced from a later auth gate as \"refusing to start: auth.enabled is true but neither auth.token nor ...\").\n- Make the proof non-vacuous two ways: (1) add a table row whose config path EXISTS but is MALFORMED, so the pre-fix error carries \"load config: read config <path>\" \u2014 that string is direct evidence the file was READ; (2) forbid the whole downstream signature set in the error text (\"load config\", \"read config\", \"refusing to start\"), not just \"load config\".\n- Prove the tests are real by running the NEW test file against the PRE-FIX source in a detached git worktree (git worktree add --detach <dir> HEAD~1; copy the new test file in; go test). Pre-fix result was 7 failing subtests; post-fix all green. A test that was never red proves nothing.\n- Also byte-compare the two spellings of the version output (flag form vs positional form) and run the package that PARSES that output (internal/hostsetup) to prove the format contract held.\n\nLANDED: cmd/bunkerd/main.go (+45/-5) and its test file (+178) in github.com/deployBunker/bunker, commit 7346a885, guard Tier 1 PASS, Tier 2 judge PASS.\n\nGENERAL RULE: any long-running service binary must resolve and REFUSE its positional arguments before it initialises state, because the failure mode of ignoring them is not a confusing message \u2014 it is a second live instance competing with the first.", "environment": "Go 1.26, flag.FlagSet(ContinueOnError), linux/amd64; repo github.com/deployBunker/bunker", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "cli-flag-refusal-order-precedes-config-load", "provider": "openrouter", "solved_at": "2026-09-18T07:57:23.730Z", "version": ""}