hooks: the three remaining guards name the environment their hatch variable must be set in, never a command prefix - #9300
Draft
claude[bot] wants to merge 2 commits into
Draft
Conversation
…riable must be set in, never a command prefix
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 <cmd>` 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK
check-upstream-port-parity red-flagged the previous commit: these guards are
PINNED ports of objectstack's files, so the deliberate act for wording that
moved upstream is a re-sync, not a hand edit. Ran the gate's own procedure, once
per ported path:
node scripts/check-upstream-port-parity.mjs --resync UPSTREAM_FILE \
--path .claude/hooks/FILE --ref 7c2c5aedd94d7b0d94c91432bc607862e9c83c6c \
--rewrite-governed-file
for guard-main-checkout-bash.sh, guard-shared-stash.sh, guard-tree-enum.sh and
their three self-tests, with UPSTREAM_FILE taken from
`git show origin/main:.claude/hooks/FILE` in the objectstack checkout. The
governed-surface flag is required because .claude/** is governed here; the gate
names it and printed the rewrite for each of the six. No pinned digest was
edited by hand.
What the re-sync changed: nothing in any hook. All six files came back
byte-identical to the previous commit's hand-ported text, because objectstack's
movement since each pinned ref is exactly this card's change and nothing else
(+4/-1 per hook, +47/+39/+41 per self-test). The only diff this commit carries
is scripts/upstream-port-pin.json: six `ref` values to 7c2c5aed and six
`upstreamSha256` values, twelve lines. The five other pinned entries keep their
own refs, as the gate guarantees. Every declared divergence was re-applied and
survives — guard-shared-stash.sh still carries its own self-test case count with
the re-derivation recipe (46 `expect ` lines + 2 inline specials + the 2 new
message assertions = 50, equal to the run's tail), and the objectui card
references and worktree example names are intact.
Verified after the re-sync: check-upstream-port-parity exits 0 with ✓ for all 11
entries at 5 distinct refs; its --self-test 58 cases pass; the three hatch
sentences read the upstream form; `git grep -n 're-run with' -- .claude/hooks`
hits 5 lines, all self-test assertions; hook self-tests 130 / 120 / 50 / 38
passed, 0 failed. Also green: check-lint-coverage, check-entry-guard,
check-control-bytes, check-bash32-floor, check-changeset-presence (no changeset
owed), check-governed-queue-guard --self-test.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK
Contributor
Author
维护者速读事情:objectui 的四个 guard hook 拒绝时都写着「re-run with OS_ALLOW_*=1」——照做没用,因为命令前缀到不了 hook 自己的环境; 改了什么:三个 hook 各一句、三个自测各加两条断言;另外 要你看一眼的:重同步需要 风险:低。纯 shell,四个 hook 自测全绿,parity 门禁 11/11 绿;CI 其余项在跑。 只问一字:是否合并? Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7775
guard-main-checkout-bash.sh,guard-shared-stash.shandguard-tree-enum.sheach ended theirrefusal with
re-run with OS_ALLOW_*=1. AVAR=1prefix on a command sets the variable for THATCOMMAND; the hook reads
${OS_ALLOW_*:-}from its OWN environment, where nothing set it, so itblocks again while echoing the prefix it just saw. The fourth guard,
guard-main-checkout.sh, wasalready repaired (objectui#7259 / PR #7749), which left two sibling guards for the same escape
hatch giving an operator opposite instructions — and the one still advertising the dead remedy was
the Bash guard, the one reached when an agent writes through a shell rather than through Edit/Write.
The wording is objectstack's, at
origin/main7c2c5aed(objectstack#15971), so the executablelines of the guards stay identical across the two repositories, which is what the card requires.
No third wording was invented.
Route: these six files are PINNED ports, so the deliberate act is a re-sync
First pass hand-edited the three sentences.
check-upstream-port-parityred-flagged it, correctly:all six files are pinned ports of objectstack's copies with declared divergences, and the gate's own
remedy for wording that moved upstream is a re-sync, not an edit. This PR now carries two commits —
the port, then the pin bump — and the gate's procedure was run once per ported path:
with
UPSTREAM_FILEfromgit show origin/main:.claude/hooks/FILEin the objectstack checkout.--rewrite-governed-fileis required because.claude/**is governed surface here; the gate namesthe flag and printed the in-place rewrite for each of the six. No pinned digest was edited by hand.
What the re-sync changed beyond the three sentences: nothing in any hook. All six files came
back byte-identical to the hand-ported commit (
diffper file printed nothing, six for six), sothe only diff the second commit carries is
scripts/upstream-port-pin.json— sixrefvalues to7c2c5aedand sixupstreamSha256values, twelve lines, with the five other pinned entrieskeeping their own refs. That is the pin registry, the gate's own output, and it is the one file in
this PR outside
.claude/hooks/**.The reason the rewrite was a content no-op was measured beforehand, not hoped for: objectstack's
movement since each pinned ref is exactly this card's change and nothing else —
+4/-1on eachhook and
+47 / +39 / +41on the three self-tests, line-for-line what the first commit adds. Everydeclared divergence was re-applied and survives:
guard-shared-stash.shstill carries its ownself-test case count with its re-derivation recipe, and the objectui card references
(objectui#3430 / #3435 / #6042) and worktree example names are intact.
The three hatch sentences, before and after
.claude/hooks/guard-main-checkout-bash.sh(was :562).claude/hooks/guard-shared-stash.sh(was :206).claude/hooks/guard-tree-enum.sh(was :321; the sentence's first line is unchanged)The assertions that keep it from being re-lost
Each of the three self-tests gains the pair the already-repaired sibling uses at
guard-main-checkout.selftest.sh:244 / :248, plus thestderr_of/says/lackshelpersthose assertions need — the three matrices had no message-text helper at all, which is the gap the
card's triage comment named: they asserted verdicts only, never message text, so nothing mechanical
would have noticed the divergence or a later regression.
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 — the twin of the
allowrows next door where the same variable really is in the hook's environment.Ablation — the new assertions can actually fail
Run from the committed state, all three hooks mutated back to the old one-line sentence at once,
with a
traprestore on absolute paths:re-run withcount 0 to 1,hook itself runs incount 1 to 0, andgit hash-objectdiffering from theHEADblob (all three, non-empty hashes both sides).128 passed, 2 failed/48 passed, 2 failed/guard-tree-enum selftest: 36 passed, 2 failed, each failing exactlyFAIL still says "re-run with"andFAIL missing "hook itself runs in".git checkout HEAD -- PATHper file, proven bygit diff HEADbeing 0 bytes andeach file's
git hash-objectequal to itsHEADblob; the three matrices return to130 / 50 / 38 passed, 0 failed.No ablation artefact is left in the tree.
Verification, on the head after the re-sync
Self-tests, verdict lines as the runs print them (exit codes captured before any pipe):
git grep -n 're-run with' -- .claude/hooks: 5 hits before, 5 after — but a different five. Before:three live hatch sentences plus the repaired hook's two assertions. After: five self-test assertion
lines and no hook body line at all.
That is every pre-install step of the Lint job plus the gates that read
.claude/hooks/**. Thepost-install half (
pnpm lint,pnpm check,check-vi-mock-override-shape,check-test-path-roots) is NOT MEASURED locally: this worktree has nonode_modulesand the card'ssurface is plain shell.
eslintin particular is not narrowed here, it is empty — its own config'spopulation is
files: ['**/*.{ts,tsx}']and narrower blocks, and this diff contains zero.ts/.tsxfiles.scripts/__tests__/upstream-port-parity-wiring.test.tshas a case "and the treeitself is at parity right now" that runs the gate through
execFileSync; it failed on the previoushead and the gate's
exit 0above is what clears it, but the vitest run itself is left to CI forthe same reason.
Gate derivation was by hand: objectui has no
scripts/pm/dispatch-gates.mjs(onlyscripts/pm/check-half-states.mjs), so the list comes from this repo's ownpackage.jsoncheck:*aliases and
.github/workflows/, filtered to the gates that read.claude/hooks/**orscripts/**.Acceptance notes
guard-shared-stash.sh's header states its self-test's case count with a recipe to re-derive it,and
hook-selftests.yml's header names that header as the one place such a count belongs. Thetwo new assertions move it, so it is re-derived by its own recipe: 46
expectlines + 2 inlinespecials + 2 message assertions = 50, equal to the run's own tail. The pin entry
selftest-case-count-and-derivationdeclares that count as a divergence from upstream's 51, soits
portedtext moved with the header in the first commit — otherwise the re-sync in the secondwould have written the stale number straight back.
guard-main-checkout-bash.shis NOT in thepin registry. Measured false on
243fc8317— it is pinned, as are all five of its siblings here,which is exactly why the first pass went red. The comment's companion claim, that
guard-main-checkout-bash.selftest.shasserts verdicts only and never message text, was true andis what this PR repairs.
#15971, which reads in this repo as anonexistent objectui card rather than the objectstack one. It is upstream's byte, re-applied by
the re-sync, and the already-landed sibling block at
guard-main-checkout.selftest.sh:244spells it the same way. Changing it here would become a seventh declared divergence.
维护者速读(草稿)
改了什么 — 三个 PreToolUse 守卫(Bash 写入守卫、stash 守卫、tree-enum 守卫)的拒绝文案末句。
原句教人
re-run with OS_ALLOW_*=1,即在命令前加变量前缀;这种前缀只对那条命令生效,而守卫读的是它自己进程的环境,所以照做仍然被拦。新句直接点名唯一有效的位置:守卫自身运行的环境(本地 settings
的
env项,或启动这个 agent 进程时带的环境)。三个自测各加一对断言,钉住「死办法已消失」和「新句已点名环境」两个方向。文案不是手写的:这六个文件是 objectstack 同名文件的 pin 化移植,所以走的是
门禁自己的
--resync,ref 重钉到 objectstack7c2c5aed;pin 台账scripts/upstream-port-pin.json的 12 行(六个 ref + 六个摘要)是本 PR 唯一落在
.claude/hooks/**之外的改动。为什么改 — 一条做不到的指示,比没有指示更糟:它把人推向绕过守卫。第四个守卫早先已修,于是同一
个逃生口上两个兄弟守卫给出相反的指示,而还在教死办法的那个恰好是走 shell 写入时命中的那个。这三个
自测此前只断言放行/拦截,从不断言文案,所以既没有东西会发现这次分歧,也没有东西会发现修好后又退回去。
风险与代价(含回滚) — 守卫的判定逻辑一行未动,改的只是 heredoc 里的说明文字和自测断言;四个自测
全绿(130 / 120 / 50 / 38,0 失败),并已用消融证明新断言真的会红。pin 重钉经实测是内容中性的:六个
文件重写后与重钉前逐字节相同,因为上游自各自 pin 以来的移动恰好只有这一次改词。回滚是 revert 这两个
commit,不涉及数据、不涉及发布物;没有 changeset(门禁判定「无需」)。
席位意见 — (留空,由席位定稿成评论)
你要做的 — 1) 读一遍三句新文案是否就是你要的规程措辞;2) 确认 pin 重钉到
7c2c5aed是你要的provenance(带
--rewrite-governed-file的写入按名保留给合并受管面的人,本轮由席位在 PM 明确改路后执行,这一点写在这里供你复核);3) 本 PR 是受管面(
.claude/**),停在 draft 等你合并,席位不翻ready、不入队、不挂 auto-merge。
Session for this implementation:
https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVKGenerated by Claude Code