Skip to content

fix(guardrails): blocking hook rows fail open when the dispatcher process crashes #7681

Open

Description

@kyle-sexton

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.mjs gains a per-row opt-in --require-verdict flag: 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 runs plugins/guardrails/hooks/exec-bash.test.sh and it passes.
  • guardrails run-guards.sh writes 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 runs plugins/guardrails/hooks/run-guards.test.sh and it passes.
  • exec-bash.test.sh covers 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.mjs are regenerated with scripts/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 and scripts/check-changelog-fragments.sh --check, and both pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_011Wn4wZmSz8fVYJLNgFG8Zz

Activity

  1. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 16, 2026
  2. github-actions commented on Oct 16, 2026

    @github-actions
    Contributor

    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.

  3. added
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 16, 2026
  4. kyle-sexton commented on Oct 18, 2026

    @kyle-sexton
    MemberAuthor

    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: block to 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.

  5. added
    status: confirmedA maintainer confirmed the item is real and in scope; ready to be worked.
    work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.
    and removed
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    on Oct 22, 2026
  6. kyle-sexton commented on Oct 25, 2026

    @kyle-sexton
    MemberAuthor

    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 (or status: needs-decision) and say why in a comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: confirmedA maintainer confirmed the item is real and in scope; ready to be worked.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions