Skip to content

Commit c252041

Browse files
os-steveclaude
andauthored
tooling(hooks): the guards' escape hatch names where it actually works (#15987)
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. Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3140345 commit c252041

10 files changed

Lines changed: 205 additions & 6 deletions

.claude/hooks/guard-governed-enqueue.selftest.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,14 @@ expect_says 'does NOT re-run on a later approval' 'the no-re-run reason is state
161161
"$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED"
162162
expect_says 'OS_ALLOW_GOVERNED_ENQUEUE=1' 'the deliberate exception is named' \
163163
"$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED"
164+
# …and it names WHERE that variable has to be set. A VAR=1 prefix sets the variable in the
165+
# environment of THAT COMMAND; this hook is not that command, and it reads its own
166+
# environment, so a prefix never reaches it (#15971). The `lacks` row is the shape shared
167+
# with the other four matrices, where the dead prefix remedy was actually printed.
168+
expect_lacks 're-run with' 'no prefix remedy is offered for the exception' \
169+
"$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED"
170+
expect_says 'hook itself runs in' 'the exception names the environment this hook reads' \
171+
"$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED"
164172
expect_says "$HEAD_SHA" 'the current head sha is named so the reader knows which PR state this is' \
165173
"$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED"
166174
expect_says 'does NOT have to sit on the' 'the remedy states the 2026-09-04 predicate, not the retired sha pin' \

.claude/hooks/guard-governed-enqueue.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,9 @@ approvals, decided by the register (check-governed-merges.mjs), not here.
555555
556556
Verdict source: check-governed-merges.mjs --test (governed) +
557557
authorizedApprovalVerdict/GOVERNED_APPROVERS from check-governed-queue-guard.mjs.
558-
Deliberate exception (you know this one is right): OS_ALLOW_GOVERNED_ENQUEUE=1.
558+
Deliberate exception (you know this one is right): set OS_ALLOW_GOVERNED_ENQUEUE=1 in the
559+
environment this hook itself runs in — a local settings "env" entry, or whatever this
560+
agent process was started with. A VAR=1 prefix on a command sets it for that command
561+
only, and this hook is not that command, so a prefix never reaches it.
559562
EOF
560563
exit 2

.claude/hooks/guard-main-checkout-bash.selftest.sh

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,34 @@ expect() { # expect <block|allow> <command> [env…]
8484
fi
8585
}
8686

87+
stderr_of() { # stderr_of <command> [env…] -> the refusal text an agent actually reads
88+
local cmd="$1"; shift
89+
local payload
90+
payload="$(jq -nc --arg c "$cmd" --arg w "$CWD" \
91+
'{cwd:$w,tool_name:"Bash",tool_input:{command:$c}}')"
92+
printf '%s' "$payload" | env "$@" "$hook" 2>&1 >/dev/null
93+
}
94+
95+
says() { # says <needle> <label> <command> [env…]
96+
local needle="$1" label="$2" subject="$3"; shift 3
97+
local out; out="$(stderr_of "$subject" "$@")"
98+
case "$out" in
99+
*"$needle"*) pass=$((pass + 1)); printf ' ok says %s\n' "$label" ;;
100+
*) fail=$((fail + 1)); printf ' FAIL missing "%s" %s\n' "$needle" "$label" ;;
101+
esac
102+
}
103+
104+
lacks() { # lacks <needle> <label> <command> [env…]
105+
# The direction only an ABSENCE assertion can hold: a remedy that stopped being true stays
106+
# in the text a reader acts on long after the thing it described stopped working.
107+
local needle="$1" label="$2" subject="$3"; shift 3
108+
local out; out="$(stderr_of "$subject" "$@")"
109+
case "$out" in
110+
*"$needle"*) fail=$((fail + 1)); printf ' FAIL still says "%s" %s\n' "$needle" "$label" ;;
111+
*) pass=$((pass + 1)); printf ' ok lacks %s\n' "$label" ;;
112+
esac
113+
}
114+
87115
echo "== writes into the shared PRIMARY checkout are blocked =="
88116
CWD="$MAIN"
89117
expect block "sed -i s/a/b/ $MAIN/pkg/x.ts"
@@ -290,6 +318,25 @@ CWD="$MAIN"
290318
expect allow 'sed -i s/a/b/ pkg/x.ts' OS_ALLOW_MAIN_EDITS=1
291319
expect allow 'echo x > README.md' OS_ALLOW_MAIN_EDITS=1
292320

321+
echo "== the hatch names WHERE it works: this hook's own environment, never a prefix =="
322+
# The refusal used to end by telling the reader to re-run the same thing with the variable
323+
# as a VAR=1 command prefix — an instruction that cannot work where it is printed. A VAR=1
324+
# prefix sets the variable in the environment of THAT COMMAND; this hook is not that
325+
# command, and it reads the variable from its own environment, so the prefix changes
326+
# nothing and the refusal repeats (#15971). An instruction that does not work is an
327+
# invitation to route around the guard, so both directions are pinned: the dead remedy is
328+
# gone, and the sentence names the environment the hook actually reads. The `allow` rows
329+
# next door — the variable really in the hook's environment — are this pair's other half.
330+
# The first row is the card's own reproduction, kept as a case: the prefix spelled exactly
331+
# as the old message told the reader to spell it must still BLOCK, because it never reaches
332+
# this hook. It is the twin of the `allow` rows above, where the same variable is really in
333+
# the hook's environment.
334+
CWD="$MAIN"
335+
expect block "OS_ALLOW_MAIN_EDITS=1 rm -f $MAIN/x"
336+
expect block 'OS_ALLOW_MAIN_EDITS=1 sed -i s/a/b/ pkg/x.ts'
337+
lacks 're-run with' 'the refusal no longer prints the prefix remedy' 'sed -i s/a/b/ pkg/x.ts'
338+
says 'hook itself runs in' 'the refusal names the environment this hook reads' 'sed -i s/a/b/ pkg/x.ts'
339+
293340
echo "== unparseable / absent payload fails open =="
294341
for probe in '{"tool_name":"Bash","tool_input":{}}' 'not json at all' '{}'; do
295342
if printf '%s' "$probe" | "$hook" >/dev/null 2>&1; then

.claude/hooks/guard-main-checkout-bash.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,10 @@ Always fine, no flag needed:
571571
writes into a linked worktree sed -i … ../${name}-<task>/packages/…
572572
writes outside any repo /tmp/…, the scratchpad, \$HOME dotfiles
573573
574-
Deliberate non-task exception: re-run with OS_ALLOW_MAIN_EDITS=1.
574+
Deliberate non-task exception: set OS_ALLOW_MAIN_EDITS=1 in the
575+
environment this hook itself runs in — a local settings "env" entry, or whatever this
576+
agent process was started with. A VAR=1 prefix on a command sets it for that command
577+
only, and this hook is not that command, so a prefix never reaches it.
575578
EOF
576579
exit 2
577580
fi

.claude/hooks/guard-main-checkout.selftest.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,31 @@ expect() { # expect <block|allow> <file_path> [env…] — the common case
123123
check "$want" "$f" "$(payload "$f")" "$@"
124124
}
125125

126+
stderr_of() { # stderr_of <payload> [env…] -> the refusal text an agent actually reads
127+
local payload="$1"; shift
128+
( cd "$CWD" && printf '%s' "$payload" | env CLAUDE_PROJECT_DIR="$PROJ" "$@" "$hook" 2>&1 >/dev/null )
129+
}
130+
131+
says() { # says <needle> <label> <payload> [env…]
132+
local needle="$1" label="$2" subject="$3"; shift 3
133+
local out; out="$(stderr_of "$subject" "$@")"
134+
case "$out" in
135+
*"$needle"*) pass=$((pass + 1)); printf ' ok says %s\n' "$label" ;;
136+
*) fail=$((fail + 1)); printf ' FAIL missing "%s" %s\n' "$needle" "$label" ;;
137+
esac
138+
}
139+
140+
lacks() { # lacks <needle> <label> <payload> [env…]
141+
# The direction only an ABSENCE assertion can hold: a remedy that stopped being true stays
142+
# in the text a reader acts on long after the thing it described stopped working.
143+
local needle="$1" label="$2" subject="$3"; shift 3
144+
local out; out="$(stderr_of "$subject" "$@")"
145+
case "$out" in
146+
*"$needle"*) fail=$((fail + 1)); printf ' FAIL still says "%s" %s\n' "$needle" "$label" ;;
147+
*) pass=$((pass + 1)); printf ' ok lacks %s\n' "$label" ;;
148+
esac
149+
}
150+
126151
echo "== the core verdict: shared PRIMARY checkout is blocked =="
127152
expect block "$MAIN/pkg/x.ts"
128153
expect block "$MAIN/pkg/deep/y.ts"
@@ -194,6 +219,27 @@ check block 'OS_ALLOW_MAIN_EDITS=yes' "$(payload "$MAIN/pkg/x.ts")"
194219
check block 'OS_ALLOW_MAIN_EDITS=11' "$(payload "$MAIN/pkg/x.ts")" OS_ALLOW_MAIN_EDITS=11
195220
check block 'OS_ALLOW_MAIN_EDITS=" 1" (padded)' "$(payload "$MAIN/pkg/x.ts")" OS_ALLOW_MAIN_EDITS=" 1"
196221

222+
echo "== the hatch names WHERE it works: this hook's own environment, never a prefix =="
223+
# The refusal used to end by telling the reader to re-run the same thing with the variable
224+
# as a VAR=1 command prefix — an instruction that cannot work where it is printed. A VAR=1
225+
# prefix sets the variable in the environment of THAT COMMAND; this hook is not that
226+
# command, and it reads the variable from its own environment, so the prefix changes
227+
# nothing and the refusal repeats (#15971). An instruction that does not work is an
228+
# invitation to route around the guard, so both directions are pinned: the dead remedy is
229+
# gone, and the sentence names the environment the hook actually reads. The `allow` rows
230+
# next door — the variable really in the hook's environment — are this pair's other half.
231+
# Both of this hook's message sites carry the sentence: the ordinary refusal and the
232+
# schema-drift refusal, which is why each is asserted separately here.
233+
CWD="$PLAIN"; PROJ="$PLAIN"
234+
lacks 're-run with' 'the block message no longer prints the prefix remedy' \
235+
"$(payload "$MAIN/pkg/x.ts")"
236+
says 'hook itself runs in' 'the block message names the environment this hook reads' \
237+
"$(payload "$MAIN/pkg/x.ts")"
238+
lacks 're-run with' 'the drift message no longer prints the prefix remedy' \
239+
'{"tool_name":"Edit","tool_input":{}}'
240+
says 'hook itself runs in' 'the drift message names the environment this hook reads' \
241+
'{"tool_name":"Edit","tool_input":{}}'
242+
197243
echo "== a ROUTED tool whose payload lacks its own path key is drift — it blocks, never guesses =="
198244
# The dangerous branch is the one that turns an unreadable payload into a confident verdict.
199245
# For a tool this guard is routed, an absent path key is not "no path given", it is the

.claude/hooks/guard-main-checkout.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ blocks instead.
8383
Fix the row for $tool in this hook's known_path_keys table, then re-run
8484
.claude/hooks/guard-main-checkout.selftest.sh.
8585
86-
Deliberate non-task exception: re-run with OS_ALLOW_MAIN_EDITS=1.
86+
Deliberate non-task exception: set OS_ALLOW_MAIN_EDITS=1 in the
87+
environment this hook itself runs in — a local settings "env" entry, or whatever this
88+
agent process was started with. A VAR=1 prefix on a command sets it for that command
89+
only, and this hook is not that command, so a prefix never reaches it.
8790
EOF
8891
exit 2
8992
fi
@@ -137,6 +140,9 @@ branch on the shared checkout is NOT enough; you must be in a dedicated worktree
137140
138141
This guard checks the edited file's OWN repo, so sibling repos are covered too.
139142
140-
Deliberate non-task exception: re-run with OS_ALLOW_MAIN_EDITS=1.
143+
Deliberate non-task exception: set OS_ALLOW_MAIN_EDITS=1 in the
144+
environment this hook itself runs in — a local settings "env" entry, or whatever this
145+
agent process was started with. A VAR=1 prefix on a command sets it for that command
146+
only, and this hook is not that command, so a prefix never reaches it.
141147
EOF
142148
exit 2

.claude/hooks/guard-shared-stash.selftest.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,33 @@ expect() { # expect <block|allow> <command> [env…]
4444
fi
4545
}
4646

47+
stderr_of() { # stderr_of <command> [env…] -> the refusal text an agent actually reads
48+
local cmd="$1"; shift
49+
local payload
50+
payload="$(jq -nc --arg c "$cmd" '{tool_name:"Bash",tool_input:{command:$c}}')"
51+
printf '%s' "$payload" | env "$@" "$hook" 2>&1 >/dev/null
52+
}
53+
54+
says() { # says <needle> <label> <command> [env…]
55+
local needle="$1" label="$2" subject="$3"; shift 3
56+
local out; out="$(stderr_of "$subject" "$@")"
57+
case "$out" in
58+
*"$needle"*) pass=$((pass + 1)); printf ' ok says %s\n' "$label" ;;
59+
*) fail=$((fail + 1)); printf ' FAIL missing "%s" %s\n' "$needle" "$label" ;;
60+
esac
61+
}
62+
63+
lacks() { # lacks <needle> <label> <command> [env…]
64+
# The direction only an ABSENCE assertion can hold: a remedy that stopped being true stays
65+
# in the text a reader acts on long after the thing it described stopped working.
66+
local needle="$1" label="$2" subject="$3"; shift 3
67+
local out; out="$(stderr_of "$subject" "$@")"
68+
case "$out" in
69+
*"$needle"*) fail=$((fail + 1)); printf ' FAIL still says "%s" %s\n' "$needle" "$label" ;;
70+
*) pass=$((pass + 1)); printf ' ok lacks %s\n' "$label" ;;
71+
esac
72+
}
73+
4774
echo "== mutating forms must be blocked =="
4875
expect block 'git stash'
4976
expect block 'git stash push -- packages/spec/src/kernel/metadata-plugin.zod.ts'
@@ -134,6 +161,18 @@ expect block "echo 'a \\' ; git stash pop"
134161
echo "== escape hatch =="
135162
expect allow 'git stash pop' OS_ALLOW_STASH=1
136163

164+
echo "== the hatch names WHERE it works: this hook's own environment, never a prefix =="
165+
# The refusal used to end by telling the reader to re-run the same thing with the variable
166+
# as a VAR=1 command prefix — an instruction that cannot work where it is printed. A VAR=1
167+
# prefix sets the variable in the environment of THAT COMMAND; this hook is not that
168+
# command, and it reads the variable from its own environment, so the prefix changes
169+
# nothing and the refusal repeats (#15971). An instruction that does not work is an
170+
# invitation to route around the guard, so both directions are pinned: the dead remedy is
171+
# gone, and the sentence names the environment the hook actually reads. The `allow` rows
172+
# next door — the variable really in the hook's environment — are this pair's other half.
173+
lacks 're-run with' 'the refusal no longer prints the prefix remedy' 'git stash pop'
174+
says 'hook itself runs in' 'the refusal names the environment this hook reads' 'git stash pop'
175+
137176
echo "== payload with no command fails open =="
138177
if printf '%s' '{"tool_name":"Bash","tool_input":{}}' | "$hook" >/dev/null 2>&1; then
139178
pass=$((pass + 1)); printf ' ok allow (empty tool_input)\n'

.claude/hooks/guard-shared-stash.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,10 @@ Already allowed, no flag needed:
221221
git stash list | git stash show | git stash create
222222
git stash apply <sha> | git stash store <sha> # literal hex id, never stash@{N}
223223
224-
Deliberate exception (the stack really is yours alone): re-run with OS_ALLOW_STASH=1.
224+
Deliberate exception (the stack really is yours alone): set OS_ALLOW_STASH=1 in the
225+
environment this hook itself runs in — a local settings "env" entry, or whatever this
226+
agent process was started with. A VAR=1 prefix on a command sets it for that command
227+
only, and this hook is not that command, so a prefix never reaches it.
225228
EOF
226229
exit 2
227230
done

.claude/hooks/guard-tree-enum.selftest.sh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,33 @@ expect() { # expect <block|allow> <command> [env…]
4747
fi
4848
}
4949

50+
stderr_of() { # stderr_of <command> [env…] -> the refusal text an agent actually reads
51+
local cmd="$1"; shift
52+
local payload
53+
payload="$(jq -nc --arg c "$cmd" '{tool_name:"Bash",tool_input:{command:$c}}')"
54+
printf '%s' "$payload" | env "$@" "$hook" 2>&1 >/dev/null
55+
}
56+
57+
says() { # says <needle> <label> <command> [env…]
58+
local needle="$1" label="$2" subject="$3"; shift 3
59+
local out; out="$(stderr_of "$subject" "$@")"
60+
case "$out" in
61+
*"$needle"*) pass=$((pass + 1)); printf ' ok says %s\n' "$label" ;;
62+
*) fail=$((fail + 1)); printf ' FAIL missing "%s" %s\n' "$needle" "$label" ;;
63+
esac
64+
}
65+
66+
lacks() { # lacks <needle> <label> <command> [env…]
67+
# The direction only an ABSENCE assertion can hold: a remedy that stopped being true stays
68+
# in the text a reader acts on long after the thing it described stopped working.
69+
local needle="$1" label="$2" subject="$3"; shift 3
70+
local out; out="$(stderr_of "$subject" "$@")"
71+
case "$out" in
72+
*"$needle"*) fail=$((fail + 1)); printf ' FAIL still says "%s" %s\n' "$needle" "$label" ;;
73+
*) pass=$((pass + 1)); printf ' ok lacks %s\n' "$label" ;;
74+
esac
75+
}
76+
5077
echo "== THE MEASURED SIGNATURE: working-tree list + origin/main read in one command =="
5178
# objectui, 2026-08-29 — the loop that reported "no workflow subscribes ready_for_review"
5279
expect block 'for f in .github/workflows/*.yml; do git show "origin/main:$f" | grep -q ready_for_review && echo "$f"; done'
@@ -101,6 +128,20 @@ expect allow 'git worktree add ../objectstack-issue-13305 -b claude/issue-13305
101128
echo "== the deliberate exception releases it =="
102129
expect allow 'for f in .github/workflows/*.yml; do git show "origin/main:$f"; done' OS_ALLOW_TREE_ENUM=1
103130

131+
echo "== the hatch names WHERE it works: this hook's own environment, never a prefix =="
132+
# The refusal used to end by telling the reader to re-run the same thing with the variable
133+
# as a VAR=1 command prefix — an instruction that cannot work where it is printed. A VAR=1
134+
# prefix sets the variable in the environment of THAT COMMAND; this hook is not that
135+
# command, and it reads the variable from its own environment, so the prefix changes
136+
# nothing and the refusal repeats (#15971). An instruction that does not work is an
137+
# invitation to route around the guard, so both directions are pinned: the dead remedy is
138+
# gone, and the sentence names the environment the hook actually reads. The `allow` rows
139+
# next door — the variable really in the hook's environment — are this pair's other half.
140+
lacks 're-run with' 'the refusal no longer prints the prefix remedy' \
141+
'for f in .github/workflows/*.yml; do git show "origin/main:$f"; done'
142+
says 'hook itself runs in' 'the refusal names the environment this hook reads' \
143+
'for f in .github/workflows/*.yml; do git show "origin/main:$f"; done'
144+
104145
echo "== fails OPEN on payloads it cannot parse =="
105146
printf '%s' '{"tool_name":"Bash","tool_input":{}}' | "$hook" >/dev/null 2>&1
106147
if [ $? -eq 0 ]; then pass=$((pass + 1)); printf ' ok allow <no command in payload>\n'

.claude/hooks/guard-tree-enum.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,10 @@ A command that enumerates with git ls-tree is never blocked here, however it the
312312
Either half alone is fine too — this fires only on the two together.
313313
314314
Deliberate exception (the working tree really is the population you mean — e.g. asking
315-
what YOUR branch changed): re-run with OS_ALLOW_TREE_ENUM=1.
315+
what YOUR branch changed): set OS_ALLOW_TREE_ENUM=1 in the
316+
environment this hook itself runs in — a local settings "env" entry, or whatever this
317+
agent process was started with. A VAR=1 prefix on a command sets it for that command
318+
only, and this hook is not that command, so a prefix never reaches it.
316319
EOF
317320
exit 2
318321
fi

0 commit comments

Comments
 (0)