Uh oh!
There was an error while loading. Please reload this page.
fix(hooks): make guard-main-checkout-bash agree with itself on backslashes and comment-named heredocs - #11278
Merged
Merged
Conversation
…eady had (#11131) Outside quotes a backslash escapes the next character, so an escaped `\"` opens no quoted region. tokenize() modelled that; split_segments() did not. The disagreement was fail-OPEN: the `"` behind the backslash opened a quote that never closed, every separator after it went inert, the command collapsed into a single `echo` segment, and a real `sed -i` into the shared checkout rode through as just another argument. Measured probe, cwd inside the shared primary checkout: echo \" ; sed -i s/a/b/ pkg/x.ts before: hook-exit=0 ALLOWED after: hook-exit=2 sed -i s/a/b/ pkg/x.ts (control) before: hook-exit=2 after: hook-exit=2 The mirror case #10247 (an escaped quote INSIDE a double-quoted word, a false BLOCK) stays allowed — its twins are in the matrix next door and are unchanged. Selftest: 100 -> 110 passed, 0 failed. Both directions pinned, including the precision twins where the escape must NOT manufacture a target, and the same command aimed at a linked worktree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
…thing (#11133) strip_heredocs() ran on raw text and modelled neither quotes nor comments, so a `<<EOF` occurring inside prose registered as a real introducer. The delimiter then never appeared on a line of its own, the pending heredoc was never satisfied, and every remaining line — including real commands — was dropped before either quote-aware pass could see it. Fail-OPEN, and total for the rest of the command. Measured probe, cwd inside the shared primary checkout: "# use cat > /tmp/n <<EOF for notes" + sed -i s/a/b/ pkg/x.ts before: hook-exit=0 ALLOWED after: hook-exit=2 "# plain prose comment" + same write (control) before: hook-exit=2 after: hook-exit=2 Shape: the card's candidate 1 (narrow, keeps the existing pass order), not candidate 2 (one merged scanner). Only the DELIMITER SCAN consults the comment rule; the line itself is passed through untouched, so the landed #10570 comment rule in the two quote-aware passes stays the single implementation of it. The card's ordering hazard — an unbalanced apostrophe in a heredoc BODY desynchronising a quote-aware comment scanner — is structurally out of reach here: body lines are consumed by the `pending` branch and never reach the scan. Pinned as its own case. Selftest: 110 -> 121 passed, 0 failed. Both directions pinned, including a QUOTED `#` on an introducing line (truncating there would expose the body as commands) and an inline comment after a real introducer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
PR #10406 correctly moved the count "Two" -> "Three" when it added layer 3. PR #11129 (#10570) added layer 4 and left the count alone. This header is the enumerated contract a reader checks a change against — and it is the thing that told objectui's dev the two copies must not drift — so a miscount is worth the one word. No logic, no self-test change: 121 passed, 0 failed, unchanged. The objectui copy carries the miscount verbatim and deliberately (objectui#5459); it ports downstream from here like any other drift item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RMTpSRF5CjMmQBFfPtPCwJ
This was referenced Aug 23, 2026
os-zhuang
self-requested a review
August 23, 2026 08:36
This was referenced Aug 23, 2026
hotlong
marked this pull request as ready for review
August 23, 2026 08:44
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 24, 2026
This was referenced Aug 24, 2026
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 freeto 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#11131
Fixes#11133
Fixes#11234
Three graded cards on one file pair, one commit each, per the PM fold ruling recorded on #11131. All measurements below were re-taken against current
main(a2ec377), not merely the commit the cards were measured at (95437e7d2) — the only churn on these two files since then is merged PR #11129, exactly as all three cards state.Governed surface — draft, human merge only
.claude/**is a governed surface. This PR is opened as draft deliberately: it is not to be flipped ready by an agent, auto-merge is not armed, and it wants a human merge.What changed, per member
#11131 —
split_segments()had no backslash branch (fail-OPEN)Outside quotes a backslash escapes the next character, so an escaped
\"opens no quoted region.tokenize()modelled that;split_segments()did not. The disagreement let a real write through: the"behind the backslash opened a quote that never closed, every separator after it went inert, the command collapsed into a singleechosegment, and thesed -irode along as just another argument.echo \" ; sed -i s/a/b/ pkg/x.tshook-exit=0ALLOWEDhook-exit=2blockedsed -i s/a/b/ pkg/x.ts(control)hook-exit=2blockedhook-exit=2blockednode -e "…require(\"./a.json\")…st=>st.a…"(#10247 mirror)hook-exit=0ALLOWEDhook-exit=0ALLOWEDgrep -rn "he said \"sed -i\" once" .claude/(#10247 mirror)hook-exit=0ALLOWEDhook-exit=0ALLOWEDThe mirror case #10247 — an escaped quote inside a double-quoted word, which was a false BLOCK — stays green in both its twins.
#11133 —
strip_heredocs()read a heredoc introducer named in a comment (fail-OPEN)strip_heredocs()ran on raw text and modelled neither quotes nor comments, so a<<EOFoccurring inside prose registered as a real introducer. The delimiter then never appeared on a line of its own, so the pending heredoc was never satisfied and every remaining line — including real commands — was dropped before either quote-aware pass could see it.# use cat > /tmp/n <<EOF for notes⏎sed -i s/a/b/ pkg/x.tshook-exit=0ALLOWEDhook-exit=2blocked# plain prose comment⏎sed -i s/a/b/ pkg/x.ts(control)hook-exit=2blockedhook-exit=2blockedhook-exit=0ALLOWEDhook-exit=0ALLOWEDhook-exit=2blockedhook-exit=2blockedShape chosen: the card's candidate 1 (narrow, keeps the existing pass order), not candidate 2 (one merged scanner). Only the delimiter scan consults the comment rule; the line itself is passed through untouched, so the landed #10570 comment rule in the two quote-aware passes remains the single implementation of it — a second copy here is exactly how the two would drift.
The card's ordering hazard is the reason candidate 2 was not taken, and it is structurally out of reach in candidate 1: an unbalanced apostrophe inside a heredoc body cannot desynchronise the comment scanner, because body lines are consumed by the
pendingbranch andcontinuebefore the scan is ever reached. Pinned as its own self-test case in both directions.#11234 — header enumeration count
Three layers:→Four layers:. PR #10406 correctly moved the countTwo→Threewhen it added layer 3; PR #11129 added layer 4 and left the count alone. Comment-only, no logic, no self-test change. The header now readsFour layers:above exactly four enumerated items (verified by counting both).Layer 3's prose is also extended to state the outside-quotes half of the backslash rule that #11131 fixes. This deliberately does not add a fifth enumerated item, so the count #11234 corrects stays right.
Verification
Self-test matrix,
.claude/hooks/guard-main-checkout-bash.selftest.sh, at1a619db2:Both directions are pinned for every behaviour change, in the file's own matrix conventions.
Reverse-verification — each fix reverted from its own commit, mutation confirmed absent on disk by grep count before reading any result, restored under an
EXIT INT TERMtrap:sed -iinto the shared checkout is allowed through #11131 →106 passed, 4 failed; the four reds are exactly the command-name-writer cases (sed -i,rm,tee, and the multi-line form).117 passed, 4 failed; the four reds are exactly the comment-named-introducer cases.One case is labelled in the matrix as not discriminating and kept as a plain regression pin:
echo \" && echo x > pkg/x.tsblocked even while the passes disagreed, because a>target is collected wherever it appears. What the disagreement actually lost was the command-name writers, once the collapsed segment's head word becameecho. The comment says so, so no future reader mistakes it for a pin on this fix.Precision sweep (the hook's stated philosophy — no new false BLOCKS): the full pre-existing allowed-forms matrix is green, including the documented fail-open shapes, the non-ASCII-operator section, the #10247 section, the #10570 comment section, the escape hatch, the jq-less fallback and the no-cwd cases. New allow-side twins were added for both fixes: the escape must not manufacture a target, the same command aimed at a linked worktree stays allowed, and a quoted
#on an introducing line must not truncate the scan (truncating there would expose the heredoc body as commands — the load-bearing precision case).Gates — derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackon the actual change set (2 paths, three-dot vs merge basea2ec37707), not from a hand-written list:pnpm check:doc-authoring✓ doc authoring guard: 389 files clean — no bare metadata literals.pnpm --filter @objectstack/lint run check:doc-formula-expressions✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 416 files / 1447 TS blocks judged cleanpnpm check:pm-governed-merges✓ check-governed-merges --self-test: 119 assertionspnpm check:skill-frame-sync✓ check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic across 3 filesnode scripts/check-nul-bytes.mjscheck-nul-bytes: OK (scanned 6332 text file(s) … no raw ASCII control bytes).check:doc-formula-expressionsfirst came back red withERR_MODULE_NOT_FOUNDon@objectstack/formula/dist/index.mjs— the stale-dist trap in a fresh worktree, unrelated to this diff (which is two.shfiles). Green afterpnpm --workspace-concurrency=2 --filter '@objectstack/lint^...' build.Control:
.claude/hooks/guard-shared-stash.selftest.shshares the Bash matcher and payload plumbing and is untouched by this PR (empty diff vsa2ec377and vsHEAD). Read32 passed, 0 failedboth before and after.Scope
File surface is exactly the hook plus its self-test — verified against the recorded base
a2ec377:Nothing else was touched. In particular, and per the dispatch exclusions: the #10570 / PR #11129 comment-rule behaviour in
tokenize()/split_segments()is left exactly as it landed,guard-shared-stash.shis a different hook and untouched, and the objectui copy is not touched here — that port follows after this merges, in the declared objectstack → objectui direction.skip-changeset:.claude/**-only, publishes nothing.Generated by Claude Code