Stale worktrees are never cleaned up

Status Open
Maintainer reply None cached
Activity 9 comments · opened Feb 18, 2026

Problem

Claude Code creates git worktrees under ~/.claude-worktrees/ for parallel task execution but has no mechanism to clean them up if a session ends without proper cleanup (crash, interruption, or bug in teardown). There is also no garbage collection on subsequent session starts — each session is completely unaware of what previous sessions left behind.

This leaves behind:

  1. Full repo copies on disk (wasting space)
  2. Git branches that cannot be deleted even with git branch -D because git refuses to delete a branch checked out in any worktree
  3. Worktree refs in .git/worktrees/ that persist indefinitely

The only way a user discovers these is by stumbling into the cannot delete branch error, at which point they have to manually git worktree remove or rm -rf the directory, then git worktree prune to clean up refs.

Reproduction

  1. Use Claude Code on a project where it creates a worktree (parallel task execution)
  2. End the session (or have it crash/interrupt)
  3. Observe that ~/.claude-worktrees/<project>/<worktree-name> persists
  4. Try to delete the branch: git branch -D <worktree-name> → fails with error: cannot delete branch used by worktree

Expected behavior

  • Claude Code should clean up worktrees on session exit
  • On session start, Claude Code should scan for and prune stale worktrees from previous sessions
  • At minimum, a claude --cleanup command or similar should exist

Environment

  • macOS (Darwin 25.2.0)
  • Claude Code (CLI)
  • Found a stale worktree from January 15, 2026 that was never cleaned up across dozens of subsequent sessions

View original on GitHub ↗

8 Comments

karan-dhir · 6 months ago

Same issue here. Had 15 orphaned agent worktrees from a single session where sub-agents were launched with isolation: "worktree". All nested under a parent session worktree, none cleaned up on exit. Had to manually git worktree remove --force each one + delete the branches + git worktree prune.

+1 for either automatic cleanup on session exit or a claude --cleanup command.

asportnoy · 6 months ago

Same issue. Once or twice I _did_ get a prompt when exiting Claude for "Do you want to delete this worktree?" but for some reason it's not showing up the vast majority of the time. Not sure why.

Celtiore · 5 months ago

<img width="616" height="864" alt="Image" src="https://github.com/user-attachments/assets/3d1462e8-250c-43f4-a2df-062d5d83b781" />

Reproduction with Agent tool + isolation: "worktree"

Same issue here — using the Agent tool with isolation: "worktree" to spawn parallel Dev agents during a planning/execution workflow (VBW plugin). A single session created 7 nested worktrees under .claude/worktrees/, none cleaned up on agent completion.

What happened

  • Spawned 6 Dev agents (2 per phase, 3 phases) with isolation: "worktree" during one session
  • After each agent completed and its branch was merged back to main, the worktree + branch persisted
  • One agent even created a nested worktree inside another agent's worktree (.claude/worktrees/agent-a21cfb32/.claude/worktrees/agent-af7f3668)
  • Total: 12 orphan worktree-agent-* branches, 7 worktree directories with uncommitted changes

Screenshot (VS Code Source Control showing all orphan worktrees)

TODO: Screenshot to be added via drag-and-drop (GitHub API does not support image upload)

Manual cleanup required

# Remove all agent worktrees
for wt in $(git worktree list --porcelain | grep "^worktree " | grep ".claude/worktrees" | sed 's/^worktree //'); do
  git worktree remove --force "$wt"
done
git worktree prune
# Delete orphan branches
git branch | grep worktree-agent | xargs git branch -D
rm -rf .claude/worktrees/

Suggestion

Since sub-agents spawned via the Agent tool with isolation: "worktree" are ephemeral by design (their commits get merged back to the parent), cleanup should be automatic when the agent returns its result. At minimum, a SessionStart garbage collection pass would catch leftovers from crashed sessions.

Environment: macOS Darwin 25.3.0, Claude Code CLI, Claude Opus 4.6

ehagerty · 4 months ago

This comment was drafted by Claude Code on my behalf — the workflow and observations are mine; the phrasing is Claude's.

Adding a data point from a multi-session Claude Code CLI workflow (up to 5 concurrent sessions on one repo, one worktree per session via a fish-function wrapper around claude).

Three pathologies worth categorising separately when thinking about cleanup:

  1. Registered but missinggit worktree list names a path that no longer exists (directory rm -rf'd without git worktree prune).
  2. Unregistered directory.claude/worktrees/<name>/ exists on disk but git worktree list doesn't reference it (typical after a parent-repo relocation, or a create that failed halfway).
  3. Orphan claude/* branches — a claude/<name> branch exists with no worktree pointing at it (session exited uncleanly — crash, reboot, kill — before any cleanup could delete the branch).

Each needs different remediation, and conflating them into one `/worktree cleanup` command can be dangerous without guardrails.

Our ad-hoc bin/worktree-gc tool runs dry-run-by-default:

bin/worktree-gc # dry-run, report only
bin/worktree-gc --apply # safe fixes (prune + -d on merged branches)
bin/worktree-gc --apply-force # also -D unmerged orphan branches

Two design choices I'd suggest for whatever ships upstream:

  • Dry-run default: sweeping worktrees is destructive; users should see the plan before it executes.
  • Branch deletion with -d (safe) unless explicitly opted in: silently -D-ing an unmerged claude/* branch loses uncommitted exploratory work. Gate force-delete behind a flag.

Separately from the sweeper, we pair session-exit auto-cleanup (runs in the shell after claude terminates — not as a Claude Code hook, so it survives claude crashes but not hard reboots) with the sweeper as a catch-up for the reboot/kill cases. Roughly:

  • Session-exit cleanup handles the happy path.
  • Sweeper handles the rest (reboots, kills, ungraceful exits).

Not proposing this as a PR — it's scoped to our repo layout — but flagging the category decomposition in case the cleanup story eventually lands. Happy to share the script as a gist if it's useful.

SmartLamScott · 3 months ago

I'm still having trouble with this same issue.

dgokcin · 2 months ago
Same issue. Once or twice I _did_ get a prompt when exiting Claude for "Do you want to delete this worktree?" but for some reason it's not showing up the vast majority of the time. Not sure why.

see my comment here -> https://github.com/anthropics/claude-code/issues/46557#issuecomment-4742119691

it's happening because the sessions with worktrees have no automatic /name and my SessionStart hook does that if it;s a worktree.

shoffeepeng · 2 months ago

This issue still reproduces. I just published a deep-dive article on Claude Code's worktree isolation that documents this, along with the manual cleanup steps users are relying on in the meantime:

https://my.feishu.cn/docx/O1O0daR6SoSiKsxksiecFjk7nKc

The article also covers the known mitigation — a bash loop to force-remove all .claude/worktrees/ entries — which remains the only reliable cleanup path right now.

tmokmss · 1 month ago

Still reproducible on v2.1.205 (macOS), specifically for interactive-session worktrees under .claude/worktrees/ (not just subagent ones).

The concrete mechanism I hit: Claude locks each session worktree as
locked claude session <name> (pid <N>). On an abnormal exit (SIGKILL / crash / closing the terminal), the lock persists pointing at a now-dead PID, and nothing GCs it — cleanupStaleAgentWorktrees only targets subagent worktrees, and cleanupPeriodDays only governs chat-transcript retention.

Because the tree stays locked, a single --force is rejected:

$ git worktree remove --force .claude/worktrees/<name>
fatal: cannot remove a locked working tree, lock reason: claude session <name> (pid <N> ...)
use 'remove -f -f' to override or unlock first

So recovery requires git worktree remove --force --force. In repos with an initialized git submodule, there's a second barrier on top (working trees containing submodules cannot be moved or removed), which also needs the double --force.

Note this is adjacent to two other reports:

  • #51643 proposed exactly this (delete dead-PID worktree locks at session startup) but was closed as not planned for inactivity.
  • #70451 / the v2.1.187 changelog fixed the killed-agent locked-registration case — but the killed interactive-session case shown here is still unhandled.

A startup sweep that unlocks/removes worktrees whose lock PID is dead (as proposed in #51643) would close this for session worktrees too.

Showing cached comments. Read the full discussion on GitHub ↗