Skip to content

fix(cli): fail loudly on unrecognized subcommands under any command group - #101

Merged
ysyneu merged 2 commits into
feat/ai-srefrom
audit-fix/unknown-verb-hard-fail
Jul 27, 2026
Merged

fix(cli): fail loudly on unrecognized subcommands under any command group#101
ysyneu merged 2 commits into
feat/ai-srefrom
audit-fix/unknown-verb-hard-fail

Conversation

@ysyneu

@ysyneu ysyneu commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem

A cobra command group (incident, oncall, automation, …) is registered as a
non-Runnable &cobra.Command{Use, Short}. For a non-Runnable command, cobra's
execute() returns flag.ErrHelp before ValidateArgs and before any
PersistentPreRun hook ever runs, and ExecuteC translates that into
"print help, return nil".

So a typo'd or hallucinated subcommand printed the group's help text and exited 0:

$ fduty incident bogusverb ; echo $?
<help text>
0

An agent reading exit 0 concludes the command succeeded and moves on with no data.
The leftover argument never reaches an Args validator, so nothing else catches it.

Fix

newGroupCmd gives every group a real RunE (making it Runnable, so cobra proceeds
to argument validation) plus Args: groupUnknownSubcommand, which rejects an
unrecognized subcommand with a non-zero exit and a Levenshtein suggestion:

$ fduty incident bogusverb ; echo $?
Error: unknown command "bogusverb" for "flashduty incident"
1
$ fduty oncall schedule lsit
Error: unknown command "lsit" for "flashduty oncall schedule". Did you mean this?
        list

A bare group with no subcommand still prints help and exits 0 — unchanged.
groupUnknownSubcommand mirrors cobra's own SuggestionsMinimumDistance default.

Root is deliberately left alone: it has no parent, so cobra's built-in legacyArgs
already rejects unknown top-level commands.

Scope

grep -rln '\.AddCommand(' internal/cli/*.go lists exactly the 17 files touched here
plus root.go. Coverage is complete, not sampled.

Verification

  • go build ./..., go vet ./..., gofmt -l internal/cli/*.go — all clean
  • go test ./... -count=1 — 9/9 packages ok, 246/246 subtests pass, including 5 new
    tests in internal/cli/group_test.go
  • Built both binaries and smoke-tested live: the base binary reproduces the reported
    bug (help + exit 0); this branch exits 1 with a clear message. Also checked nested
    groups, legit subcommands, and a bare group.

Known side effect

Making groups Runnable means root's PersistentPreRunE/PersistentPostRun
(--output-format validation, auto-update check) now run for a bare group
invocation, which previously short-circuited earlier. The update check is gated on
isTerminalFn(stderr), false whenever stderr is piped — the normal agent/automation
case. Measured under a non-TTY: 7ms, no network call, byte-identical help output.

ysyneu added 2 commits July 27, 2026 03:06
…roup

Cobra's built-in "unknown command" detection (legacyArgs in args.go) only
raises an error for the root command; a non-root group's leftover args
silently pass Find(). Every command group in this CLI (alert, incident,
oncall schedule, incident war-room, the generated groups via genGroup, ...)
was declared as a bare &cobra.Command{Use, Short} with no RunE, so cobra
judged it non-Runnable and returned flag.ErrHelp — which ExecuteC always
turns into "print help, exit 0" regardless of SilenceErrors. A typo'd verb
(e.g. `incident list-alerts`, `incident list-timeline`, `alert detail`) looked
exactly like a successful run, so an agent guessing a subcommand name had no
signal that it guessed wrong.

Add a shared newGroupCmd constructor (command.go) that gives every group a
real Args validator (groupUnknownSubcommand, which reuses cobra's own
SuggestionsFor/SuggestionsMinimumDistance mechanism to append a "Did you mean
this?" block when a close match exists) plus a RunE that prints help. This
makes the group Runnable so cobra actually calls ValidateArgs on the leftover
token instead of discarding it, while running the group with no subcommand at
all is unchanged (help, exit 0).

Route every curated group constructor and the generated-command genGroup
helper through newGroupCmd so the fix applies uniformly across the whole
command tree instead of being copied into each file.
newGroupCmd (#101) gives every command group a real RunE so a mistyped
subcommand fails loudly, which made Build's `c.Runnable() && !c.Hidden`
predicate start emitting a card entry for all 23 groups (incident,
oncall, change, ...). A group only dispatches to its subcommands; it has
no behavior of its own, so documenting it as an invocable command is
wrong content, not just churn.

Key the predicate off HasSubCommands() instead: every node in the tree
with children is a pure container (no command mixes its own business
logic with child commands), so this cleanly separates groups from
leaves regardless of Runnable(). Regenerating all fences with this fix
reproduces the base branch's cards byte-for-byte.
@ysyneu
ysyneu merged commit 1f1f020 into feat/ai-sre Jul 27, 2026
12 checks passed
@ysyneu
ysyneu deleted the audit-fix/unknown-verb-hard-fail branch July 27, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant