Desktop app's worktree pool grants an already-active worktree to a second, concurrent session — registry createdAt proves duplicate allocation, not a reclaim race (distinct from #75911)
Desktop app's worktree pool grants an already-active worktree to a second, concurrent session — registry createdAt proves duplicate allocation, not a reclaim race (distinct from #75911)
Summary
The desktop app's worktree pool (git-worktrees.json, the same registry #75911 documents) assigned a worktree directory to a second, unrelated Claude Code session while a first session was still actively using it — not after the first session finished or went idle, but 14 minutes into a live, ongoing session. The registry's own createdAt timestamp for this slot is stamped essentially the same second the second session's transcript begins, proving the pool's bookkeeping asserted a brand-new lease over a directory that had never been released.
This is filed separately from #75911 because the mechanism is different. #75911 is about reclaiming an existing, previously-registered lease based on a mistimed signal (a worktree gets detached/handed off while its original session is still working, or shortly after). Every incident there involves one worktree changing hands. What happened here is two live sessions holding the same lease at once — the pool's allocation step handed out a path that was already actively in use, and stamped it as freshly created rather than detecting the conflict.
Environment
- Claude Code CLI 2.1.209 (desktop-app-bundled engine)
- Desktop app (Claude.app) 1.20186.9, macOS 15.7.7 (Darwin 24.6.0, arm64)
- Affected repo: a private repo using desktop-app background sessions with worktree isolation (
.claude/worktrees/<name>)
The mechanism, evidenced by the registry itself
Registry entry for the contested worktree (paths/branch names redacted, same convention as #75911):
"<worktree-name>": {
"name": "<worktree-name>",
"path": "/…/<repo>/.claude/worktrees/<worktree-name>",
"leasedBy": null,
"baseRepo": "/…/<repo>",
"branch": "claude/<branch-B>",
"sourceBranch": "main",
"createdAt": 1784064832769,
"pooledAt": 1784068529228
}
Converted:
createdAt: 2026-07-14 21:33:52.769 UTC (17:33:52 EDT)
pooledAt: 2026-07-14 22:35:29.228 UTC (18:35:29 EDT)
- Session A (this worktree's originally-assigned branch,
claude/<branch-A>): first transcript message 21:19:00 UTC. - Session B (the branch now recorded in the registry,
claude/<branch-B>): first transcript message 21:33:53 UTC — one second after the registry'screatedAtfor this exact slot.
Session A had been actively working in this directory — reading files, running tests, issuing tool calls — for 14 minutes before the pool's own bookkeeping claims the worktree was "created." This isn't a race on reclaiming something idle; the registry itself asserts a fresh allocation landed on top of a live one.
Evidence
- The registry snapshot above.
- Independent corroboration from our own session-history log (unrelated to the bug, kept for other reasons): Session A's record begins 21:19:00 UTC in this path; Session B's begins 21:33:53 UTC in the identical path.
- From inside Session A: mid-session,
git status/git branchshowed the worktree had switched fromclaude/<branch-A>toclaude/<branch-B>with no checkout ever issued by Session A. Watching the directory over the following minutes showed real, ongoing writes to multiple source files, one at a time, roughly every 1–2 minutes — Session B's actual, legitimate work landing in real time in a directory Session A believed was exclusively its own. - From inside Session B, independently: it found unexplained test-file additions and an untracked instructions file it hadn't written, and — before being corrected — concluded (in writing, to its own user) that its own spawned review subagent had "overreached its scope" and authored them without permission. This was wrong: those files were Session A's work. It was only corrected because the human operator directly challenged the inference ("are these really from your subagent, or a parallel session that got reaped?"), forcing Session B to reconstruct a file-mtime timeline proving its own subagents had already finished 12+ minutes before the files in question were written.
- Both sessions eventually committed to their own distinctly-named branches from within the same shared directory, pushed successfully, and were later merged to
mainwith no data loss — but only after three separate rounds of manual, cross-session forensic reconciliation by the shared human operator.
Why this evaded detection
- Same root problem as #75911: pool activity produces no tool call visible to either session.
- Worse here: because both sessions were live and doing real, legitimate work, the symptoms didn't read as "unexplained corruption" — they read as plausible, in-scope activity from one's own tooling. Session B's misattribution to its own subagent shows how convincingly this masquerades as a mundane internal explanation rather than an external cause, which makes it strictly harder to catch than a bare detached-HEAD symptom would be.
- Neither session had any signal, lock, or lease-conflict warning indicating another session held the same path.
Suggested fix directions
- Before creating a worktree entry, the pool must check the target path against real git state (
git worktree list) and/or an existing, unexpiredleasedByvalue — not assume a path is free just because a new lease request arrived. createdAtshould mean what it claims. If a path already has live lease state, a competing allocation request should fail loudly (at minimum to the app; ideally surfaced to the session) instead of silently overwriting the registry entry.- Surfacing pool lifecycle events (as #75911 already requests) would help here too, but the more fundamental fix is a uniqueness/liveness check at allocation time — better logging of reclaim events doesn't address a duplicate-allocation bug.
Related issues (not exact duplicates)
- #75911 — same registry/subsystem; different mechanism (reclaim of an existing lease vs. duplicate allocation of a live one).
- #71433 (feature request) — closest conceptual match: cross-session collision on a contested worktree path. Scoped to explicit
EnterWorktreecalls where two sessions independently derive the same name; this report is about the desktop app's own automatic background allocation, with registry-timestamp evidence pinning the exact allocation-time defect. - #77506 — confirms
WorktreePoolas the actual subsystem name; different defect (maintenance sweep detaching the main repo's HEAD during unattended sleep, non-atomic removal). - #74386 — same "no liveness signal" root-cause shape, but for an explicit cleanup action, not automatic silent allocation.
- #76144 — same pool, different corruption (
gitdirwritten as literal.git) feeding its own reclaim/delete path. - #77268 — recycling destroys live sibling worktrees; same family, destructive rather than silent-swap.
- #60896 (closed, stale-bot, not resolved on merits) — same broad bleed-through symptom (HEAD/commits jumping between sessions), different topology (root checkout + one worktree, not worktree + worktree).
- #76727 — broader measurement/research report on cross-session coordination gaps in the same problem space; no specific defect or evidence.
Can provide on request
- Full (unredacted)
git-worktrees.jsonsnapshot and reflogs for both worktrees involved - Session transcript excerpts showing each session's own forensic investigation and the misattribution/correction sequence
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗