[FEATURE] Desktop: manage worktrees by lease and state, not PR-merge events (long sessions span many PRs)

Status Open
Reported on v2.1.296
Maintainer reply None cached
Activity 0 comments · opened Oct 11, 2026

Problem

The desktop app cleans up a session's worktree when its PR merges. That rule assumes one session = one branch = one PR. Long sessions don't work that way: one session often lands several PRs in a row, moves between a few worktrees, and runs subagents in nested worktrees. When the first PR merges, the worktree the session is still working in gets removed. The session's cwd disappears, Bash and hooks start failing, and in the worst case gitignored work in that tree is gone.

The opposite failure happens too: worktrees whose sessions are long dead, whose branches were squash-merged weeks ago, are never cleaned up, and disk fills up.

We ended up banning worktree removal from sessions entirely and writing our own reclaim script that only runs from the main checkout. Again, that is infrastructure the app should own.

Proposal: clean up based on the worktree's state, not on a PR event

1. Leases, not events. Each worktree has a lease held by every live session or subagent whose cwd is inside it, or that has touched it recently, renewed by heartbeat. A worktree with a live lease is never removed, whatever happened to its PRs. A PR merge is a signal to check, never a trigger to delete.

2. A merged PR resets the branch, not the tree. When a session's PR merges and the session is still alive, offer "start the next task here": fetch, create a fresh branch off the latest default branch in the same worktree, delete the merged branch. The session keeps its cwd, build caches and gitignored config (env files, local.properties, signing config) and continues with the next PR.

3. Removal requires every condition to be true:

  • no live lease, and the owning session has been archived or idle past a configurable grace period
  • working tree clean (no modified or untracked files)
  • every local branch head in it is merged into the default branch, including squash and rebase merges (check PR state or patch-id, not only git branch --merged), or pushed to the remote
  • no gitignored files beyond a known-regenerable list (build outputs, node_modules, caches); anything else blocks removal and is shown to the user

4. Preserve before removing. Before removal, save each head under a ref (for example refs/claude/reclaimed/<worktree>), never use --force on ignored data, and write a small receipt (what was removed, when, which heads were kept, how to restore).

5. One place to see and control it. A Worktrees view listing every worktree with its state: live (which session), idle, merged and reclaimable, dirty, unpushed, has unknown ignored files. Offer a dry run plus "reclaim all reclaimable", and run the same check automatically on a schedule instead of on PR events.

6. Hooks. A WorktreeReleased / WorktreeReclaimed hook so projects can copy out or clean up their own state.

Related

  • #88255, #94221 (auto cleanup deletes worktrees still in use)
  • #98186 (force-remove deleted gitignored data)
  • #92061 (squash-merged PR never triggers cleanup)
  • #26725 (stale worktrees never cleaned up)
  • #91472 (one worktree per session, removed only when the session is archived)
  • #92342 (hook after a session releases its worktree)
  • #95723 (stale worktreePath after removal)

Environment

  • Claude desktop app 2.31226.1, Claude Code 2.1.296
  • macOS 27.0.1

View original on GitHub ↗