Repository navigation
test(hooks): add the missing self-test matrix for guard-main-checkout.sh (87 cases, ported) - #7264
Merged
Conversation
`guard-main-checkout.sh` was the only PreToolUse guard in this repo with no self-test — and it is the one both CLAUDE.md and AGENTS.md §9 name by path as the mechanism that enforces worktree-first. Its Bash-side follow-on and guard-shared-stash both ship hermetic matrices that `hook-selftests.yml` runs on every PR touching `.claude/hooks/**`; nothing exercised the primary guard at all, in either direction (a fail-open regression stops guarding the shared checkout in silence; a fail-closed one reads to an agent as a broken environment). Ported from objectstack's copy of the same matrix. The port is verbatim: the two repos' hooks differ by 9 diff lines, all inside one comment block, no executable line differs, and both settings.json route the same Edit|Write|NotebookEdit matcher — the sibling file was run here byte-for-byte unmodified first and returned 87 passed, 0 failed. The only edits are the two KNOWN HOLE sections, re-pointed at this repo's own cards for the same defects. 87 cases, 0 failed. No wiring edit is owed: `hook-selftests.yml` discovers matrices with `find` since #7019, and its header states that its recorded case counts are a dated measurement that must not be refreshed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
os-zhuang
approved these changes
Sep 2, 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 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 #6451
guard-main-checkout.shwas the only PreToolUse guard in this repo with no self-test, and it is the one bothCLAUDE.mdandAGENTS.md§9 name by path as the mechanism enforcing worktree-first. Its Bash-side follow-on andguard-shared-stashboth ship hermetic matrices; the primary guard had none, in either direction — a fail-open regression stops guarding the shared checkout in silence, a fail-closed one reads to an agent as a broken environment.One new file:
.claude/hooks/guard-main-checkout.selftest.sh(333 lines, mode 100755). Nothing else changed — see "the two legs that turned out to be no-ops" below.The port is verbatim, and that was measured before it was claimed
Ported from
objectstack-ai/objectstack:.claude/hooks/guard-main-checkout.selftest.sh@d63c8a2, per the 2026-08-26 triage ruling on the card.The two repos' hooks differ by 9 diff lines, all inside one comment block (objectstack's copy carries an extra note above the heredoc). No executable line differs, and both
settings.jsonroute the identicalEdit|Write|NotebookEditmatcher. So the sibling file was first run here byte-for-byte unmodified, through its ownGUARD_MAIN_CHECKOUT_HOOK/GUARD_MAIN_CHECKOUT_SETTINGSindirection, pointed at this repo's hook and settings:Zero cases needed adapting. The only edits below the header are the two
KNOWN HOLEsections, whose issue references are re-pointed at this repo's own cards (below); each keeps the sibling number as a cross-ref.diffagainst upstream is 4 hunks, all comment orecholabel text.In place, and through the real CI step
The
hook-selftests.ymlrun:block, executed locally verbatim (git rev-parse --short HEAD=3f5d6faf0):Non-vacuity — the matrix can go red against THIS repo's hook
Five mutation classes, each driven through the file's own footer recipe against a scratch copy in
$TMPDIR(GUARD_MAIN_CHECKOUT_HOOK); the real hook is never touched, and each mutation was confirmed to have landed on disk by a marker grep count going1 -> 0before the reading was accepted:*/worktrees/*) exit 0arm62 passed, 25 faileddirname "$file"withCLAUDE_PROJECT_DIR52 passed, 35 failedOS_ALLOW_MAIN_EDITSline85 passed, 2 failedexit 2becomesexit 045 passed, 42 failed83 passed, 4 failedgit diff --statonguard-main-checkout.shis empty after all five; only the new untracked matrix appeared.Two defects the port surfaced — filed, not fixed
Both
KNOWN HOLEsections reproduce here identically, so they are this repo's defects too and now have this repo's cards. Per the card's fence, this PR pins today's behaviour and says the hole out loud; it does not change the guard.guard-main-checkout.shreads any git-dir path containing/worktrees/as a linked worktree — a PRIMARY checkout under a dir namedworktreesis unguarded, and by DEPTH #7259 — any git-dir path containing/worktrees/reads as a linked worktree, so a primary checkout living under a dir namedworktreesis unguarded, and by depth (git prints a relative git-dir at a toplevel, absolute from a subdirectory). 4 cases pinned.guard-main-checkout.shnever readsnotebook_path, so everyNotebookEditis judged byCLAUDE_PROJECT_DIR— wrong in BOTH directions, though the matcher routes that tool to it #7260 —NotebookEdit's path key isnotebook_path, which the hook never reads, so every notebook edit is judged byCLAUDE_PROJECT_DIRinstead of by the file. Wrong in 4 of 5 measured directions, including two fail-open ones. 5 cases pinned.Neither card is closed by this PR; #7259 and #7260 remain open, and each names the mechanical edit to this matrix that its fix implies.
The two legs of the triage ruling that turned out to be no-ops
The 2026-08-26 triage named three legs. The port is leg 1. The other two were hypotheses, re-checked against today's tree:
hook-selftests.yml" — already automatic. The step discovers matrices withfind .claude/hooks -type f -name '*.selftest.sh'since ci(hook-selftests): discover the self-test matrices instead of enumerating them #7019, and the run above shows it picking up all four with no workflow edit. The header even anticipates this card by name at:51.:95-101: "Those counts stay, and the distinction is deliberate (objectui#6089): a measurement is HISTORY. It is read together with its date and its sha, and a matrix that grows afterwards does not make it false — it makes it older. Both have grown since (121 and 41 as of 2026-08-31, plus a third matrix), which is exactly what a dated measurement is allowed to do." That second sentence, itself dated, landed after the triage comment was written and already records the current figures. Editing either would convert a dated measurement into a live claim, which is the shape the same header rejects for step names. The live count is published by the thing that cannot get it wrong: the step prints how many matrices it discovered.So the workflow is untouched, deliberately. The hook header is untouched too, matching upstream — objectstack's
guard-main-checkout.shcarries no self-test pointer either, despite having had the matrix for longer.Gates
Derived from the workflow files that can see
.claude/hooks/**(onlyhook-selftests.ymlcarries a matchingpaths:filter; the rest are unfiltered). All run on3f5d6faf0, exit code captured before any pipe:.claude/hooks/guard-main-checkout.selftest.sh87 passed, 0 failed(exit 0)121 / 41 / 36 passed, 0 failed(all PASS)check-control-bytes✅ check-control-bytes: OK (scanned 5997 tracked text file(s); skipped 85 binary).check-shell-escape-residue✅ check-shell-escape-residue: OK (4/4 root(s) resolved …)check-changeset-presence✅ No source or published contract of a released package changed in this range, so no changeset is owed.check-upstream-port-parity✓ … 2 ported file(s) match objectstack-ai/objectstack@bf10debd5 modulo their declared divergences.check-governed-queue-guard --self-testOK check-governed-queue-guard self-test: 132 cases passTwo honest notes on scope rather than green-washing them:
check-shell-escape-residueresolvesAGENTS.md,CLAUDE.md,skills/andcontent/docs/— not.claude/hooks/. Its green is true and says nothing about this file. NOT MEASURED, not passed.check-control-bytesdoes cover it (the file is tracked and inside the 5997), and a manualgrep -naP '[\x00-…]'over the new file independently returns 0 hits.No changeset owed — quoted above;
.claude/**publishes nothing. This repo declares that with an empty-frontmatter changeset where one is needed and has noskip-changesetmechanism, and the presence gate is green without one, so none was added.Review posture
.claude/**is governed surface: draft, human merge only. Not marked ready, not enqueued, no auto-merge armed.🤖 Generated with Claude Code
https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
Generated by Claude Code