[BUG] Worktree recycling destroys live sibling sessions' worktrees — including locked ones and uncommitted work
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Summary
Under heavy multi-session use of one repository (many concurrent Claude Code sessions, peak ~18 registered worktrees), Claude Code's worktree recycling repeatedly deletes worktrees that belong to other live sessions. It does not respect git worktree lock, and recycling a worktree that contains uncommitted/untracked files destroys them (recycle = delete + recreate fresh checkout). One session's worktree was destroyed five times in a single session; across sessions the same day we permanently lost uncommitted work once (7 dirty files) and nearly lost a 23-file feature implementation.
Observed timeline (one session, 2026-07-13)
- Session starts in registered worktree
.claude/worktrees/<name>(clean, tip == main — it was a read-only analysis session). - Worktree deleted out from under the session mid-command: "Working directory ... was deleted; shell cwd recovered to ...". No harness notice at this point.
- Session recreates the worktree manually (
git worktree add) and locks it:git worktree lock --reason "ACTIVE Claude session — do not prune" <path>. Deleted again ~3 minutes later. Metadata under.git/worktrees/was cleanly removed — the lock was not honored (plaingit worktree remove, even with a single--force, refuses locked trees; whatever removed it went past that). - Session creates a worktree outside the repo at
/private/tmp/<name>(same branch) and edits files there. Deleted minutes later with uncommitted changes in it. - Harness then emits, twice in succession: "The git worktree at ... was recycled. This session now operates on a fresh worktree at ..."
- Finally: "The git worktree at ... was deleted. This session now operates on the origin repository at ..." — i.e. after losing the race repeatedly, the session is dropped into the shared main checkout, the highest-contention place possible, where its writes collide with every other session.
Corroborating losses (same repo, same day, other sessions)
- A session's worktree holding 7 dirty uncommitted files (feature WIP) was swept mid-session; the files were permanently lost.
- A worktree holding a 23-file uncommitted feature implementation was deleted; the building session's own commit message recorded the work as "lost to repeated worktree recycling before commit". (Recovered only because that session had copied the files into the main checkout minutes before.)
- A third session reported: "my worktree got deleted mid-command by a parallel session (the 18-worktree contention)". Every session perceives a hostile "parallel session"; the recycle notices in step 5 indicate the deletions come from Claude Code's own worktree lifecycle.
Why this seems like a bug
Docs describe worktrees as "auto-cleaned if unchanged" — but a live session's in-use worktree qualifies merely because its tree is clean and its branch tip equals main, which is also the signature of a freshly started or read-only session. And dirty worktrees were deleted too.
Suggested fixes
- Ownership/liveness check before recycle: only remove worktrees created by the current session, or lock worktrees at creation with
--reason "pid <N> session <id>"and probe liveness before removal (this also makes liveness visible to external tooling). - Honor
git worktree lockunconditionally — never double-force a locked tree. - Never delete dirty: if
git status --porcelainis non-empty, refuse or move the tree aside (e.g. rename to<name>.orphaned-<ts>) instead of deleting. - Don't fall back to the origin repository after repeated recycles — that converts an isolation mechanism into a shared-checkout collision.
- Log which process/heuristic triggered each recycle in the notice, so multi-session users can diagnose.
What Should Happen?
- A worktree registered to a live session is never deleted by another session's harness.
git worktree lock(git's native "in use" claim) is honored.- A worktree containing uncommitted or untracked files is never deleted silently; recycling preserves (or refuses on) dirty state.
- A session is never silently relocated into the shared main checkout while sibling sessions are active.
Error Messages/Logs
Bash tool, mid-session (no prior notice):
Working directory "/Users/<user>/dev/<repo>/.claude/worktrees/<name>" was deleted; shell cwd recovered to "/Users/<user>". Re-issue your command (it will run from the recovered directory).
Harness system reminders, later in the same session (back to back):
The git worktree at /Users/<user>/dev/<repo>/.claude/worktrees/<name-1> was recycled. This session now operates on a fresh worktree at /Users/<user>/dev/<repo>/.claude/worktrees/<name-2>, checked out to the same branch (<branch>). Absolute paths from earlier in the conversation that reference the old worktree no longer exist.
The git worktree at /Users/<user>/dev/<repo>/.claude/worktrees/<name-2> was recycled. This session now operates on a fresh worktree at /Users/<user>/dev/<repo>/.claude/worktrees/<name-3> ...
The git worktree at /Users/<user>/dev/<repo>/.claude/worktrees/<name-3> was deleted. This session now operates on the origin repository at /Users/<user>/dev/<repo>. File paths from earlier in the conversation that reference the worktree no longer exist.
Environment: Claude Code 2.1.207 (desktop app), macOS 26.6, git 2.50.1 (Apple Git-155). Many concurrent sessions against one repo; peak ~18 registered worktrees.
Steps to Reproduce
- Pick one repository. Start 6–10 concurrent Claude Code sessions against it, each isolated in a
.claude/worktrees/<name>worktree (the default worktree isolation). - Keep one session read-only (analysis/review work): its tree stays clean and its branch tip stays == main.
- Watch
git worktree listwhile other sessions start and stop.
Observed results:
- The read-only session's worktree is removed by sibling activity within minutes ("Working directory ... was deleted" on its next Bash call, later "The git worktree at ... was recycled" notices).
- Repeat with
git worktree lock --reason "in use" <path>applied — the worktree is still removed (metadata cleanly deleted, so it is a real remove/prune, not an rm). - Scatter untracked files into a candidate worktree first — they are destroyed on recycle.
- After several recycle rounds the session is relocated into the origin repository (shared main checkout).
Related issues (this report is distinct from both):
- #74386 covers a user-directed discard with no liveness signal; this report shows the AUTOMATIC recycler doing the same unprompted, and bypassing
git worktree lock— a liveness signal that does exist. - #74782 (closed) covers the mid-session teardown + silent fall-through to the parent repo's .git; still reproducible on 2.1.207, and the "operates on the origin repository" fallback here recreates exactly the shared-checkout hazard that issue describes.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.207 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other
Additional Information
Sessions run in the Claude Code desktop app (macOS) — hence Terminal/Shell = Other. Cursor IDE worktrees under ~/.cursor/worktrees/<repo>/ also existed in the same repo but were NOT the actor: no repo script or IDE process was running at the deletion times, and the harness's own "was recycled" notices account for the removals.
User-side mitigations we've adopted meanwhile, which may be useful signal for the fix design: worktree-locking at creation with a PID reason, an age-floor + lock-respect patch to our repo's own cleanup script, and falling back to plain git clone (own .git, invisible to git worktree list) as the only isolation that survives — which defeats the purpose of the built-in worktree isolation.
3 Comments
Independent reproduction on 2.1.209 — and it is not confined to heavy multi-session use
Corroborating this on a different surface, with a much smaller worktree population than the ~18 in the report, plus five controlled probes that rule out three plausible-but-wrong triggers. Environment: Claude Code 2.1.209, macOS arm64, model
claude-opus-4-8.What happened
Two
isolation: "worktree"Agent-tool subagents (not sessions in manually-registered worktrees — a different entry point to the same recycler) had their worktrees deleted mid-run, at ~225s and ~227s elapsed, while both were alive and mid-task. Every subsequent tool call failed:Repo had only 4 registered worktrees at the time (main + one analysis worktree + the two agent worktrees). So the trigger does not require the ~18-worktree, heavy-concurrency regime described above — two harness-managed agent worktrees was enough.
Corroborating the two load-bearing claims in this report
1. The lock is not honored — confirmed independently. The harness does lock these worktrees while the agent runs;
git worktree listshows it plainly for a live agent:The two that died were locked by the same mechanism, and were removed anyway.
2. Clean removal semantics, not an
rm -rf. Directory and.git/worktrees/admin entry both gone, branch retained, and a subsequentgit worktree prunefound nothing to prune. A strayrm -rfleaves a stale admin entry thatprunethen reports. Whatever removed them went through git's own removal path, past the lock.3. Nothing logs it.
~/.claude/daemon.logcontains no record of the removal — no "removed", no "recycled". The victim discovers the deletion only viaENOENTon its next tool call. Even a single log line naming the remover would collapse the diagnostic cost here from hours to seconds.Ruling out #75609 for this instance
#75609 attributes worktree deletion to a failed self-update. Not our path: our update succeeded ~12 h before the incident, and there were no daemon worktree events.
So the recycler can destroy live locked worktrees without a failed update in the picture. Worth keeping the two issues distinct.
Negative results — five controlled probes, three triggers ruled out
I could not reproduce it deliberately, which is itself triage signal. Each probe was an
isolation: "worktree"agent in the same repo/harness, pollingtest -d "$(pwd)"every 20–30s:| hypothesis | probe | max elapsed | verdict |
|---|---|---|---|
| "auto-cleaned if unchanged" fires on a live agent (worktree stays clean during the long read-only phase) | control: empty
git statusthroughout, then an edit | 402s | NOT the trigger — survived || Two concurrent worktree agents collide | probes A + B, launched in one message | 351s / 419s | NOT the trigger — both survived |
| Agent switches off the harness-assigned
worktree-agent-<id>branch viagit checkout -b(one victim had done exactly this) | probe C (210s, inconclusive) → probe D | 498s | NOT the trigger — survived, 2.2× past the death window |Also excluded by direct inspection: neither victim removed its own worktree (audited every
git/rmcommand in both transcripts — nogit clean, norm -rf, nogit worktree remove/prune), and no hook or script in my setup touches worktrees.So: not idle-unchanged reaping, not 2-way concurrency, not branch switching, not self-inflicted, not a failed update. The trigger remains unidentified — consistent with this report's "recycling" framing, but I could not force it.
Impact here
No uncommitted work was lost (both victims were still in their read-only phase, which is why I initially misread this as the "unchanged ⇒ reaped" behaviour). The cost was two killed agents and a long false-lead investigation. Others will be less lucky: this report already documents 7 dirty files destroyed, and the entry point I hit — the Agent tool's
isolation: "worktree"— is the one most likely to be running unattended work nobody is watching.Suggestions, in order of value
git worktree lock. The harness sets the lock itself; something in the same process ignores it. Whichever component removes these should refuse a locked tree, or state loudly why it is overriding.daemon.log. Right now a destroyed worktree is indistinguishable from a mystery, and the victim's error message points at the symptom (cwd does not exist), which sends everyone hunting their own tooling first. I audited hooks, scripts, and my own agents' commands before suspecting the harness.git worktree add+ passcwdto the agent) instead ofisolation: "worktree". Every explicitly-provisioned worktree in the same session survived; only the harness-managed ones died. Small sample, unknown mechanism — but the asymmetry was 100%.Happy to supply the full probe timelines or the victim transcripts if useful.
The worktree recycler bypassing
git worktree lockis the sharp edge here. The lock exists precisely so external actors can't remove a worktree that something considers in-use. When the recycler ignores it (and the corroborating report from szhygulin confirms this is reproducible on 2.1.209), the lock has no protective value in practice.The most robust fix is probably not git-lock-based at all -- it is a harness-level liveness registry: a lightweight file under
.claude/(or a named pipe / UNIX socket) where each session records its own session ID, PID, and worktree path at start, and clears the record on clean exit. Before recycling any worktree, the recycler checks: is any live session record pointing at this path? If yes, skip it. The git lock is then a second-layer defense, not the primary one.The
explicitly provisioned worktree survives, harness-managed one diesasymmetry you documented in the probes is useful triage signal. It suggests the recycler's reap heuristic keys on worktrees it created itself (tracked in some internal registry) rather than all worktrees under.claude/worktrees/. If the harness only recycles worktrees it originally registered, a worktree added withgit worktree addoutside the harness would be invisible to the recycler -- which matches the survival pattern you observed.A few local mitigations for anyone hitting this while waiting on a fix:
git worktree add .claude/worktrees/<name> -b <branch>before starting the session, then point the session at that path. Based on the probe results above, explicitly-provisioned worktrees are not targeted by the recycler.pre-sessionhook to lock the worktree the session is starting in:git worktree lock --reason "pid-$$ claude-session-$(date +%s)" "$CLAUDE_WORKTREE_PATH". Locks set inside the session's process tree are still getting bypassed, but it creates an auditable record.CLAUDE_SESSION_ACTIVEfile with PID and timestamp). This does nothing to stop the recycler today, but gives you post-mortem evidence: if the file is missing after a recycle, the recycler removed it rather than the session cleaning up.The "never fall back to the origin repository after repeated recycles" suggestion in the report is important. Landing an agent in the shared main checkout after it has lost its isolation is worse than a hard failure -- it converts a session isolation bug into a data integrity bug.
Same bug on macOS with 2.1.228. I have the transcripts from both sides of one reassignment: the session that lost its worktree and the session that was handed it. Slugs and paths are redacted as
<worktree-A>,<worktree-B>,<repo>; the quoted notices are otherwise verbatim.Setup
.claude/worktrees/, ~60 session workspaces on this project, several sessions running in parallel most daysWhat happened
.claude/worktrees/<worktree-A>. Its tree was clean and its branch was pushed. It sat idle for ~3 days between turns.<worktree-B>) had already been recycled while it sat idle.The two notices
Session B was told it got a "fresh" worktree. It was actually session A's:
Session A was told, after the fact:
Note: when a worktree is recycled, the receiving session is checked out to the same branch the original session had, but the losing session is moved to the repo root on
main. This is a second failure: you lose your worktree AND your branch context.How the victim was picked
Impact
.claude/worktrees/, which the pool never touches.Repro