Found while porting the sibling repo's self-test matrix for this hook (#6451). Out of scope for that card — that PR pins today's behaviour and says the hole out loud; it deliberately does not change the hook (.claude/** is governed surface, and the dispatch fence forbids weakening or "fixing" the guard in a matrix-only PR). Filed rather than fixed.
This is the objectui copy of objectstack-ai/objectstack#11809. The two hooks' executable lines are identical (their whole diff is 9 lines inside one comment block), so the defect is the same one, present here.
The line
.claude/hooks/guard-main-checkout.sh:42-44
case "$gitdir" in
*/worktrees/*) exit 0 ;;
esac
That is a substring match on a path, not a test for a linked worktree. A primary checkout that merely lives under a directory named worktrees produces a git-dir matching the pattern, and the guard exits 0 — an unguarded edit into a shared primary checkout, which is the exact failure worktree-first exists to stop.
Measured, not argued — and it is depth-dependent
git rev-parse --git-dir prints a RELATIVE git-dir (.git) at a repo's toplevel and an ABSOLUTE one from any subdirectory. Combined with the hook's nearest-existing-ancestor walk, the same unguarded checkout gets opposite verdicts by depth. With a fixture repo at TMP/worktrees/oddrepo:
payload file_path |
ancestor handed to git |
git-dir |
verdict today |
ODD/README.md |
repo toplevel |
.git (relative) |
block — correct, but only by accident |
ODD/brand/new/f.ts |
resolves up to toplevel |
.git (relative) |
block — ditto |
ODD/pkg/x.ts |
a subdirectory |
absolute, contains /worktrees/ |
allow — WRONG |
ODD/pkg/brand/new/f.ts |
a subdirectory |
absolute, contains /worktrees/ |
allow — WRONG |
All four are pinned as cases in the matrix landing on #6451, under a KNOWN HOLE banner that names this card and states they are a record of today's behaviour, not of intended behaviour. When this is fixed, the last two flip to block and that section of the matrix becomes a mechanical edit.
Note the fixture path is not exotic: git worktree add ../repo-task is the prescribed recipe, and an operator who instead keeps trees under a worktrees/ parent directory gets a silently unguarded primary checkout.
Shape of the fix (not asserted)
The real question git can answer is whether the git-dir is a linked worktree of some repo, e.g. comparing --git-dir against --git-common-dir (they differ exactly in a linked worktree), rather than pattern-matching the path. Whoever picks this up should decide; both repos should move together, and the matrix on #6451 gives the change a red/green.
Filed unassigned; no domain:* or type label set.
Found while porting the sibling repo's self-test matrix for this hook (#6451). Out of scope for that card — that PR pins today's behaviour and says the hole out loud; it deliberately does not change the hook (
.claude/**is governed surface, and the dispatch fence forbids weakening or "fixing" the guard in a matrix-only PR). Filed rather than fixed.This is the objectui copy of
objectstack-ai/objectstack#11809. The two hooks' executable lines are identical (their whole diff is 9 lines inside one comment block), so the defect is the same one, present here.The line
.claude/hooks/guard-main-checkout.sh:42-44That is a substring match on a path, not a test for a linked worktree. A primary checkout that merely lives under a directory named
worktreesproduces a git-dir matching the pattern, and the guard exits 0 — an unguarded edit into a shared primary checkout, which is the exact failure worktree-first exists to stop.Measured, not argued — and it is depth-dependent
git rev-parse --git-dirprints a RELATIVE git-dir (.git) at a repo's toplevel and an ABSOLUTE one from any subdirectory. Combined with the hook's nearest-existing-ancestor walk, the same unguarded checkout gets opposite verdicts by depth. With a fixture repo atTMP/worktrees/oddrepo:file_pathODD/README.md.git(relative)ODD/brand/new/f.ts.git(relative)ODD/pkg/x.ts/worktrees/ODD/pkg/brand/new/f.ts/worktrees/All four are pinned as cases in the matrix landing on #6451, under a
KNOWN HOLEbanner that names this card and states they are a record of today's behaviour, not of intended behaviour. When this is fixed, the last two flip toblockand that section of the matrix becomes a mechanical edit.Note the fixture path is not exotic:
git worktree add ../repo-taskis the prescribed recipe, and an operator who instead keeps trees under aworktrees/parent directory gets a silently unguarded primary checkout.Shape of the fix (not asserted)
The real question git can answer is whether the git-dir is a linked worktree of some repo, e.g. comparing
--git-diragainst--git-common-dir(they differ exactly in a linked worktree), rather than pattern-matching the path. Whoever picks this up should decide; both repos should move together, and the matrix on #6451 gives the change a red/green.Filed unassigned; no
domain:*or type label set.