Skip to content

Commit d83fca2

Browse files
committed
test(hooks): pin the nested-quote rule per hook and the shared copies byte for byte
Each Bash guard's matrix gains the measured false blocks for $(...), backticks and ${...} inside "...", the recall controls next to them, and the fail-open mirrors the rule closes. The three copying guards also assert that their subst_end() equals guard-main-checkout-bash.sh's and, for guard-process-kill.sh and guard-tree-enum.sh, that their split_segments() equals guard-shared-stash.sh's. Claude-Session: https://claude.ai/code/session_01M82feuetLzrCiBz41BJb7L Co-authored-by: Claude <noreply@anthropic.com>
1 parent 851a529 commit d83fca2

4 files changed

Lines changed: 140 additions & 0 deletions

File tree

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,13 +326,44 @@ expect block "echo \"\$(echo \"\$(printf '\"')\")\" ; sed -i s/a/b/ pkg/x.ts"
326326
# introducer behind it is found and the body stays prose.
327327
expect allow "$(printf 'echo "$(echo " #")" <<EOF\nsed -i s/a/b/ pkg/x.ts\nEOF\n')"
328328

329+
echo "== a \${…} inside \"…\" is its own quoting context too (#24901) =="
330+
CWD="$MAIN"
331+
# The measured false block: the `"` that opens the default was read as CLOSING the outer string,
332+
# so `b}` became a write target. Pure reads — nothing is written. Each one exercises a different
333+
# pass: tokenisation (the first two), segmentation (the `;`), nesting both ways (the third).
334+
expect allow 'echo "${x:-"a > b"}"'
335+
expect allow 'echo "${x#"a > b"}"'
336+
expect allow 'echo "${x:-"$(printf "a > b")"}"'
337+
expect allow 'echo "${x:-"a; sed -i s/a/b/ pkg/x.ts"}"'
338+
expect allow "echo \"\${x:-'a > b'}\"" # control: no nested `"`, allowed before and after
339+
# The heredoc scan agrees: the ` #` inside the expansion is no comment, so the introducer
340+
# behind it is found and the body stays prose.
341+
expect allow "$(printf 'echo "${x:-" #"}" <<EOF\nsed -i s/a/b/ pkg/x.ts\nEOF\n')"
342+
# Recall is untouched: a write behind, beside or inside the script of a quoted expansion. A
343+
# '…' run holding no character the shells disagree on is skipped, so the write behind it stays.
344+
expect block 'sed -i "s/a/${x:-"b"}/" pkg/x.ts'
345+
expect block 'sed -i "s/a/${x}/" pkg/x.ts'
346+
expect block 'echo "${HOME}" > pkg/x.ts'
347+
expect block 'echo "${x:-"a"}" ; sed -i s/a/b/ pkg/x.ts'
348+
expect block "echo \"\${x:-'a'}\" ; sed -i s/a/b/ pkg/x.ts"
349+
# A `$(…)` inside the expansion is read by the same function, quotes and all: the `"` inside
350+
# its single quotes closes nothing, so the separator behind the word still splits.
351+
expect block "echo \"\${x:-\$(printf '\"')}\" ; sed -i s/a/b/ pkg/x.ts"
352+
329353
echo "== shapes this guard deliberately does NOT claim (documented fail-open) =="
330354
CWD="$MAIN"
331355
expect allow "bash -c \"sed -i s/a/b/ $MAIN/pkg/x.ts\""
332356
expect allow 'echo pkg/x.ts | xargs sed -i s/a/b/'
333357
expect allow 'node -e "fs.writeFileSync(\"pkg/x.ts\", \"y\")"'
334358
expect allow 'python3 -c "open(\"pkg/x.ts\",\"w\").write(\"y\")"'
335359
expect allow 'echo "$(sed -i s/a/b/ pkg/x.ts)"' # a QUOTED $(…) body is one inert word
360+
expect allow 'echo "${x:-"$(sed -i s/a/b/ pkg/x.ts)"}"' # …and so is a QUOTED ${…} body
361+
expect allow 'echo hi > "${x:-"a > b"}"' # a target holding an expansion
362+
# A '…' run inside a quoted ${…} that holds `}` (or `"` `\` `$` and a backtick): bash 5.2 reads
363+
# it as quoted and POSIX shells as literal, so they end the expansion in different places and
364+
# everything after it is inert. Under bash the first is a pure read, under dash a real write.
365+
expect allow "echo \"\${x:-'}\"; sed -i s/a/b/ pkg/x.ts'}\""
366+
expect allow "echo \"\${x:-'}'}\" ; sed -i s/a/b/ pkg/x.ts"
336367
expect allow 'sed -i s/a/b/ $SOMEDIR/x.ts'
337368
expect allow 'sed -i s/a/b/ pkg/*.ts'
338369

‎.claude/hooks/guard-process-kill.selftest.sh‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,41 @@ echo "== an escaped \\\" INSIDE a double-quoted word does NOT close it (#10406 h
176176
expect allow 'grep -rn "he said \"pkill -f vitest\" once" .claude/'
177177
expect block 'echo "he said \"x\"" && pkill -f vitest'
178178

179+
echo "== a \$(…), backtick or \${…} inside \"…\" is its own quoting context (#24901) =="
180+
# Reading the `"` inside the body as CLOSING the outer string split the body at its `;`, so a
181+
# pure read became a name-matched kill. All four were BLOCKED before subst_end() existed here.
182+
expect allow 'echo "a $(echo "x; pkill -f node")"'
183+
expect allow 'echo "a `echo "x; pkill -f node"`"'
184+
expect allow 'echo "${x:-"a; pkill -f node"}"'
185+
expect allow 'echo "${x:-"; killall node"}"'
186+
# Recall: a kill behind a quoted expansion is still seen, and a pattern held in one is still a
187+
# pattern — the expansion is part of the word, not a way around the rule.
188+
expect block 'echo "${x:-"a"}" ; pkill -f node'
189+
expect block 'kill $(pgrep -f "${pat:-"node"}")'
190+
expect block 'pkill -f "${pat:-"vitest; x"}"'
191+
# The fail-OPEN mirror: the `"` inside the single quotes left the outer string hanging, so the
192+
# separator behind it went inert. ALLOWED before subst_end() existed here.
193+
expect block "echo \"\$(printf '\"')\" ; pkill -f vitest"
194+
# Deliberately not claimed, as before: a kill INSIDE a quoted substitution is one inert word.
195+
expect allow 'echo "$(pkill -f node)"'
196+
197+
echo "== the shared quoting rules are copies, byte for byte (#24901) =="
198+
# split_segments() is guard-shared-stash.sh's and subst_end() is guard-main-checkout-bash.sh's.
199+
# A copy that drifts is a quoting rule the guards no longer share, and nothing else notices:
200+
# each hook's own cases keep passing on its own dialect.
201+
fn_text() { sed -n "/^$1() {\$/,/^}\$/p" "$2"; }
202+
same_fn() { # same_fn <function> <file holding the copy this hook must equal>
203+
local mine theirs
204+
mine="$(fn_text "$1" "$hook")"; theirs="$(fn_text "$1" "$2")"
205+
if [ -n "$mine" ] && [ "${mine%%$'\n'*}" = "$1() {" ] && [ "$mine" = "$theirs" ]; then
206+
pass=$((pass + 1)); printf ' ok same %s() as %s\n' "$1" "${2##*/}"
207+
else
208+
fail=$((fail + 1)); printf ' FAIL %s() is missing here or differs from %s\n' "$1" "${2##*/}"
209+
fi
210+
}
211+
same_fn split_segments "$here/guard-shared-stash.sh"
212+
same_fn subst_end "$here/guard-main-checkout-bash.sh"
213+
179214
echo "== escape hatch =="
180215
expect allow 'pkill -f vitest' OS_ALLOW_PROCESS_KILL=1
181216
expect allow 'ps aux | grep node | xargs kill' OS_ALLOW_PROCESS_KILL=1

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,43 @@ expect block 'echo "a \\" ; git stash pop'
158158
# quotes it would swallow the closing `'` and let the stash behind it through.
159159
expect block "echo 'a \\' ; git stash pop"
160160

161+
echo "== a \$(…), backtick or \${…} inside \"…\" is its own quoting context (#24901) =="
162+
# Reading the `"` inside the body as CLOSING the outer string split the body at its `;`, so a
163+
# pure read became a stash. The first three were BLOCKED before subst_end() existed here; the
164+
# fourth (both kinds nested) was allowed then only because two misread quotes cancelled out,
165+
# and the single-level `echo "x; git stash pop"` was allowed throughout.
166+
expect allow 'echo "a $(echo "x; git stash pop")"'
167+
expect allow 'echo "a `echo "x; git stash pop"`"'
168+
expect allow 'echo "${x:-"a; git stash pop"}"'
169+
expect allow 'echo "${x:-"$(echo "a; git stash pop")"}"'
170+
expect allow 'echo "x; git stash pop"'
171+
# Recall: a stash behind or beside a quoted expansion is still seen.
172+
expect block 'echo "${x:-"a"}" ; git stash pop'
173+
expect block 'X="${Y:-"z"}" && git stash push -m wip'
174+
expect block 'git stash push -m "${msg:-"wip; x"}"'
175+
# The fail-OPEN mirror: the `"` inside the single quotes left the outer string hanging, so the
176+
# separator behind it went inert and the real stash rode through as text. Both were ALLOWED
177+
# before subst_end() existed here.
178+
expect block "echo \"\$(printf '\"')\" ; git stash pop"
179+
expect block "echo \"\${x:-\$(printf '\"')}\" ; git stash pop"
180+
# Deliberately not claimed, as before: a stash INSIDE a quoted substitution is one inert word.
181+
expect allow 'echo "$(git stash pop)"'
182+
183+
echo "== the shared quoting rule is guard-main-checkout-bash.sh's, byte for byte (#24901) =="
184+
# subst_end() lives in four hooks. A copy that drifts is a quoting rule the guards no longer
185+
# share, and nothing else notices: each hook's own cases keep passing on its own dialect.
186+
fn_text() { sed -n "/^$1() {\$/,/^}\$/p" "$2"; }
187+
same_fn() { # same_fn <function> <file holding the copy this hook must equal>
188+
local mine theirs
189+
mine="$(fn_text "$1" "$hook")"; theirs="$(fn_text "$1" "$2")"
190+
if [ -n "$mine" ] && [ "${mine%%$'\n'*}" = "$1() {" ] && [ "$mine" = "$theirs" ]; then
191+
pass=$((pass + 1)); printf ' ok same %s() as %s\n' "$1" "${2##*/}"
192+
else
193+
fail=$((fail + 1)); printf ' FAIL %s() is missing here or differs from %s\n' "$1" "${2##*/}"
194+
fi
195+
}
196+
same_fn subst_end "$here/guard-main-checkout-bash.sh"
197+
161198
echo "== escape hatch =="
162199
expect allow 'git stash pop' OS_ALLOW_STASH=1
163200

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,43 @@ expect allow 'echo "never feed a working-tree glob into git show origin/main:"'
119119
expect allow 'git grep -n "ls-tree --name-only origin/main"'
120120
expect allow 'cat .claude/hooks/guard-tree-enum.sh'
121121

122+
echo "== a \"…\" string ends only where bash ends it (#24901) =="
123+
# Two shapes inside "…" whose `"` does NOT close the outer string, and this pass read both as
124+
# closing it, so a pure read split into an enumeration segment and an origin/main read. Every
125+
# allow-case below was BLOCKED before the fix; the single-level forms next door never were.
126+
# (1) an escaped `\"`, the #11804 repair its two siblings already carried:
127+
expect allow 'echo "say \"; ls *.yml; git show origin/main:x\" ok"'
128+
expect allow 'echo "say ; ls *.yml; git show origin/main:x ok"'
129+
# (2) a `"` inside a `$(…)`, a backtick or a `${…}`, which opens a context of its own:
130+
expect allow 'echo "a $(echo "x; ls *.yml")" && git show origin/main:README.md'
131+
expect allow 'echo "a `echo "; find ."`"; git grep foo origin/main'
132+
expect allow 'echo "${x:-"; ls *.yml"}" && git show origin/main:README.md'
133+
expect allow 'echo "x; ls *.yml" && git show origin/main:README.md'
134+
# Recall: the sweep this guard exists for is still caught when a quoted expansion sits in it.
135+
expect block 'for f in "${dir:-"docs"}"/*.md; do git show "origin/main:$f"; done'
136+
expect block 'for f in .github/workflows/*.yml; do git show "origin/main:${f}"; done'
137+
expect block 'echo "${x:-"a"}" ; for f in *.yml; do git show "origin/main:$f"; done'
138+
# The fail-OPEN mirror of (1): the escaped quotes paired up and left the region hanging, so
139+
# the real sweep behind them rode through as text. ALLOWED before the fix.
140+
expect block 'echo "he said \"x\"" ; for f in *.yml; do git show "origin/main:$f"; done'
141+
142+
echo "== the shared quoting rules are copies, byte for byte (#24901) =="
143+
# split_segments() is guard-shared-stash.sh's and subst_end() is guard-main-checkout-bash.sh's.
144+
# This pass's header called it identical while it lacked the in-quote escape rule — exactly
145+
# the drift a prose claim cannot catch and this pin does.
146+
fn_text() { sed -n "/^$1() {\$/,/^}\$/p" "$2"; }
147+
same_fn() { # same_fn <function> <file holding the copy this hook must equal>
148+
local mine theirs
149+
mine="$(fn_text "$1" "$hook")"; theirs="$(fn_text "$1" "$2")"
150+
if [ -n "$mine" ] && [ "${mine%%$'\n'*}" = "$1() {" ] && [ "$mine" = "$theirs" ]; then
151+
pass=$((pass + 1)); printf ' ok same %s() as %s\n' "$1" "${2##*/}"
152+
else
153+
fail=$((fail + 1)); printf ' FAIL %s() is missing here or differs from %s\n' "$1" "${2##*/}"
154+
fi
155+
}
156+
same_fn split_segments "$here/guard-shared-stash.sh"
157+
same_fn subst_end "$here/guard-main-checkout-bash.sh"
158+
122159
echo "== unrelated commands are untouched =="
123160
expect allow 'pnpm --filter @objectstack/spec test'
124161
expect allow 'git status'

0 commit comments

Comments
 (0)