Worktree pool reuses worktrees that belong to live, unarchived sessions — and evicts the owning session into the origin repo
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:
- A new session was started inside another live session's worktree (twice, on the same worktree).
- 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.
- During that recovery,
rebindWorktreeattemptedgit checkoutinside 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 (internalappVersioninmain.log:1.49585.0) - Claude Code (CCD)
2.1.265, Node24.20.0 - Windows 11 Pro 26200
- Git worktrees under
<repo>/.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
- Start session A on repo R with a worktree (
.claude/worktrees/issue-201-…). Open a PR from it. Do not archive the session. - Let session A go idle so its process stops (
isRunning: falsein the session list). Restarting the app reproduces this reliably. - Start a new session B on the same repo R, also requesting a worktree.
- Observe in
main.log:[WorktreePool] Reused worktree issue-201-… for session B (was leased by none). - Session B's cwd is now A's worktree directory. Its branch name does not match the directory name.
- 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.
leasedByshould carry the owning session id, so the sweep does not have to infer ownership from "is executing".rebindWorktreemust never rungit checkoutinside 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
<repo>/.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.