Exit-time worktree cleanup deletes worktree + branch holding unpushed commits as "no pending changes" (after earlier ExitWorktree + compaction in the same session)
Bug Description
Session-exit worktree cleanup printed Cleaning up worktree (no pending changes)… and silently removed a worktree holding 5 unpushed commits, deleting its branch with it. No keep/remove prompt appeared. Per the worktrees docs, new commits count as "work that removal would delete" and must trigger the prompt.
The failing session had previously entered and removed a different worktree via ExitWorktree, and went through one auto-compaction — the exit cleanup then apparently evaluated the "pending changes" check against stale state, so the second worktree's commits were not counted. Likely related to #48751 (originalHeadCommit / currentSessionWorktree state lost after long sessions/compaction), but with the opposite (and worse) outcome: instead of leaving the worktree behind, cleanup deleted it.
Environment
- Claude Code v2.1.216, Linux
- Interactive session, unnamed, started in the main checkout
Timeline (from the session transcript)
| Time (UTC) | Event |
|---|---|
| 10:31 | EnterWorktree → worktree A (relay-multi-tenant) |
| 12:06 | ExitWorktree {action: remove} then {action: remove, discard_changes: true} — worktree A removed (its feature was squash-merged; removal intended and correct) |
| 12:16 | auto-compaction (compact_boundary in transcript) |
| 14:57 | EnterWorktree → worktree B (relay-self-service) |
| 15:11–19:10 | 5 commits on worktree B's branch (never pushed) |
| ~19:10 | session exit → Cleaning up worktree (no pending changes)… — worktree B and its branch deleted, no prompt |
Expected behavior
Worktree B has 5 new commits ⇒ exit must prompt keep/remove ("The worktree has work in it: Claude prompts you to keep or remove the worktree", https://code.claude.com/docs/en/worktrees#clean-up-worktrees). At minimum, "no pending changes" must never be reported for a worktree whose branch carries unpushed commits.
Actual behavior
Cleanup classified worktree B as clean and auto-removed it (unnamed session ⇒ no prompt), destroying the only ref to 5 commits.
Impact / recovery
The commits survive as unreachable objects in the shared object store. Recovery that worked:
git fsck --unreachable --no-reflogs | grep commit # find tips
git log -1 --format="%h %cd %s" <sha> # identify
git branch <name> <tip-sha> # re-anchor before gc
Users who don't know this will assume the work is gone — and after a git gc prune it genuinely is.
Prior occurrence (same repo, 10 days earlier)
On 2026-07-11 the same failure class hit a different worktree in this repository: .claude/worktrees/pickup-task-tracking and its branch worktree-pickup-task-tracking disappeared while the branch held unpushed commits (a design spec + ADR). The removal itself wasn't directly observed (it happened outside the session that discovered it — possibly the periodic sweep or another session's exit cleanup; the docs state the sweep "skips a worktree that still holds work: … unpushed commits"). The session that noticed it hit fatal: cannot change to '….claude/worktrees/pickup-task-tracking': No such file or directory, found the commits via git fsck --lost-found (e.g. 1ffb0133…, ab8a8595…, both still present as dangling commit objects today), and re-anchored the branch. The recovery is permanently recorded in that feature's PR history: a commit titled "Recovered branch from dangling commits after worktree prune; rebased onto …". So this report is the second data point of automatic worktree cleanup destroying the only ref to unpushed commits.
Suspected cause
The exit-time "pending changes" check appears to use per-session worktree state (base/original HEAD) recorded for the first EnterWorktree of the session, or state invalidated by the intervening ExitWorktree/compaction. When the session later enters a second worktree, the new-commits comparison silently passes. Repro should therefore be: EnterWorktree A → ExitWorktree(remove) → (compaction) → EnterWorktree B → commit → /exit.