[BUG] Desktop app WorktreePool maintenance sweep detached the MAIN repository's HEAD; worktree removal is non-atomic
Summary
The Claude desktop app's WorktreePool maintenance sweep (orphaned-worktree pruning) detached the main working copy's HEAD in one of my repositories while the machine was asleep (DarkWake). The repo sat on a detached HEAD for a week; 15 unattended automation commits landed on it before discovery. The same sweep also left 7 half-removed worktree directories behind (.git link deleted, directory contents still present), because removal is not atomic.
Two asks:
- Worktree housekeeping must never move the main working copy's HEAD. Whatever cleanup path runs in the repo root should be incapable of issuing a checkout/detach against the primary working tree.
- Worktree removal should be atomic — either the worktree is fully removed (git metadata + directory) or untouched. The current
git worktree remove --force→ "manual cleanup" fallback can delete the.gitlink and then stop, leaving directories thatgit statuscan no longer interpret.
Environment
- Claude desktop app 1.20186.1 (macOS), Claude Code CLI 2.1.187
- macOS 26.4.1 (build 25E253), git 2.53.0
- Incident date: 2026-07-07 06:15 EDT (machine in DarkWake, 13% battery, lid closed — no interactive session)
Evidence
1. The main repo's reflog shows a bare detach at the exact moment of the sweep (~/reasons_brain, the primary working copy — not a worktree):
46b1c30 HEAD@{2026-07-07 06:15:13 -0400}: checkout: moving from main to HEAD
checkout: moving from main to HEAD is the reflog signature of git checkout --detach (verified separately: a plain git checkout HEAD is a no-op and writes no reflog entry). No prior entry in this repo's history looks like this; every other checkout names a real ref.
2. The desktop app's own log shows the WorktreePool sweep running dozens of git commands with cwd = the main repo root at the same second (~/Library/Logs/Claude/main.log, lines ~52476–52810):
2026-07-07 06:15:11 [warn] [event-loop-stall] main process blocked for 17333ms [likely sleep/wake] ...
2026-07-07 06:15:11 [info] [WorktreePool] Pruning orphaned store entry reverent-wozniak-9e3cbb (directory gone)
2026-07-07 06:15:11 [info] Removing worktree "reverent-wozniak-9e3cbb" (leased by local_bb654bad-...): /Users/ryanwalsh/reasons_brain/.claude/worktrees/reverent-wozniak-9e3cbb
...
2026-07-07 06:15:11 [error] Git command failed: git -c core.longpaths=true worktree remove --force /Users/ryanwalsh/reasons_brain/.claude/worktrees/nostalgic-hypatia-4a2278 { code: undefined, stderr: "fatal: '...' is not a working tree\n", cwd: '/Users/ryanwalsh/reasons_brain' }
2026-07-07 06:15:11 [warn] Git worktree remove failed, will try manual cleanup: fatal: '...' is not a working tree
2026-07-07 06:15:11 [error] Refusing to delete worktree path outside managed directory (possible tampering): ...
In the 06:15:11–06:15:14 window the sweep issued 51 git worktree remove --force commands across 5 repos (reasons_brain, reasons-os, reasonable-ux, reasonable-days, …), with 101 "is not a working tree" failures and 50 "manual cleanup" fallbacks — all with cwd set to the respective main repo root. The reflog detach in (1) is timestamped 06:15:13, inside this window.
3. Nothing else ran a detach in that window. Checked: no cron job, no launchd task, no shell history, no Claude session, and no script in my automation inventory issues git checkout --detach (my automation only ever commits on the current branch). The machine was in DarkWake on battery — no interactive use.
Impact
- 15 unattended automation commits (nightly/morning scheduled jobs, 2026-07-07 23:30 → 2026-07-13 23:58) landed on the detached HEAD instead of
mainbefore I discovered it on 2026-07-14 and recovered with a checkout back tomain. Anyone without reflog fluency would plausibly lose that week of commits. - 7 half-removed worktree directories left under
<repo>/.claude/worktrees/—git statusinside them fails with.git link missing (partial remove);git worktree listno longer knows about them, so they're invisible to normal tooling but still hold files.
Reproduction context
Likely preconditions, in case they matter for reproducing:
- A large number of orphaned WorktreePool store entries ("directory gone") had accumulated across several repos — the sweep pruned dozens in one pass.
- The app was running through system sleep/wake; the sweep fired immediately after a 17s event-loop stall flagged
[likely sleep/wake], so many removals ran back-to-back, most hitting already-deleted paths. - The detach happened in the repo with the most orphaned entries. I don't know which specific sweep step issues the detach (nothing in main.log logs a checkout), but the correlation is exact to the second and repeated
cwd: '<main repo root>'git invocations are in the log either side of it.
Happy to provide fuller log excerpts or the complete reflog privately if useful.