EnterWorktree(path=) rejects valid worktrees when .git is a symlink — gitdir realpath mismatch vs string join (regression in 2.1.207)
Summary
EnterWorktree with path=<existing worktree> rejects every valid, Claude-created worktree withCannot enter worktree: <path> is not a linked worktree of <repo> when the repository's .git is a
symlink (e.g. .git → .git.nosync, the common macOS pattern for keeping git internals out of
iCloud Drive sync under ~/Documents). Creating worktrees (name=) works fine; only re-entering an
existing one is broken.
Regression window (bisected from session transcripts)
- ≤ 2.1.206: 88
EnterWorktreecalls across these repos, 0 failures. - 2.1.207 (first ran 2026-07-13T09:47Z): first failure 2026-07-13T10:07Z, 20 minutes later.
- Still reproduces on 2.1.235 (macOS arm64, Homebrew cask).
Root cause
The ownership check resolves symlinks on only one side of the comparison. From the 2.1.235 binary
(minified):
let b = Sc.join(a, ".git", "worktrees"); // expected admin dir — plain string join, NOT symlink-resolved
v = await bc.realpath(S); // actual gitdir admin dir — realpath'd through .git → .git.nosync
if (!v || !E || yP(Sc.dirname(v)) !== yP(b) || yP(E) !== yP(Sc.join(s, ".git")))
throw new vv(`Cannot enter worktree: ${e} is not a linked worktree of ${i}.`);
With .git → .git.nosync, dirname(v) is always <repo>/.git.nosync/worktrees while b is always<repo>/.git/worktrees — the comparison can never succeed. Verified empirically: replicating the two
expressions in a script over four affected repos yields a mismatch every time. git itself is entirely
happy with the layout; git worktree list shows the worktrees as registered and healthy.
Reproduction
mkdir repo && cd repo && git init -b main && git commit --allow-empty -m init
mv .git .git.nosync && ln -s .git.nosync .git
claude # EnterWorktree(name="wt1") → works, creates .claude/worktrees/wt1
# exit; start a new session in the repo root:
# EnterWorktree(path=".claude/worktrees/wt1")
# → "Cannot enter worktree: ... is not a linked worktree of ..."
Suggested fix
Resolve the expected path the same way the actual one is resolved before comparing — realpath(b)
with a fallback to the literal path when it doesn't exist. This compares directory identity rather
than string spelling and does not loosen the check (genuinely different directories still mismatch;
the surrounding guards are unchanged). It would also fix latent /tmp vs /private/tmp mismatches
on macOS.
Impact
Any repo using the .git → .git.nosync iCloud-exclusion pattern (59 repos on this machine) loses
the ability to re-enter existing worktrees — e.g. resuming an open PR's worktree — and background
sessions must fall back to workarounds. EnterWorktree(name=…) creation is unaffected, which is
consistent with the creation path recording its own ownership state instead of re-deriving it.