Repository navigation
fix(guardrails): blocking hook rows fail open when the dispatcher process crashes #7681
Description
Refs: #7627
Summary
On Windows Git Bash, a bash process that crashes (for example a stack overflow, 0xc0000005) is reported to its parent as exit 0 with empty stdout. The node launcher lib/exec-bash.mjs, vendored into 24 plugins, then returns 0, so Claude Code allows the tool call. On Linux, a child killed by a signal becomes exit 1 in the launcher, which does not block either.
#7627 fixes the case that reproduced: run-guards.sh stacks each guard's frames under the next guard. It also makes the dispatcher deny when a verdict it already recorded would be lost. A crash of the dispatcher itself before any guard blocks still fails open.
Update (2026-10-21): #8674 shipped --fail-closed for blocking rows: a signal or abnormal exit is now denied (lib/exec-bash.mjs:67-75). Still open: the silent exit 0 with no verdict.
Evidence
Reproduced on melo-lap-001 with Git Bash 5.3.15: an attended reset --hard plus an alias chain past the budget, on the pre-fix dispatcher, gave rc=0 stdout= and node launcher rc=0. Details are in #7627's Windows diagnosis (commit 2f647a4).
Fix
Require a verdict for blocking rows: the dispatcher writes an end marker, and the launcher treats a missing marker, a signal, or an abnormal exit as deny for rows declared blocking. Consider onFailure: block on the guardrails rows too; it covers exit 1, but not the silent exit 0 on Windows. This changes the shared launcher contract across 24 plugins.
Owner decision (2026-10-18): option A, the verdict-marker contract, opt-in per row so non-blocking rows behave as today; after #7896 (merged); structural, so a human builds or merges it.
Split to #8992: repeat the melo-lap-001 crash repro on Windows Git Bash and confirm a deny.
Done when
-
lib/exec-bash.mjsgains a per-row opt-in--require-verdictflag: the launcher hands the child a marker path, and a child exit 0 with no marker written exits 2 with one stderr line. Rows that do not pass the flag behave as today. Check: the PR's CI runsplugins/guardrails/hooks/exec-bash.test.shand it passes. - guardrails
run-guards.shwrites the marker as its last action, and the five guardrails rows that pass--fail-closed(plugins/guardrails/hooks/hooks.json) also pass--require-verdict. Check: the PR's CI runsplugins/guardrails/hooks/run-guards.test.shand it passes. -
exec-bash.test.shcovers exit 0 with no marker (deny), exit 0 with the marker (allow), and a row without the flag (unchanged). Check: as above. - The 24 registered copies of
lib/exec-bash.mjsare regenerated withscripts/sync-shared-copies.sh, with a fragment for every carrier (scripts/new-changelog-fragment.sh --carriers-of lib/exec-bash.mjs minor). Check: the PR's CI runs the shared-copy sync check andscripts/check-changelog-fragments.sh --check, and both pass.
🤖 Generated with Claude Code
Activity
github-actions commentedon Oct 16, 2026
Thanks for the report. Automated triage handed this issue to a maintainer.
Proposed work class: work-class: structural
Basis: automated intake triage; a maintainer confirms or changes it.
Automated triage. A maintainer reviews it.
kyle-sexton commentedon Oct 18, 2026
Decision recorded.
Question: #7681: how should a blocking hook row fail when its bash dispatcher crashes or exits with no verdict? Options: A. Verdict-marker contract: the dispatcher writes an end marker, and the shared launcher denies a row declared blocking on a missing marker, a signal, or an abnormal exit. Covers the Windows silent exit 0. Changes the contract for 24 plugins. / B. Launcher-only: for rows declared blocking, map a signal or abnormal exit to deny, with no dispatcher marker. Covers Linux signals, not the Windows silent exit 0. / C. Add
onFailure: blockto the guardrails rows only (Claude Code hook setting; UNVERIFIED that it exists, not checked against the hooks docs this run). Per the issue it covers exit 1, not the silent exit 0. / D. Accept fail-open for a dispatcher crash and document it.Owner's answer: A, opt-in per row, so non-blocking rows behave as today; land it after #7896 merges; a human builds it or merges it, since it is structural.
The owner accepted this in an attended backlog-shaping session on 2026-10-18. The batch question: "Accept the 10 decisions with my two changes: #6730 option C (precise parsing, not blanket blocking) and #7639 a verifier proposes each case with your spot-check?" The owner answered: "Accept, with my changes (Recommended)".
Posted by AI from the owner's answer in that session.
kyle-sexton commentedon Oct 25, 2026
Released for re-triage under the owner's 2026-10-24 rule: an issue stops for a person only for an intent or vision question, a security call, a host or attended run, or spend outside prepaid credit. The hold label came off and intake triage runs again now; it re-applies a hold, with a numbered question, when one of those applies.
To hold this issue again, add
needs-human(orstatus: needs-decision) and say why in a comment.