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)

Resolved 💬 2 comments Opened Jul 14, 2026 by blwfish Closed Jul 15, 2026

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's createdAt for 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

  1. The registry snapshot above.
  2. 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.
  3. From inside Session A: mid-session, git status/git branch showed the worktree had switched from claude/<branch-A> to claude/<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.
  4. 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.
  5. Both sessions eventually committed to their own distinctly-named branches from within the same shared directory, pushed successfully, and were later merged to main with 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

  1. Before creating a worktree entry, the pool must check the target path against real git state (git worktree list) and/or an existing, unexpired leasedBy value — not assume a path is free just because a new lease request arrived.
  2. createdAt should 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.
  3. 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 EnterWorktree calls 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 WorktreePool as 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 (gitdir written 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.json snapshot and reflogs for both worktrees involved
  • Session transcript excerpts showing each session's own forensic investigation and the misattribution/correction sequence

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗