Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .claude/hooks/guard-shared-stash.selftest.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,28 @@ expect allow 'grep -rn "cd x && git stash pop" .claude/'
expect allow 'echo "never run git stash pop in a shared checkout"'
expect allow 'git grep -n "git stash"'

echo "== an UNQUOTED \\\" opens no quote, so the stash behind it is still seen (#11738) =="
# The measured hole: segmentation read the escaped `"` as OPENING a quoted region that never
# closed. Every separator behind it went inert, the whole command collapsed into a single
# `echo` segment, and the real `git stash pop` was just another argument. Same repair
# guard-main-checkout-bash.sh's split_segments() took in #11131. Every case below was
# ALLOWED before that branch existed; the bare forms next door are the controls that say
# the guard was reached at all.
expect block 'echo \" ; git stash'
expect block 'echo \" ; git stash pop'
expect block 'echo \" && git stash pop'
expect block 'printf \" ; git stash drop'
expect block 'echo \" ; git -C ../objectstack-issue-11738 stash pop'
expect block "$(printf 'echo \\"\ngit stash pop\n')"
# Precision twins: the escape must not manufacture a block where nothing touches the stack,
# and the read-only and SHA-pinned forms stay allowed behind one.
expect allow 'echo \" ; echo hello'
expect allow 'echo \" ; git status'
expect allow 'echo \" ; git stash list'
expect allow 'echo \" ; git stash apply abc1234'
expect allow 'echo a\ b'
expect allow 'echo \\ ; git status'

echo "== escape hatch =="
expect allow 'git stash pop' OS_ALLOW_STASH=1

Expand Down
18 changes: 17 additions & 1 deletion .claude/hooks/guard-shared-stash.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,7 @@
# for anyone who means it. Widening it to string-match anywhere in the command would block
# every `grep "git stash"` run against this very file.
#
# Self-test (32 cases, no network, no build): .claude/hooks/guard-shared-stash.selftest.sh
# Self-test (44 cases, no network, no build): .claude/hooks/guard-shared-stash.selftest.sh

set -uo pipefail

Expand All@@ -89,6 +89,18 @@ fi
# A separator inside '…' or "…" does NOT split, so writing *about* the ban is never caught
# by the ban: `grep -n "cd x && git stash pop" AGENTS.md` stays one segment whose first
# word is grep. (objectstack#4890's lesson — the PR writing a rule must not trip it.)
#
# OUTSIDE quotes a backslash escapes the NEXT character, so an escaped `\"` opens no quoted
# region at all. Without that branch this pass read the `"` as opening a region that never
# closed, went inert for every separator behind it, collapsed the whole command into one
# segment whose head word was the harmless one, and let a real `git stash pop` ride through
# as a mere argument of `echo` (#11738). Measured on this repo's copy before the fix:
# `echo \" ; git stash pop` was ALLOWED while a bare `git stash pop` was blocked.
#
# This is the same repair guard-main-checkout-bash.sh's split_segments() took in #11131,
# in the same shape; that guard's `word` bookkeeping has no analogue here because this pass
# has no comment rule to track word starts for. Both characters are kept verbatim — this
# pass only SPLITS, and check_segment() re-reads the words afterwards.
segments=()
split_segments() {
local s="$1" seg="" q="" ch i n=${#1}
Expand All@@ -100,6 +112,10 @@ split_segments() {
continue
fi
case "$ch" in
'\')
seg+="$ch"
if [ $((i + 1)) -lt "$n" ]; then i=$((i + 1)) ; seg+="${s:i:1}" ; fi
;;
"'" | '"') q="$ch" ; seg+="$ch" ;;
';' | '|' | '&' | '(' | ')' | '{' | '}' | $'\n') segments+=("$seg") ; seg="" ;;
*) seg+="$ch" ;;
Expand Down
Loading