[BUG] Desktop app WorktreePool maintenance sweep detached the MAIN repository's HEAD; worktree removal is non-atomic

Resolved 💬 0 comments Opened Jul 14, 2026 by ReasonEquals Closed Jul 16, 2026

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:

  1. 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.
  2. 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 .git link and then stop, leaving directories that git status can 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 main before I discovered it on 2026-07-14 and recovered with a checkout back to main. Anyone without reflog fluency would plausibly lose that week of commits.
  • 7 half-removed worktree directories left under <repo>/.claude/worktrees/git status inside them fails with .git link missing (partial remove); git worktree list no 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.

View original on GitHub ↗