Repository navigation
bug(hooks): a hook emits permissionDecision "warn", which is not in the contract and does nothing (#15956 family) #16079
Description
Summary
.claude/hooks/block-dangerous-commands.sh:347 emits
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"warn", ...}}warn is not a value in the PreToolUse contract. The harness recognises
allow, deny and ask. An unrecognised value is not a weaker decision — it is
no decision, so this emission does nothing at all.
It is inert twice over:
| the value | warn is not in the contract, so nothing acts on it |
| the channel | it is written to stderr, and stdout is what carries a decision; the script then falls through to exit 0 at :410, where stderr is not surfaced |
So the advisory reaches neither the harness nor the operator.
Why it matters beyond one dead line
The advisory exists to warn when a commit is made outside a per-task workspace,
citing #6512 — a real coordination hazard, and the reason the advisory was
written. What shipped cannot deliver that warning, and nothing in the source
distinguishes it from one that does: it is well-formed JSON, correctly shaped,
sitting beside three sibling emissions in the same file that are all real
decisions and all work.
This is the #15956 family exactly. There the defect was an ask whose exit code
discarded it; here it is a decision whose value is not in the contract. Both
produce a hook that looks correct at every site a reader would check.
Found by the guard, which could not see it
repo_tests/hook_decision_exit_codes_15956_test.py swept every hook and reported
green over this line, because DECISION_RE enumerated ask|allow|deny — the
valid values. A detector keyed on the valid set cannot see an invalid member
of it, which is the one thing it most needs to catch. PR #15996 widens the regex
to match any value and asserts membership, so this fails loudly.
That PR carries this instance as a ratchet entry pinned to this issue rather than
fixing it, because the remedy is a behaviour decision (below) and bundling it
would make that PR's review harder than it should be. The ratchet asserts the
line is still inert, so it fails the moment this is fixed and cannot be
forgotten.
The decision this needs
Restoring the intent is not a one-line change, because a PreToolUse hook has no
advisory channel that is both non-blocking and visible:
ask— visible and correct in shape, but turns every commit made outside a
per-task workspace into a prompt. That is a real behaviour change and should be
chosen deliberately, not arrived at.- plain text on stderr, no JSON — honest about being a message rather than a
decision, but onexit 0it stays about as invisible as today. - drop it — only defensible if bug(tools): parallel agent switches main session's branch mid-edit, causing commits to land on wrong branch #6512's hazard is now covered elsewhere.
Acceptance criteria
- No hook emits a
permissionDecisionvalue outsideallow/deny/ask - The chosen remedy is stated against bug(tools): parallel agent switches main session's branch mid-edit, causing commits to land on wrong branch #6512's hazard, not just the dead line
- If the advisory is kept, it is demonstrably reachable by an operator — the
claim is evidenced, not assumed from the code shape - The ratchet entry in
hook_decision_exit_codes_15956_test.pyis removed in
the same PR that fixes this
Activity
github-actions commentedon Sep 8, 2026
👋 Auto-triage could not confidently classify this issue (no confident signal). Could a maintainer add the appropriate labels (
frontend,backend,infrastructure,docs,testingandgood-first-issue,intermediate,advanced)?test(hooks): read the decision's value, not just its exit and channel (…
fix(hooks): a non-deny permissionDecision must exit 0 to be read (#15956