Desktop app's worktree pool reclaims/re-leases a directory while a session is still using it, detaching HEAD mid-task (and once, on the main clone itself)
Desktop app's worktree pool reclaims/re-leases a directory while a session is still using it, detaching HEAD mid-task (and once, on the main clone itself)
Summary
The Claude desktop app maintains a background pool of reusable git worktree directories, tracked in an internal registry file (git-worktrees.json under the app's Application Support directory, schemaVersion: 2). Each entry has createdAt, pooledAt (when the worktree is returned to the pool for reuse), and leasedBy (which session currently holds it).
Across one ~32-hour window (2026-07-07 19:50 through 2026-07-08 20:32, all times EDT) we captured three separate incidents in one repo where this pool mechanism corrupted a session's git state — in two cases via an unexplained bare git checkout HEAD that detached the worktree while the app's own registry shows the pool reclaiming or re-leasing it, and in one case by silently swapping which branch a pooled directory pointed to between two unrelated sessions. In the worst instance, after a session successfully merged and pushed to main, the main clone's own HEAD detached with the identical fingerprint, unexplained by any tool call the session itself made.
None of this caused permanent data loss — each incident was caught after the fact by the session itself (or a later session) doing a manual forensic check, not by the harness surfacing anything. Nothing about the pool's reclaim/re-lease activity is visible to the session as a tool call, so from inside a session it looks like unexplained git corruption.
Environment
- Claude Code CLI 2.1.202 (desktop-app-bundled engine), macOS (Darwin 24.6.0, arm64)
- Desktop app (Claude.app) 1.19367.0
- Affected repo: a private git repo using desktop-app background sessions (
claude agents) with worktree isolation (.claude/worktrees/<name>)
The mechanism (root cause, as best we can reconstruct from evidence)
The desktop app's git-worktrees.json registry entries look like:
"magical-hofstadter-2816a8": {
"name": "magical-hofstadter-2816a8",
"path": "/…/mneme/.claude/worktrees/magical-hofstadter-2816a8",
"leasedBy": null,
"baseRepo": "/…/mneme",
"branch": "claude/python-app-dock-icon-812914",
"sourceBranch": "main",
"createdAt": 1783541449358,
"pooledAt": 1783542118706
}
createdAt and pooledAt are epoch-ms. Converted:
createdAt: 1783541449358 → 2026-07-08 16:10:49 EDT
pooledAt: 1783542118706 → 2026-07-08 16:21:58 EDT
These two timestamps land exactly on two lines from that session's own git reflog:
16:10:49 checkout: moving from main to claude/python-app-dock-icon-812914 ← matches createdAt
16:21:58 checkout: moving from claude/python-app-dock-icon-812914 to HEAD ← matches pooledAt
checkout: moving from <branch> to HEAD is not something a user or a normal git workflow produces — it's the fingerprint of a bare git checkout HEAD, which detaches HEAD in place. The timing match says the pool's reclaim step (whatever runs at pooledAt) is what performs this detach — and it did so 11 minutes into a session that was still actively working (the session's own commit didn't land until 16:32:35, after the detach). The session had no visibility into this; there is no corresponding tool call in its own transcript. It discovered the problem only because post-commit git status output looked wrong, then manually verified the commit was a clean fast-forward descendant and reattached the branch.
We believe this is a premature-reclaim race: the pool manager decides a worktree is free and detaches/reclaims it based on some signal other than "the session currently leasing it is done," and can fire while the session is mid-task.
Second incident: a worktree that was already broken before the session even touched it
A different registry entry:
"cool-mclean-8e90bf": {
"path": "/…/mneme/.claude/worktrees/cool-mclean-8e90bf",
"leasedBy": "local_e5ff258c-6828-4d96-ab57-ea8b9a1ee8de",
"baseRepo": "/…/mneme",
"branch": "claude/practical-booth-4ac915",
"createdAt": 1783468202717 → 2026-07-07 19:50:02 EDT
}
A session starting at 2026-07-07 23:51 (about 4 hours after this pool slot's createdAt) checked this worktree at 23:52 and found it already HEAD detached at 1ca6a76 — before the session had done anything. Four sequential commits (23:58–00:29) all landed on that pre-existing detached HEAD. The session didn't notice until 04:54:xx, then manually ran git branch -f claude/practical-booth-4ac915 <tip> to reattach.
This looks like the counterpart bug to the first: when the pool re-leases a previously-reclaimed (and therefore detached) worktree directory to a new branch/task, the checkout to the new branch doesn't reliably happen — the new lease inherits the prior lease's detached state.
Third incident: main clone's own HEAD detached after a successful push
The same session (started 23:51, above) merged its branch and pushed to main successfully at 05:12:08 (git push origin main → 8a3b891..513f5e4 main -> main). It then did cleanup (git worktree remove, git branch -d) on other worktrees — not on main. At 05:51:26 it discovered its own main clone now reported HEAD detached at 513f5e4, with reflog:
513f5e4 HEAD@{0}: checkout: moving from main to HEAD
513f5e4 HEAD@{1}: commit (merge): Merge branch 'claude/practical-booth-4ac915' into main
Same bare-HEAD fingerprint, on the primary checkout this time, with no git checkout anywhere in the session's own tool-call history between the push and the discovery. Self-repaired with git checkout main.
We can't yet explain why the pool mechanism would touch the main clone at all — worth noting given the first incident's worktree (magical-hofstadter-2816a8) was, per separate forensics, never actually registered as a real git worktree on disk at all (no .git, git rev-parse --show-toplevel from inside it resolved to the main clone) despite being createdAt/pooledAt-tracked in the app's own registry. If the pool's reclaim logic trusts its own JSON bookkeeping rather than querying real git state before acting, and that bookkeeping can diverge from reality (as it demonstrably did here), it's plausible the same reclaim codepath can end up operating against whatever git rev-parse --show-toplevel resolves to for a broken/unregistered entry — which, for an orphaned worktree, is the main clone.
Fourth incident: a pooled worktree silently changed branch identity between two unrelated sessions
A fourth worktree slot (branch claude/update-13f9cf at creation) was confirmed correctly on that branch by one session at 00:05:27. A second, unrelated session touching the same path at 05:09:14 found it on a completely different branch (claude/spec-md-libreoffice-conversion-32b18f) — no checkout call from either session explains the change. A later git worktree list (05:46:47, from the third session above) found this same path had gone to 8a3b891 (detached HEAD). No data was at risk (the second session made no edits there), but it's the same underlying pattern: the pool reassigned this directory's branch identity mid-flight, outside of any visible session action.
Why this evaded detection until now
- Pool reclaim/re-lease activity produces no tool call visible to the session — from inside a session, this looks like unexplained git corruption, not a documented lifecycle event.
git-worktrees.jsonis an internal, undocumented artifact — nothing in the desktop app UI or CLI docs describes worktree pooling as a concept, so there's no way for a user or an agent session to know this mechanism exists, let alone that it's racing them.- We only found the mechanism by correlating this registry file's
createdAt/pooledAttimestamps against reflog timestamps after already suspecting a race from independent forensic evidence (see repro below).
Suggested fix directions
- The pool reclaim step must not detach/reclaim a worktree while a session still holds an active lease on it — gate reclaim strictly on lease release, not on an independent timer/heuristic.
- Re-leasing a pooled worktree to a new branch must verify the checkout actually succeeded (and that HEAD is attached to the intended branch) before handing it to a new session — currently a failed/incomplete re-checkout silently hands off a broken worktree.
- Whatever performs the reclaim-time detach should verify against real git state (
git worktree list,git rev-parse --show-toplevel) rather than trusting the registry file alone, so a registry entry that was never actually backed by a real worktree can't cause the reclaim logic to act on the wrong checkout (up to and including the main clone). - Consider surfacing pool lifecycle events (
reclaimed,re-leased,detached-for-reuse) as something visible in session transcripts or logs, so a future incident like this is diagnosable from the session side instead of requiring cross-referencing an undocumented internal file.
Related issues (not exact duplicates)
- #75490 — desktop app's worktree mechanism detached the main tree's HEAD unexpectedly (same broad symptom:
checkout: moving from main to <hash>, no user action, worktree-mechanism activity bracketing it in time) and additionally deleted gitignored directories. Same family of "worktree lifecycle mechanism silently mutates a checkout out from under the user," but no pool/lease registry angle and a different (more destructive) blast radius. - #36182, #70456, #66442, #69026, #74071 — an older, separate cluster about tool calls landing in the wrong checkout when worktree isolation silently falls back to the parent clone. Related in spirit (isolation guarantees silently not holding) but a different mechanism — those are about where edits land, not about a background pool detaching/re-leasing a worktree already in active use.
Can provide on request
- Full reflog excerpts and
git-worktrees.jsonsnapshots for all four incidents - Session transcript excerpts showing the absence of any corresponding tool call at each detach point
5 Comments
Diligence check: before/after filing, reviewed the other ~35 worktree/git-related issues filed in the same 2026-07-07/07-08 window for overlap with the pool-reclaim mechanism described above (the
git-worktrees.jsonpooledAt/leasedByregistry, and the barecheckout ... to HEADfingerprint appearing while a worktree is still in active use). None describe the same mechanism, so no duplicate/merge action needed there.Two are worth cross-linking as related-but-distinct — same general subsystem, different concrete defect:
rm -rftraverses NTFS junctions and deletes data outside the worktree. Same family (the harness's own background worktree housekeeping acting invisibly), but the defect is junction traversal during deletion of an already-dormant worktree, not a checkout/HEAD-detach race against a worktree still being actively used by a live session.Recurrence, six CLI versions after filing. Filed on 2.1.202; today's occurrence was on 2.1.209. Same fingerprint as the original report — a bare
checkout ... to HEADdetach, plus a mid-session swap to a completely unrelated branch before detaching again — in a different repo (redacted, same as the original report).Reflog for the affected worktree, this occurrence:
claude/<unrelated-branch-B>belongs to a different, unrelated task — not something checked out by any command in this session's own tool history. The worktree sat detached from ~12:35 onward; nothing in the harness surfaced this before a session began working in it hours later and discovered it via unexpectedgit statusoutput (same detection path as the original report — no proactive signal from the harness itself).No data was lost this time — caught before any commits landed on the detached state, and resolved by moving/resetting the branch to the current commit and re-checking it out. Adding this mainly as a version/recurrence data point, since the mechanism (pool reclaim/re-lease touching a worktree's branch attachment without the owning session's involvement) still appears to be live well past the fixes that landed in this general area recently.
Second recurrence report, ~1 hour after the first (still CLI 2.1.209). This one is sharper evidence than either prior report: two different worktrees in the same repo detached 10 seconds apart, and a commit landed on the detached HEAD in between — i.e., this isn't just "a worktree occasionally ends up detached," it looks like a sweep process touching multiple worktrees in one pass, at least one of which was actively in use.
Reflogs (repo/branch names redacted, same convention as the original report):
Both detaches carry the same bare
checkout ... to HEADfingerprint as the original report and my earlier comment. The 10-second gap between two unrelated worktrees in the same repo is the new piece of evidence — it doesn't look like two independent races, it looks like one sweep visiting both.Worktree A's case is also the clearest demonstration yet of the actual risk this issue describes, not just the annoyance: the 16:07:12 amend commit landed after the detach and before the manual reattach — i.e., on the detached HEAD, at risk of being orphaned, exactly the failure mode in the original report's incident #1. It was caught and reattached manually this time (rebase surfaced "detached HEAD" during an unrelated operation), same as every prior occurrence — still no proactive signal from the harness itself.
Also for the record: a third worktree, in a different repo, detached separately today (~12:05–12:35, several hours before the two above) with the same fingerprint — noted in my previous comment. Three worktrees across two repos, all on CLI 2.1.209, in one calendar day.
Third recurrence today (still CLI 2.1.209), and the cleanest
createdAt/pooledAtcorrelation yet — both ends of the lease match the reflog to the second, not just the detach.Same repo family as my two comments above, different worktree, different task. Reflog for the affected worktree:
The app's own registry entry for this worktree:
createdAtmatches the clean initial handoff exactly.pooledAt— the timestamp your own schema documents as "when the worktree is returned to the pool for reuse" — matches the silent detach exactly, to the second, same as the correlation in the original report's Incident #1. The difference here: this session was demonstrably still active at that moment (running review subagents, no commits yet — its first commit doesn't land until 32 minutes later) and nobody else ever claimed the worktree afterward. The session just kept working, unaware, until its own commit landed on the orphaned detached HEAD and it caught the mismatch by inspectinggit status.So this is about as clean a confirmation as I've been able to produce of the original premature-reclaim hypothesis: the pool decided this actively-leased, actively-used worktree was free 20 minutes into the session, stamped it as pooled, and detached it — with no second claimant, no session tool call, and no proactive signal to the session that anything had happened. Detection path is identical to every prior report: caught after the fact via unexpected git state, not surfaced by the harness.
One cross-reference for anyone reading both: today's forensics also turned up a related-but-mechanically-different bug — the pool assigning a still-actively-leased worktree to a second, concurrent session (registry
createdAtfor a slot postdating a different session's confirmed start in that same slot) — filed separately as #77609 rather than folded in here, since that one isn't a reclaim-timing race at all: it's a duplicate allocation of a lease that was never released. This comment's incident, by contrast, is a clean instance of the mechanism already described above.Periodic verification — August 2026 ⚠️ Possible partial fix — reporter action needed
Checked against Claude Code v2.1.220 (latest release as of 2026-08-01).
Release notes for v2.1.216 (2026-07-20): "Fixed worktree sessions landing in another project's leftover worktree when the working directory did not match the selected project"
This is adjacent to what is reported here — a session receiving a worktree it should not have — but the note describes a cross-project working-directory mismatch, not the pool reclaiming and re-leasing a directory while a session still holds it. The
createdAt/pooledAtcorrelation documented above points at lease bookkeeping, which this note does not obviously cover. Flagged rather than assumed.Related worktree-lifecycle fixes also landed in v2.1.210–v2.1.212 (lock sweeping for killed sessions,
ExitWorktreeafter--resume), none of which address lease reclamation either.Reproduction test: ℹ️ Not testable on demand — this is a concurrency race; it surfaced three times in one day under load and cannot be forced.
Reporter: please watch for recurrence on v2.1.220 and close if the reclaim race is gone.
This comment is posted automatically every ~10-20 days to prevent stale-bot closure.