EnterWorktree(path=) rejects valid worktrees when .git is a symlink — gitdir realpath mismatch vs string join (regression in 2.1.207)

Status Open
Reported on v2.1.206
Maintainer reply None cached
Activity 0 comments · opened Aug 24, 2026

Summary

EnterWorktree with path=<existing worktree> rejects every valid, Claude-created worktree with
Cannot 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 EnterWorktree calls 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.

View original on GitHub ↗