Skip to content

test(hooks): add the missing self-test matrix for guard-main-checkout.sh (87 cases, ported) - #7264

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-6451-guard-main-checkout-selftest
Sep 2, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-6451-guard-main-checkout-selftest

Conversation

@yinlianghui

Copy link
Copy Markdown
Contributor

Fixes #6451

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 enforcing worktree-first. Its Bash-side follow-on and guard-shared-stash both 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.json route the identical Edit|Write|NotebookEdit matcher. So the sibling file was first run here byte-for-byte unmodified, through its own GUARD_MAIN_CHECKOUT_HOOK / GUARD_MAIN_CHECKOUT_SETTINGS indirection, pointed at this repo's hook and settings:

87 passed, 0 failed

Zero cases needed adapting. The only edits below the header are the two KNOWN HOLE sections, whose issue references are re-pointed at this repo's own cards (below); each keeps the sibling number as a cross-ref. diff against upstream is 4 hunks, all comment or echo label text.

In place, and through the real CI step

$ .claude/hooks/guard-main-checkout.selftest.sh
87 passed, 0 failed                                     # exit 0

The hook-selftests.yml run: block, executed locally verbatim (git rev-parse --short HEAD = 3f5d6faf0):

discovered 4 hook self-test(s):
  .claude/hooks/guard-main-checkout-bash.selftest.sh     121 passed, 0 failed   PASS
  .claude/hooks/guard-main-checkout.selftest.sh           87 passed, 0 failed   PASS
  .claude/hooks/guard-shared-stash.selftest.sh            41 passed, 0 failed   PASS
  .claude/hooks/guard-tree-enum.selftest.sh               36 passed, 0 failed   PASS

Hook self-tests — all 4 ran and passed                  # step exit 0

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 going 1 -> 0 before the reading was accepted:

mutation of a scratch copy result
drop the */worktrees/*) exit 0 arm 62 passed, 25 failed
replace dirname "$file" with CLAUDE_PROJECT_DIR 52 passed, 35 failed
drop the OS_ALLOW_MAIN_EDITS line 85 passed, 2 failed
final exit 2 becomes exit 0 45 passed, 42 failed
rename the key in the jq-less grep fallback 83 passed, 4 failed

git diff --stat on guard-main-checkout.sh is empty after all five; only the new untracked matrix appeared.

Two defects the port surfaced — filed, not fixed

Both KNOWN HOLE sections 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.

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:

  • "wire it into hook-selftests.yml" — already automatic. The step discovers matrices with find .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.
  • "refresh the header's stale recorded counts (100/32 vs the tree's 121/41)" — the header now forbids exactly that. :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.sh carries no self-test pointer either, despite having had the matrix for longer.

Gates

Derived from the workflow files that can see .claude/hooks/** (only hook-selftests.yml carries a matching paths: filter; the rest are unfiltered). All run on 3f5d6faf0, exit code captured before any pipe:

gate verdict line
.claude/hooks/guard-main-checkout.selftest.sh 87 passed, 0 failed (exit 0)
the other three matrices 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-test OK check-governed-queue-guard self-test: 132 cases pass

Two honest notes on scope rather than green-washing them:

  • check-shell-escape-residue resolves AGENTS.md, CLAUDE.md, skills/ and content/docs/ — not .claude/hooks/. Its green is true and says nothing about this file. NOT MEASURED, not passed.
  • check-control-bytes does cover it (the file is tracked and inside the 5997), and a manual grep -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 no skip-changeset mechanism, 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

`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
os-zhuang marked this pull request as ready for review September 2, 2026 11:26
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit 9650430 Sep 2, 2026
30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-6451-guard-main-checkout-selftest branch September 2, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants