From a4f2124a4f8c56d0b58d57f7d6789bcda5dbd8c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 17:18:21 +0000 Subject: [PATCH] tooling(hooks): the guards' escape hatch names where it actually works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every guard's refusal ended by telling the reader to re-run the blocked thing with OS_ALLOW_*=1 as a command prefix. A VAR=1 prefix sets the variable in the environment of THAT COMMAND; the hook is not that command, and it reads the variable from its own environment — so the one place an operator naturally applies the printed remedy is the one place it cannot work, and a remedy that does not work is an invitation to route around the guard. The hatch stays: it is the repo's sanctioned deliberate exception, named in CLAUDE.md. What changes is that the sentence now names the environment the hook actually reads and says plainly that a prefix never reaches it. One wording, byte-identical across all six message sites, so the objectui twins can mirror it mechanically. Message text only — no predicate, no env check, no block/allow verdict is touched. Each matrix gains the absence assertion (the dead prefix remedy is gone) and its positive twin (the sentence names the environment the hook reads); the Bash matrix also pins the card's own reproduction, that the prefix spelled as the old message told the reader to spell it still blocks. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox --- .../hooks/guard-governed-enqueue.selftest.sh | 8 ++++ .claude/hooks/guard-governed-enqueue.sh | 5 +- .../guard-main-checkout-bash.selftest.sh | 47 +++++++++++++++++++ .claude/hooks/guard-main-checkout-bash.sh | 5 +- .claude/hooks/guard-main-checkout.selftest.sh | 46 ++++++++++++++++++ .claude/hooks/guard-main-checkout.sh | 10 +++- .claude/hooks/guard-shared-stash.selftest.sh | 39 +++++++++++++++ .claude/hooks/guard-shared-stash.sh | 5 +- .claude/hooks/guard-tree-enum.selftest.sh | 41 ++++++++++++++++ .claude/hooks/guard-tree-enum.sh | 5 +- 10 files changed, 205 insertions(+), 6 deletions(-) diff --git a/.claude/hooks/guard-governed-enqueue.selftest.sh b/.claude/hooks/guard-governed-enqueue.selftest.sh index 6f19676ddf..46a88dea88 100755 --- a/.claude/hooks/guard-governed-enqueue.selftest.sh +++ b/.claude/hooks/guard-governed-enqueue.selftest.sh @@ -161,6 +161,14 @@ expect_says 'does NOT re-run on a later approval' 'the no-re-run reason is state "$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED" expect_says 'OS_ALLOW_GOVERNED_ENQUEUE=1' 'the deliberate exception is named' \ "$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED" +# …and it names WHERE that variable has to be set. A VAR=1 prefix sets the variable in the +# environment of THAT COMMAND; this hook is not that command, and it reads its own +# environment, so a prefix never reaches it (#15971). The `lacks` row is the shape shared +# with the other four matrices, where the dead prefix remedy was actually printed. +expect_lacks 're-run with' 'no prefix remedy is offered for the exception' \ + "$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED" +expect_says 'hook itself runs in' 'the exception names the environment this hook reads' \ + "$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED" expect_says "$HEAD_SHA" 'the current head sha is named so the reader knows which PR state this is' \ "$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED" expect_says 'does NOT have to sit on the' 'the remedy states the 2026-09-04 predicate, not the retired sha pin' \ diff --git a/.claude/hooks/guard-governed-enqueue.sh b/.claude/hooks/guard-governed-enqueue.sh index 112b8c5748..961088ab3d 100755 --- a/.claude/hooks/guard-governed-enqueue.sh +++ b/.claude/hooks/guard-governed-enqueue.sh @@ -555,6 +555,9 @@ approvals, decided by the register (check-governed-merges.mjs), not here. Verdict source: check-governed-merges.mjs --test (governed) + authorizedApprovalVerdict/GOVERNED_APPROVERS from check-governed-queue-guard.mjs. -Deliberate exception (you know this one is right): OS_ALLOW_GOVERNED_ENQUEUE=1. +Deliberate exception (you know this one is right): set OS_ALLOW_GOVERNED_ENQUEUE=1 in the +environment this hook itself runs in — a local settings "env" entry, or whatever this +agent process was started with. A VAR=1 prefix on a command sets it for that command +only, and this hook is not that command, so a prefix never reaches it. EOF exit 2 diff --git a/.claude/hooks/guard-main-checkout-bash.selftest.sh b/.claude/hooks/guard-main-checkout-bash.selftest.sh index 9de71357b1..d5943e7616 100755 --- a/.claude/hooks/guard-main-checkout-bash.selftest.sh +++ b/.claude/hooks/guard-main-checkout-bash.selftest.sh @@ -84,6 +84,34 @@ expect() { # expect [env…] fi } +stderr_of() { # stderr_of [env…] -> the refusal text an agent actually reads + local cmd="$1"; shift + local payload + payload="$(jq -nc --arg c "$cmd" --arg w "$CWD" \ + '{cwd:$w,tool_name:"Bash",tool_input:{command:$c}}')" + printf '%s' "$payload" | env "$@" "$hook" 2>&1 >/dev/null +} + +says() { # says