anthropics / anthropics/claude-code
EnterWorktree(path=) rejects valid worktrees when .git is a symlink — gitdir realpath mismatch vs string join (regression in 2.1.207)
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
## Summary
`EnterWorktree` with `path=` rejects every valid, Claude-created worktree with
`Cannot enter worktree: is not a linked worktree of ` 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):
```js
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 `/.git.nosync/worktrees` while `b` is always
`/.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
```bash
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.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by locating the EnterWorktree ownership check corresponding to the minified path comparisons in the report. Reproduce the case with a symlinked .git and verify the registered worktrees with git worktree list. Done means an existing worktree can be re-entered through the symlink while genuinely different directories are still rejected.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, javascript
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100