From 564423d9f97ff88ffb0f3d6b7f476eeb65f9b766 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 15:32:11 +0000 Subject: [PATCH 1/2] hooks: the three remaining guards name the environment their hatch variable must be set in, never a command prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit guard-main-checkout-bash.sh, guard-shared-stash.sh and guard-tree-enum.sh each ended their refusal with `re-run with OS_ALLOW_*=1`. Followed literally, a `VAR=1 ` prefix sets the variable for that command only; the hook reads `${OS_ALLOW_*:-}` from its OWN environment, where nothing set it, so it blocks again while echoing the prefix it saw. An instruction that cannot work where it is printed is an invitation to route around the guard, and after the guard-main-checkout.sh half landed the two sibling guards for the same hatch were giving an operator opposite instructions — the Bash guard, reached when an agent writes through a shell, being the one still advertising the dead remedy. Each hatch sentence is replaced with the objectstack twin's wording, ported byte-for-byte so the executable lines of the guards stay identical across the two repositories. Reversing the pin's declared divergences out of all six ported files reproduces objectstack@7c2c5aed's blobs exactly. Each self-test gains the pair that keeps this from being re-lost: `lacks 're-run with'` (the dead remedy is gone) and `says 'hook itself runs in'` (the sentence names the environment the hook really reads), in the idiom guard-main-checkout.selftest.sh already uses, together with the stderr_of / says / lacks helpers those two assertions need. The bash matrix also takes upstream's two prefix-reproduction rows, which assert that the prefix spelled exactly as the old message told the reader to spell it still blocks. guard-shared-stash.sh's self-test case count is re-derived by the recipe the header itself gives (46 `expect ` lines + 2 inline specials + the 2 new message assertions = 50, equal to the run's own tail), and the pin entry that declares that count as a divergence from upstream moves with it. Self-tests, in this worktree: guard-main-checkout-bash 130 passed 0 failed, guard-main-checkout 120/0 (untouched), guard-shared-stash 50/0, guard-tree-enum 38/0. `git grep -n 're-run with' -- .claude/hooks` now hits 5 lines, every one of them a self-test assertion. check:upstream-port-parity is RED on six stale pin entries and needs a re-sync at merge: the pinned refs predate this wording upstream, and the re-sync write path is reserved by name for the human merging governed surface. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK --- .../guard-main-checkout-bash.selftest.sh | 47 +++++++++++++++++++ .claude/hooks/guard-main-checkout-bash.sh | 5 +- .claude/hooks/guard-shared-stash.selftest.sh | 39 +++++++++++++++ .claude/hooks/guard-shared-stash.sh | 12 +++-- .claude/hooks/guard-tree-enum.selftest.sh | 41 ++++++++++++++++ .claude/hooks/guard-tree-enum.sh | 5 +- scripts/upstream-port-pin.json | 2 +- 7 files changed, 144 insertions(+), 7 deletions(-) diff --git a/.claude/hooks/guard-main-checkout-bash.selftest.sh b/.claude/hooks/guard-main-checkout-bash.selftest.sh index 8ebe430e86..c9d11c6d42 100755 --- a/.claude/hooks/guard-main-checkout-bash.selftest.sh +++ b/.claude/hooks/guard-main-checkout-bash.selftest.sh @@ -83,6 +83,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