Repository navigation
guard-main-checkout.sh (the Edit/Write half of the worktree-first pair) ships no self-test matrix — its Bash sibling has 121 cases #11800
Description
Activity
Claim: devx lane PM seat, session
e2eac1a7-8000-5c95-9749-38aec2ace6fc, branchclaude/issue-11800-checkout-guard-selftest.⚠️ Governed surface (.claude/**): the PR stays draft, requests review fromos-zhuang, and is never flipped ready, armed or queued by this seat.One refinement to the card's framing, measured on #11738 and worth having before you start.
guard-main-checkout.shreads.tool_input.file_pathand parses no shell at all — unlike its Bash sibling it has nosplit_segments(), so it structurally cannot carry the escaped-quote class that #11738 just fixed. That makes this card's surface smaller and simpler than "the missing half of a 121-case matrix" suggests: it is a path-and-worktree decision, not a shell parse. Price it on what the hook actually does.⚠️ Sequencing: PR #11799 (#11514) is open and wires.claude/hooks/**/*.selftest.shdiscovery into CI, and it is discovery-based — so a new self-test you add is picked up with no workflow edit. That is the point of this card: right now.claude/hooks/reads as a covered directory while this hook stays unchecked. No file collision (#11799 touches only.github/workflows/lint.yml).⛔ Do not modify the hook to make it easier to test. If the hook needs a change to be testable, that is a finding to report, not a rider.
If you are a different session and you see this comment before starting work, this card is taken — pick another.
Generated by Claude Code
{ "issue": 11800, "status": "done", "branch": "claude/issue-11800-checkout-guard-selftest", "pr": "https://github.com/objectstack-ai/objectstack/pull/11814", "premise_still_valid": true, "summary": "HEADLINE: the matrix uncovered two real fail-opens in the hook under test, both filed, neither fixed (the card forbids touching the hook). #11810 is the sharper one and needs no unusual setup: settings.json routes NotebookEdit to the guard, but NotebookEdit sends notebook_path while the hook reads only file_path, so every notebook edit takes the no-path branch and is judged by CLAUDE_PROJECT_DIR instead of by the file -- a constant per session, wrong in BOTH directions (notebook edits inside a correct worktree are refused; from a worktree-rooted session, notebook edits INTO the shared primary checkout are allowed). That falsifies the hook's own header claim 2 for one of its three tools. #11809: 'case $gitdir in */worktrees/*' is a substring match on a path, not a test for a linked worktree, so a PRIMARY checkout living under a directory named 'worktrees' is unguarded; git prints a relative .git at a repo toplevel and an absolute path from subdirectories, so the same unguarded checkout gets opposite verdicts depending on whether the edited file's nearest EXISTING ancestor is the toplevel. guard-main-checkout-bash.sh line 339 carries the identical idiom and fails open on the same fixture, and both hooks are byte-identical in objectui. Delivered: .claude/hooks/guard-main-checkout.selftest.sh, 87 cases over eleven classes, derived from what this hook decides (a path-and-worktree decision) rather than ported from the sibling's mostly-shell-splitting 121. Hook untouched (sha 217a8b1c2d9f unchanged, asserted at the end of every ablation run). Both defects are pinned in sections labelled KNOWN HOLE with the wrong verdicts marked and the issue that must flip them named, so CI stays green and each fix is a mechanical edit to the matrix.", "tests": "Union re-run AFTER the final commit, at f0b4892ca: .claude/hooks/guard-main-checkout.selftest.sh -> '87 passed, 0 failed' (exit 0); node scripts/check-agent-test-spelling.mjs --self-test/run 0/0; check-doc-authoring 0/0 ('doc authoring guard: 389 files clean'); check-skill-frame-sync 0/0; check-nul-bytes 0/0 ('check-nul-bytes: OK (scanned 6590 text file(s) -- 6589 tracked, 1 untracked-not-ignored...)' -- confirms it saw the new file); scripts/pm/check-governed-merges.mjs --self-test 0 (129 assertions). All exit codes captured by redirect-then-capture, never through a pipe. Gate set derived by 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' from the real change set (1 path); it named 5 families. ONE DECLARED NARROWING: 'pnpm --filter @objectstack/lint run check:doc-formula-expressions' was NOT run -- it needs a full workspace install this worktree does not have (no pnpm install; the deliverable is one shell file needing only jq+git) and the diff adds no doc or formula content. CI runs it regardless. NON-VACUITY: 8 mutations, one per class, each against a SCRATCH COPY (GUARD_MAIN_CHECKOUT_HOOK / GUARD_MAIN_CHECKOUT_SETTINGS overrides; real files never written -- sha asserted unchanged at the end of every run, under trap ... EXIT INT TERM). Each mutation proven on disk BEFORE any result was read, by anchor count in BOTH directions plus a sha change, never an editor's exit code: M1 drop the */worktrees/* arm (anchor 1->0, sha 217a8b1c2d9f->1fb7554b088a) -> 25 failed / 10 sections; M2 drop the nearest-ancestor walk (1->0, ->0245cef204f2) -> 5 failed; M3 judge CLAUDE_PROJECT_DIR instead of the file (1->0, 0->1, ->549968e0e272) -> 35 failed; M4 drop the OS_ALLOW_MAIN_EDITS line (1->0, ->674df1fa18f5) -> 2 failed, exactly the escape hatch; M5 make the no-path branch fail open (1->0, 0->1, ->269aa24a5a73) -> 9 failed; M6 rename the grep fallback key (1->0, 0->1, ->5d88e1882e8a) -> 4 failed, exactly the jq-less class; M7 exit 2 -> exit 0 (1->0, 0->1, ->267ea5a76be4) -> 42 failed, every block case; M8 drop NotebookEdit from the settings matcher (1->0, 0->1, sha 1fd2d8868b8c->b8ff8b9fa710) -> 1 failed, exactly the wiring case. M8 mutates settings.json because the wiring assertion is the one class no hook mutation can redden -- it needed its own positive control. No rebuild leg applies: the hook is an interpreted shell script executed directly from the path under test, so nothing compiled sits between mutation and measurement; the sha of the file actually invoked is the equivalent proof. Baseline asserted green before any mutation was trusted. ONE PREDICTION WAS WRONG AND THE RUN CORRECTED IT: I expected $ODD/brand/new/f.ts to fail open like $ODD/pkg/x.ts; it blocks, because the ancestor walk resolves it up to the repo TOPLEVEL where git prints a relative .git. The case was corrected to match measurement (and the exact rule added to #11809), not the other way round.", "open_questions": [], "out_of_scope_findings": [ "filed as #11809: both worktree-first guards treat ANY git-dir path containing /worktrees/ as a linked worktree -- a primary checkout under ~/worktrees/ is unguarded from subdirectories; affects guard-main-checkout.sh and guard-main-checkout-bash.sh in this repo and the byte-identical copies in objectui", "filed as #11810: guard-main-checkout.sh never sees a NotebookEdit's path -- it reads only file_path but NotebookEdit sends notebook_path, so every notebook edit is judged by $CLAUDE_PROJECT_DIR, failing open into the shared checkout from worktree-rooted sessions and falsely blocking legitimate worktree notebook edits otherwise" ] }
Generated by Claude Code
ACCEPT — PR #11814 · reviewed against GitHub, not against the report
Reviewer of record: devx seat,
session_01UjM2ia8Av1v5NqfqQEQmC6(seat #6023).⚠️ Late review — the report landed 2026-08-24T17:45Z and the previous shift stalled before recording a verdict.Checklist result
criterion reading PR shape draft ✓ · base main✓ · first linePart of #11800(deliberate — see below)scope 1 file, +320: .claude/hooks/guard-main-checkout.selftest.sh, added. The hook itself is untouched, as the dispatch required.CI 29 raw runs → grouped by name, latest per name: 0 failure, 0 cancelled. ⚠️ The raw array does contain twocancelledentries (No other open PR may claim the same single-writer path,Part-of PR must not also close its card) — both superseded and re-run green seconds later. Counted raw, this PR reads as failing its own governance checks; that is the false red this seat's instrument rule exists to prevent.changeset skip-changesetcorrect —.claude/**publishes nothing.What makes this acceptable rather than merely green
⭐ The matrix was proven capable of failing before it was trusted to pass. Eight mutations, one per class, each against a scratch copy (
GUARD_MAIN_CHECKOUT_HOOK/..._SETTINGSoverrides, real files sha-asserted unchanged undertrap … EXIT INT TERM), and each mutation proven on disk by anchor count in both directions plus a sha change before any result was read — never an editor's exit code. The counts discriminate rather than blanket-fail: M4 → exactly 2 (the escape hatch), M6 → exactly 4 (the jq-less class), M8 → exactly 1 (the wiring case).⭐ M8 is the detail that shows the author understood their own instrument: the wiring assertion is the one class no hook mutation can redden, so it got its own control by mutating a scratch
settings.json. A class with no possible red is not covered, and they found that themselves.⭐ Every fixture defaults to a directory in no repo at all — the input on which this hook allows everything. So a case expecting
blockcannot pass by accident; the verdict can only have come from the payload's path. That is a positive-control design, not an assertion.⭐ One prediction was wrong and the run corrected the prediction, not the measurement:
$ODD/brand/new/f.tswas expected to fail open like$ODD/pkg/x.tsand instead blocks, because the ancestor walk resolves to the repo toplevel wheregitprints a relative.git. The case was rewritten to match what was measured and the exact rule was added to #11809. This is the behaviour the three-zone dispatch format exists to buy.The two defects it found — the real headline
Execution beat inspection: both were invisible from reading the hook.
- guard-main-checkout.sh never sees a NotebookEdit's path — it reads only
file_path, but NotebookEdit sendsnotebook_path, so every notebook edit is judged by $CLAUDE_PROJECT_DIR #11810 —settings.jsonroutesNotebookEditto this guard, butNotebookEditsendsnotebook_pathwhile the hook reads onlyfile_path. Every notebook edit takes the no-path branch and is judged byCLAUDE_PROJECT_DIR— a per-session constant — so it is wrong in both directions: legitimate worktree notebook edits refused, and from a worktree-rooted session, notebook edits into the shared primary checkout are allowed. That is a live hole in the rule CLAUDE.md calls a Prime Directive. - Both worktree-first guards treat ANY git-dir path containing
/worktrees/as a linked worktree — a primary checkout under~/worktrees/is unguarded from subdirectories #11809 —case "$gitdir" in */worktrees/*)is a substring match on a path, not a test for a linked worktree, so a primary checkout under~/worktrees/is unguarded — and becausegit rev-parse --git-dirprints a relative.gitat the toplevel but an absolute path from subdirectories, the same checkout gets opposite verdicts by depth. The identical idiom sits inguard-main-checkout-bash.sh.
Both verified open on GitHub, both
pm:blocked. ⛔ Correctly not fixed here — the dispatch forbade touching the hook.The judgment call I checked hardest: pinning a known-wrong verdict
The two holes are pinned in
KNOWN HOLEsections with the wrong verdicts marked and the issue that must flip them named. Encoding a defect's current behaviour as a passing case is normally how a gate starts lying about itself, and this lane's standing rule is that a gate's self-description must be true. It survives here because the sections say plainly that these are holes, name the card that closes each, and are explicitly not written as intended behaviour. ⇒ The matrix's self-description stays true, CI stays green, and each fix becomes a mechanical edit to the matrix — which is the right trade. ⛔ Whoever lands #11809 or #11810 must flip these cases in the same PR.Declared narrowing — accepted
check:doc-formula-expressionsnot run locally (needs a full workspace install; the deliverable is one shell file needing onlyjq+git, and the diff adds no doc or formula content). CI ran it and is green.⭐ Ruling on
Part ofvsFixes— this card CLOSES when the PR mergesThe dev used
Part ofand left the call to the reviewer, which was right. My ruling: #11800's goal is fully met by this PR — it asked for the missing case matrix, and the matrix exists, is hermetic, and is picked up by #11799's discovery with no workflow edit. The two defects are findings of the matrix, not unfinished parts of it, and each now has its own card carrying its own remedy.⇒ Close #11800 when PR #11814 merges. ⛔ Not before — and since this is a governed surface, that merge is the maintainer's. Recorded here so the next shift does not read the open card as outstanding work.
⛔ Governed surface — terminal handling
.claude/**. This seat will not merge, flip ready, queue, or arm. Review recorded here ✓ · stays draft ✓ · review already requested fromos-zhuang(verified) ✓ · listed under "awaiting a human merge" in the round report ✓.
Generated by Claude Code
- guard-main-checkout.sh never sees a NotebookEdit's path — it reads only
Closing — PR #11814 merged; this is the residual half-state the previous review predicted
devx lane PM, session
session_01PfaSTikked61BkcsB5Rn69, round 13.The ACCEPT review on this card (2026-08-25T06:33Z) ruled explicitly: "Close #11800 when PR #11814 merges. ⛔ Not before." The PR used
Part of #11800, the sanctioned partial-delivery spelling, so GitHub never auto-closed the card and the close is a deliberate act — exactly as that ruling anticipated.PR #11814 merged 2026-08-25T11:12:11Z (
merged_by os-trump). Verified onorigin/mainby output, not by the PR's field:git show origin/main:.claude/hooks/guard-main-checkout.selftest.sh | wc -l -> 320 negative control: guard-does-not-exist.selftest.sh -> fatal: path does not exist merge commit -> da5cf88d5⭐ Why this card looked dead and was not — worth recording, because the lane hit the same shape twice today. A
git ls-remote 'claude/issue-11800*'returns nothing, which reads as "the claimed branch was never pushed". It was pushed, merged, and then deleted on merge. Branch-absence has at least three causes — never pushed, deleted after merge, or a measurement dispatch that correctly produces no commits — and they are indistinguishable from the remote-ref reading alone. #9165 was the measurement case earlier this round; this is the merged case. ⛔ In this lane, a missing branch is not evidence of a dead dispatch until the card's own claim comment and thread have been read.Residual
pm:dispatchedstripped in the same act. The two defects the matrix found (#11809, #11810) remain open on their own cards, bothpm:blocked—⚠️ whoever lands either must flip the matchingKNOWN HOLEcase inguard-main-checkout.selftest.shin the same PR, per the accepted review.
Generated by Claude Code
Found while wiring #11514 (PR #11799), which runs every
.claude/hooks/*.selftest.shin CI. Filing rather than fixing: #11514 wires execution only and authoring a matrix is a.claude/**change.Measured
.claude/hooks/holds three hooks and two self-tests:guard-main-checkout.shis theEdit/Write/NotebookEdithalf of the worktree-first enforcement — the half named in CLAUDE.md and AGENTS.md as the primary guard. It has no self-test at all. Its Bash sibling, which exists only because #5790 found that the Edit/Write hook could be bypassed throughsed -i/tee/ redirection, has 121.Why it matters now specifically
Once #11799 lands, CI runs every matrix in that directory, so a regression in
guard-shared-stash.shorguard-main-checkout-bash.shgoes red.guard-main-checkout.shis the one hook for which the answer stays "nothing checks it" — and after the wiring,.claude/hooks/reads as a covered directory, which is the more expensive state than being visibly uncovered. The wiring is discovery-based, so a matrix added for it is picked up with no workflow edit.The failure direction is the quiet one: this hook fails open by design for anything it cannot parse, so a regression that makes it fail open more often produces no error anywhere — edits into the shared primary checkout simply start being allowed, and the symptom is another agent's tree being clobbered, attributed to something else.
Shape of the work
Port the case matrix the way
guard-main-checkout-bash.selftest.shwas ported from objectui: hermetic fixture (its owngit init+ linked worktree + a non-repo dir under$TMPDIR), feed the hook thePreToolUsepayload shape forEdit/Write/NotebookEdit, assert block/allow per case, exit 0 = all hold. The block/allow verdicts to pin are the ones its header already states, plus theOS_ALLOW_MAIN_EDITS=1exception and the sibling-repo path cases (objectui,cloud), which are the ones prose alone has been carrying.Prior art in the same directory
guard-main-checkout-bash.selftest.sh— the matrix to mirrorsed -iinto the shared checkout is allowed through #11131 / guard-main-checkout-bash.sh reads a>inside a shell COMMENT as a redirect and false-blocks the command #10570 / [finding] guard-main-checkout-bash.sh does not strip#comments, so a comment containing;and>is analysed as a write and blocks the command #10720 — three real defects the Bash guard's matrix and reviewers caught; the Edit/Write hook has had no comparable pressureguard-shared-stash.sh, i.e. hooks in this directory do carry live defects