Development record
Hook sub-verb names are now a compatibility contract, not a cushion. iss-266 makes every parent runnable, so an unknown sub-verb exits 2 instead of printing help at exit 0. On the Claude Code hook plane exit 2 is the BLOCKING status: exit 2 on UserPromptSubmit blocks the prompt, and exit 2 from 'guard hook' on PreToolUse blocks every Bash tool call -- and the PreToolUse wrapper in hooks/hooks.json treats 2 as a recognised code, so its 'FAILED TO RUN ... UNGUARDED' safety net does not fire. hooks.json ships with the plugin git clone while the binary is fetched from the latest release by hooks/bootstrap.sh, so the two can skew. No live instance today (every sub-verb hooks.json names exists at v0.5.1), and the fail-closed direction is intended. But a future rename of 'guard hook' or any 'hook <sub-verb>' must now carry an alias rather than rely on the old silent exit-0 cushion: against a skewed hooks.json/binary pair it would brick a session where it previously degraded silently. Found by the security review of the iss-266 fix.
Hook sub-verb names are now a compatibility contract, not a cushion. iss-266 makes every parent runnable, so an unknown sub-verb exits 2 instead of printing help at exit 0. On the Claude Code hook plane exit 2 is the BLOCKING status: exit 2 on UserPromptSubmit blocks the prompt, and exit 2 from 'guard hook' on PreToolUse blocks every Bash tool call -- and the PreToolUse wrapper in hooks/hooks.json treats 2 as a recognised code, so its 'FAILED TO RUN ... UNGUARDED' safety net does not fire. hooks.json ships with the plugin git clone while the binary is fetched from the latest release by hooks/bootstrap.sh, so the two can skew. No live instance today (every sub-verb hooks.json names exists at v0.5.1), and the fail-closed direction is intended. But a future rename of 'guard hook' or any 'hook <sub-verb>' must now carry an alias rather than rely on the old silent exit-0 cushion: against a skewed hooks.json/binary pair it would brick a session where it previously degraded silently. Found by the security review of the iss-266 fix.
Frontmatter
| id | iss-267 |
|---|---|
| type | issue |
| lifecycle | resolved |
| severity | minor |
.abcd/work/issues/resolved/iss-267-hook-sub-verb-names-are-now-a.md
open on GitHub ↗ · commit history ↗
mentions3
- dcn-26081700000000032026-08-17 — Command parents owe TWO guarantees, not one (iss-266, #293).
- iss-266cobra parents with no RunE are not Runnable, so cobra prints help and exits 0 WITHOUT validating args: an unknown sub-verb reads as success to a script. Confirmed on 'abcd docs nonsense' and 'abcd guard nonsense' (both exit 0); 'abcd memory nonsense' and 'abcd history nonsense' exit 1 because those parents have a RunE. spc-30 fixed the disembark parent (a retired sub-verb spelling must refuse, not silently succeed) by giving it a RunE that prints help, so cobra.NoArgs then refuses a stray token at exit 2. The same one-line fix is owed to every other RunE-less parent — sweep them so no unknown sub-verb anywhere exits 0.
- iss-269The hook plane's fail-open fix covers sub-verbs only, not flags or leaf positionals. iss-267 routes the guard and hook PARENTS through failOpenNoArgs so an unknown sub-verb refuses at exit 1 rather than the host's blocking status 2. Three neighbouring paths still exit 2: an unknown TOP-LEVEL token hits the root's Args validator (root's exit-2 contract is pinned by 04-surfaces/08-abcd.md, lint_surface_test.go and root_dispatch_test.go, so inverting it is a larger change), a stray positional on a leaf such as 'guard hook' exits 2, and ANY unknown flag exits 2 via FlagErrorFunc which failOpenNoArgs does not touch. None is reachable from today's hooks.json, but each becomes reachable the moment the manifest changes shape: if a future hooks.json adds a flag (e.g. 'abcd guard hook --strict') and skews against an older binary, iss-267 returns verbatim -- same brick, same wrapper blind spot -- and TestHooksManifestNamesLiveSubverbs would not catch it, because its regex stops at the first non-subverb token and never inspects flags. Also worth deciding: whether a top-level rename of 'guard' itself (this release already renames audit to lint) should be covered, since a skewed pair would then block every Bash call through the root validator. Options: extend failOpenNoArgs to the leaf and to FlagErrorFunc on the hook plane; extend the manifest test to pin flags too; or rely solely on the manifest-freeze doctrine and document it. Found by the security and ruthless reviews of the iss-267 fix.