anthropics / anthropics/claude-code
Worktree pool reuses worktrees that belong to live, unarchived sessions — and evicts the owning session into the origin repo
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
## Summary
The Claude Desktop worktree pool leases a worktree to a **new** session while the worktree still belongs to an **existing, unarchived session with an open PR**. The lease appears to be tied to whether a session's *process is currently executing*, not to the session's *lifecycle*. As soon as an existing session goes idle (`isRunning: false`) its worktree is reported as `leasedBy none` and becomes a reuse candidate, even though the session is still listed, not archived, and has unmerged work.
Consequences observed in a single afternoon:
1. A new session was started inside another live session's worktree (twice, on the same worktree).
2. When the original owner session resumed, its worktree was gone from the pool; the app tried to re-lease it, failed, and **fell back to the origin repo** — so a session that was supposed to be isolated in a worktree is now operating directly on the main checkout.
3. During that recovery, `rebindWorktree` attempted `git checkout` **inside two other sessions' worktrees** (both with open PRs). It only failed because the target branch no longer existed. Had the branch existed, it would have switched the branch out from under two unrelated live sessions.
This is a data-safety issue, not just an annoyance: multiple sessions end up sharing one working directory, and branch checkouts are attempted in directories owned by other sessions.
## Environment
- Claude Desktop `2.110.1 (ae8d2a)`, built Sep 16 2026 (internal `appVersion` in `main.log`: `1.49585.0`)
- Claude Code (CCD) `2.1.265`, Node `24.20.0`
- Windows 11 Pro 26200
- Git worktrees under `/.claude/worktrees/`
- Workflow: one session per issue, each in its own worktree, several sessions alive in parallel with open PRs. Sessions are left unarchived on purpose while their PR is under review.
> Paths, repo names and issue numbers below are anonymized. Session ids are replaced with stable labels (`local_AAAA…`, `local_BBBB…`). Real ids and unredacted logs can be provided on request.
## Evidence from `main.log`
**Sept 15 — Session A legitimately creates and leases its worktree:**
```
17:55:27 [info] Created worktree "issue-201-a1b2c3" at C:\work\repo-a\.claude\worktrees\issue-201-a1b2c3
17:55:27 [info] Using worktree "issue-201-a1b2c3" ... for session local_AAAA...
17:55:27 [info] Starting local session local_AAAA... in C:\work\repo-a\.claude\worktrees\issue-201-a1b2c3
```
Session A stays alive and unarchived; its PR is open.
**Sept 17, 17:07 — a brand-new session takes A's worktree:**
```
17:07:12 [info] [rebindWorktree] Rebound C:\work\repo-a\.claude\worktrees\issue-201-a1b2c3
(was leased by none) to local_CCCC... on branch claude/issue-202-analysis-d94434
17:07:12 [info] [WorktreePool] Reused worktree issue-201-a1b2c3 for session local_CCCC... (was leased by none)
```
**17:09 — it happens a second time, to a different new session:**
```
17:09:19 [info] [WorktreePool] Released worktree issue-201-a1b2c3 to pool (was leased by local_CCCC...)
17:09:25 [info] [rebindWorktree] Rebound C:\work\repo-a\.claude\worktrees\issue-201-a1b2c3
(was leased by none) to local_BBBB... on branch claude/issue-202-analysis-9227da
17:09:25 [info] [WorktreePool] Reused worktree issue-201-a1b2c3 for session local_BBBB... (was leased by none)
17:09:25 [info] Starting local session local_BBBB... in C:\work\repo-a\.claude\worktrees\issue-201-a1b2c3
```
**17:13 — the original owner (Session A) resumes and is evicted:**
```
17:13:58 [error] [rebindWorktree] checkout --quiet claude/issue-201-a1b2c3 failed in
C:\work\repo-a\.claude\worktrees\issue-204-g7h8i9:
error: pathspec 'claude/issue-201-a1b2c3' did not match any file(s) known to git
17:13:59 [error] [rebindWorktree] checkout --quiet claude/issue-201-a1b2c3 failed in
C:\work\repo-a\.claude\worktrees\issue-203-d4e5f6:
error: pathspec 'claude/issue-201-a1b2c3' did not match any file(s) known to git
17:13:59 [info] [WorktreePool] No reusable worktree for C:\work\repo-a (2/2 candidates checked, 0 known unclean)
17:14:00 [warn] [CCD] Pool re-lease and fresh-create both failed for session local_AAAA...
on branch claude/issue-201-a1b2c3; falling back to origin repo C:\work\repo-a
```
Note that `issue-203-d4e5f6` and `issue-204-g7h8i9` are the worktrees of **two other live sessions with open PRs**. The pool tried to check out a foreign branch inside both of them.
## The `leasedBy none` bug
The sweep log shows `leasedBy` is empty even for a worktree whose session is *actively executing in it*:
```
16:48:53 [info] [WorktreePool] sweep: skipping eligible issue-205-j0k1l2 —
session local_DDDD... is executing in C:\work\repo-a\.claude\worktrees\issue-205-j0k1l2
(leasedBy none)
```
So `leasedBy` does not reflect ownership at all. The only thing protecting a worktree from reuse is the secondary "is executing" check — which saves a session **only while its process is running**. Any session that is alive but idle (app restarted, session not currently executing, user working elsewhere) is unprotected, no matter how much unmerged work it has.
## Reproduction
1. Start session A on repo R with a worktree (`.claude/worktrees/issue-201-…`). Open a PR from it. Do **not** archive the session.
2. Let session A go idle so its process stops (`isRunning: false` in the session list). Restarting the app reproduces this reliably.
3. Start a new session B on the same repo R, also requesting a worktree.
4. Observe in `main.log`: `[WorktreePool] Reused worktree issue-201-… for session B (was leased by none)`.
5. Session B's cwd is now A's worktree directory. Its branch name does not match the directory name.
6. Resume session A. It fails to re-lease its worktree and falls back to the origin repo.
Observed on 4 distinct sessions across 2 repos in 3 days, so the rate is not marginal.
## Expected behavior
- A worktree must stay leased to its session for the session's **lifetime**, not for the lifetime of its process. It should only return to the pool when the session is archived/deleted, or when the user explicitly releases it.
- `leasedBy` should carry the owning session id, so the sweep does not have to infer ownership from "is executing".
- `rebindWorktree` must never run `git checkout` inside a directory leased to another session.
- If a worktree cannot be re-leased, the session should be told clearly instead of silently falling back to the origin repo. Falling back to the main checkout defeats the isolation the worktree was created for, and it is not visible in the UI.
## Detection signature (useful for a fix and for users)
A hijacked session is detectable without logs: the worktree directory basename no longer matches the session branch.
```
healthy: cwd .../worktrees/issue-201-a1b2c3 branch claude/issue-201-a1b2c3
hijacked: cwd .../worktrees/issue-201-a1b2c3 branch claude/issue-202-analysis-9227da
```
## Secondary issue: orphaned worktree directories
`/.claude/worktrees/issue-205-j0k1l2` exists on disk but is absent from `git worktree list`, while its session is still alive with an open PR. Something removed the git registration (or created the directory outside git) without removing the directory. Related log line from the same period:
```
16:48:52 [info] [WorktreePool] untracked-dir GC: 1 untracked dir(s), 1 past min age, 1 removed
```
## Note on branch renames
In this workflow the branch created by the app (`claude/issue-NNN-…`) is later renamed to the team's convention (`feat/NNN-…`). That rename is why the re-lease `git checkout` failed with `pathspec … did not match`. The rename explains why *recovery* failed loudly; it does **not** cause the reuse itself — the pool had already handed the worktree to another session while the owner was alive.
## Current workaround
Archive a session as soon as its worktree must be protected, and check `basename(cwd) == branch` before trusting a new session's working directory. Neither is acceptable long-term: archiving a session with an open PR loses the working context needed for review feedback.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by tracing the WorktreePool sweep and rebindWorktree paths described in the logs, reproducing the idle-session case from the listed steps. Check how leasedBy, session lifecycle, and untracked-dir GC are handled. Done means idle unarchived sessions retain ownership, rebindWorktree never checks out in another session's worktree, and failed re-leasing does not silently fall back to the origin repo.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git
- Domain
- cli, devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100