Found while landing #6880 (PR #6976). Filing rather than widening that PR: its triage comment pinned scope to the FLAG INSERT ONLY in the two instruction files, so the hook copies were deliberately left alone.
What drifted
#6880 hardens the prescribed worktree recipe to git worktree add --no-track ... in CLAUDE.md:16 and AGENTS.md:234. But the recipe is prescribed in four places in this repo, not two. The other two are the PreToolUse guards' own remediation messages:
.claude/hooks/guard-main-checkout.sh:57.claude/hooks/guard-main-checkout-bash.sh:544
both spelling it (placeholders written as words here; the files use angle-bracket placeholders):
git fetch origin main && git worktree add ../NAME-TASK -b BRANCH origin/main
Why this copy matters more than the other two
The instruction-file copies are read at orientation. These two are printed at the exact moment an agent has just been blocked and is looking for the command to run instead. That is the highest-conversion copy in the repo — it is read under time pressure, by an agent that has already been told it did something wrong, and it is copy-pasteable as-is. Hardening the docs while the block message still hands out the unhardened form leaves the hazard sitting at the point of use.
The hazard is #6880's, unchanged: plain -b writes branch.NAME.remote and branch.NAME.merge into the one .git/config every linked worktree of a repo shares. That write can fail after the branch is created, leaving a branch with no worktree — a half state the error text does not name, whose "the worktree exists" misreading sends the agent to edit the shared primary checkout, i.e. exactly what these two hooks exist to prevent.
Measured
Complete repo-wide sweep on origin/main at fd8dace (excluding node_modules, .git) for worktree add with a -b:
| location | kind | in #6880's scope? |
|---|
CLAUDE.md:16 | prescribed recipe | yes — hardened by PR #6976 |
AGENTS.md:234 | prescribed recipe | yes — hardened by PR #6976 |
.claude/hooks/guard-main-checkout.sh:57 | hook remediation text | no — this card |
.claude/hooks/guard-main-checkout-bash.sh:544 | hook remediation text | no — this card |
.changeset/6208-worktree-recipe-fetch-base.md:15 | historical changeset | no — a record of a past change, must not be rewritten |
guard-main-checkout-bash.selftest.sh:40 | creates a real scratch worktree in a temp repo | no — not a prescription, no shared .git |
guard-tree-enum.selftest.sh:99 | fixture asserting the hook ALLOWS that command | no — an allow-list input, not a prescription |
No test pins the spelling
Checked before filing, because a pin would have made this the same change's test face and therefore in scope for #6880. The three self-tests that mention AGENTS.md / CLAUDE.md use them only as filenames inside test command strings (expect allow 'grep -n "git stash" AGENTS.md' and similar); none reads or asserts instruction-file content. So the two hook messages are free text that nothing verifies against the documented recipe — which is also why they could drift apart in the first place.
⇒ Worth considering alongside the fix: a check that the recipe has ONE spelling across all four sites, so the next hardening cannot land in two of them. Without it this drift class recurs on every change to the recipe.
Duplicate check
Searched before filing; instrument alive with a non-empty control (the query returned #6880, this finding's own parent subject, as a positive control — not a false zero). Two open neighbours, neither covering this:
Refs
Found while landing #6880 (PR #6976). Filing rather than widening that PR: its triage comment pinned scope to the FLAG INSERT ONLY in the two instruction files, so the hook copies were deliberately left alone.
What drifted
#6880 hardens the prescribed worktree recipe to
git worktree add --no-track ...inCLAUDE.md:16andAGENTS.md:234. But the recipe is prescribed in four places in this repo, not two. The other two are the PreToolUse guards' own remediation messages:.claude/hooks/guard-main-checkout.sh:57.claude/hooks/guard-main-checkout-bash.sh:544both spelling it (placeholders written as words here; the files use angle-bracket placeholders):
Why this copy matters more than the other two
The instruction-file copies are read at orientation. These two are printed at the exact moment an agent has just been blocked and is looking for the command to run instead. That is the highest-conversion copy in the repo — it is read under time pressure, by an agent that has already been told it did something wrong, and it is copy-pasteable as-is. Hardening the docs while the block message still hands out the unhardened form leaves the hazard sitting at the point of use.
The hazard is #6880's, unchanged: plain
-bwritesbranch.NAME.remoteandbranch.NAME.mergeinto the one.git/configevery linked worktree of a repo shares. That write can fail after the branch is created, leaving a branch with no worktree — a half state the error text does not name, whose "the worktree exists" misreading sends the agent to edit the shared primary checkout, i.e. exactly what these two hooks exist to prevent.Measured
Complete repo-wide sweep on
origin/mainatfd8dace(excludingnode_modules,.git) forworktree addwith a-b:CLAUDE.md:16AGENTS.md:234.claude/hooks/guard-main-checkout.sh:57.claude/hooks/guard-main-checkout-bash.sh:544.changeset/6208-worktree-recipe-fetch-base.md:15guard-main-checkout-bash.selftest.sh:40.gitguard-tree-enum.selftest.sh:99No test pins the spelling
Checked before filing, because a pin would have made this the same change's test face and therefore in scope for #6880. The three self-tests that mention
AGENTS.md/CLAUDE.mduse them only as filenames inside test command strings (expect allow 'grep -n "git stash" AGENTS.md'and similar); none reads or asserts instruction-file content. So the two hook messages are free text that nothing verifies against the documented recipe — which is also why they could drift apart in the first place.⇒ Worth considering alongside the fix: a check that the recipe has ONE spelling across all four sites, so the next hardening cannot land in two of them. Without it this drift class recurs on every change to the recipe.
Duplicate check
Searched before filing; instrument alive with a non-empty control (the query returned #6880, this finding's own parent subject, as a positive control — not a false zero). Two open neighbours, neither covering this:
guard-main-checkout.shis the only PreToolUse guard with no self-test — the one both CLAUDE.md files name by path as the enforcement mechanism #6451 (pm:blocked) —guard-main-checkout.shhas no self-test. Adjacent and related (a self-test is one way this drift would have been caught) but a different defect: that card is about the absence of a test, this one is about the content of the remediation string. Whoever takes either should read the other.Refs
guard-main-checkout.shis the only PreToolUse guard with no self-test — the one both CLAUDE.md files name by path as the enforcement mechanism #6451 — the missing self-test on one of the same two hooksmain, a ref nothing in the recipe fetches — the objectui half of objectstack#11540 #6208 — the previous drift in the same recipe (branching off an unfetched localmain)--no-trackhardening this family came from