Stale worktrees are never cleaned up

Status Open
Maintainer reply None cached
Activity 14 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 ↗

14 Comments

karan-dhir · 7 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 · 7 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 · 7 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 · 5 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 missing — git 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 · 5 months ago

I'm still having trouble with this same issue.

dgokcin · 3 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.

liuminsheng3 · 3 months ago

The category split in @ehagerty's comment is the right safety boundary. I would make the cleanup command prove three separate states before deleting anything:

printf "== registered worktrees ==\n"
git worktree list --porcelain

printf "\n== registered but missing ==\n"
git worktree list --porcelain | awk '/^worktree /{print substr($0,10)}' | while read -r wt; do
  [ -e "$wt" ] || printf '%s\n' "$wt"
done

printf "\n== unregistered Claude worktree dirs ==\n"
find "$HOME/.claude-worktrees" .claude/worktrees -mindepth 1 -maxdepth 4 -type d 2>/dev/null | while read -r d; do
  git worktree list --porcelain | grep -Fqx "worktree $d" || printf '%s\n' "$d"
done

printf "\n== branches that look Claude-created ==\n"
git for-each-ref --format='%(refname:short) %(committerdate:iso8601)' refs/heads/claude refs/heads/worktree-agent 2>/dev/null || true

Then I would keep the action policy conservative:

  • git worktree prune is safe for registered-but-missing entries.
  • git worktree remove <path> is review-first if the worktree still exists and has uncommitted changes.
  • branch deletion should be git branch -d first, with -D only behind an explicit force flag and after showing the commits that would be lost.
  • nested worktrees need their own pass before deleting the parent directory, otherwise a broad rm -rf .claude/worktrees can hide what was active vs abandoned.

For a product-level fix, I would prefer two modes: session-exit cleanup for the happy path, plus a dry-run claude --worktree-gc that runs on session start and reports stale candidates with age, branch, dirty state, and last known session id before offering deletion.

I keep a short AI-agent storage cleanup checklist here, including stale worktrees, session logs, VM bundles, crash dumps, and cache buckets:
https://www.getsafedisks.com/ai-coding-agent-storage-enospc-mac?utm_source=github&utm_medium=issue-comment&utm_campaign=agent-storage-issue&utm_term=stale-worktrees-are-never-cleaned-up&utm_content=anthropics-claude-code-26725

If someone wants a private second pass on a redacted git worktree list --porcelain + top-level size table, the $29 cleanup order request is here. No remote access or file contents needed:
https://www.getsafedisks.com/checkout?product=pilot_audit&utm_source=github&utm_medium=issue-comment&utm_campaign=agent-storage-issue&utm_term=stale-worktrees-are-never-cleaned-up&utm_content=anthropics-claude-code-26725-private-review

shoffeepeng · 3 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.

liuminsheng3 · 3 months ago

One caution on the current "force-remove all .claude/worktrees/ entries" workaround: I would make that a last step, not the first step.

Before a loop removes anything, it should print enough evidence to tell whether a worktree contains unmerged or uncommitted user work:

git worktree list --porcelain

git worktree list --porcelain | awk '/^worktree /{print substr($0,10)}' | while read -r wt; do
  case "$wt" in
    *".claude/worktrees"*|*".claude-worktrees"*)
      echo "== $wt =="
      git -C "$wt" status --short
      branch="$(git -C "$wt" branch --show-current 2>/dev/null || true)"
      [ -n "$branch" ] && git -C "$wt" log --oneline --decorate -5 "main..$branch" 2>/dev/null || true
      ;;
  esac
done

Then the safe tiers are:

  • git worktree prune first, because it only cleans stale metadata for paths already gone.
  • git worktree remove <path> for clean worktrees.
  • git worktree remove --force <path> only after the status/log output shows there is nothing worth preserving.
  • rm -rf .claude/worktrees should be a final recovery path, because it can bypass git's ability to distinguish registered, nested, dirty, and already-missing worktrees.

This matters especially for the nested-worktree cases reported above: deleting the parent directory first can hide the child worktree state and make later branch cleanup more ambiguous.

tmokmss · 3 months 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.

mattgdrums-cloud · 2 months ago

Adding a data point on scale, and one detail that may widen the issue.

I cleaned up 13 abandoned worktrees holding 13.8 GB today on a single project — roughly 7× the size of the main checkout, accumulated over about 11 days. All 13 were registered in git worktree list and none had been touched by any cleanup mechanism.

Two things that may be useful:

1. Not all of them came from Claude Code. One was under .claude/worktrees/; the other twelve were created by a different agent CLI operating on the same repo, under /private/tmp. Since every worktree registers into the same .git/worktrees/, a session-start GC in Claude Code would surface and prune all of them regardless of origin. That makes the "scan on session start" half of the request more valuable than the "clean up on exit" half — exit cleanup only ever covers your own.

2. /private/tmp makes it worse, not better. macOS purges that directory on its own schedule, so those worktrees eventually vanish from disk while their refs persist in .git/worktrees/ — producing exactly the "branch cannot be deleted" state described here, with no directory left to point at.

A read-only claude --cleanup --dry-run that just listed stale worktrees with their branch and whether the directory still exists would have caught this on day one. The removal decision genuinely needs a human — one of my 13 had 69 staged renames that required per-path verification before it was safe to discard — but the detection is trivial and currently doesn't exist anywhere.

happytimes88 · 1 month ago

Data point + two default-behavior proposals from a ~4-month single-repo case study (governance-heavy project, heavy subagent isolation: worktree use, macOS).

Scale observed. On 2026-05-26 we cleaned up 113 of 114 worktrees under .claude/worktrees/, 3.0 GB, plus 112 stale claude/* branches and 20 stale gitlinks — accumulated over ~2 months at roughly 22–36 worktrees/month with zero session-start visibility. The backlog was found by a human noticing an anomaly, not by any tooling. Post-cleanup, with the newer auto-clean of unchanged agent worktrees, the residual today is 1 worktree / ~99 MB — so the auto-clean clearly helps, but see the second proposal.

What our own per-project mitigation converged on, offered here as candidate defaults rather than as a patch:

  1. Session-start backlog telemetry (one line, always on): N worktrees in .claude/worktrees/ (X GB). Run /worktree-sweep to review. The backlog compounds silently precisely because nothing surfaces it at the moment the operator is present. This is the cheapest possible cure for the root cause described in this issue and in #58914, and it is orthogonal to the GC policy itself. (Several comments above already converge on a session-start sweep; the telemetry line is the read-only half that can ship first.)
  1. An operator-declared "keep" sentinel that cleanup must honor. We classified the 113 into auto-delete-safe (89) vs. requires-audit (24 — large training data or unmerged work); 24 of 24 in the second class were load-bearing. The heuristics were 25% false-positive on "safe to delete" and 0% false-negative. Given that, the newer automatic cleanup is the right direction only if the operator can pin a worktree out of its reach — #88255 (live worktrees deleted) and #79899 (unpushed commits deleted as "no pending changes") are exactly the failure an untracked sentinel file at the worktree root (we use .LOAD_BEARING) would have prevented. Heuristic classification should recommend; the operator's explicit mark should win.

What we would not propose: auto-deletion by default without the sentinel escape hatch, or heuristic detection of "important" content — that is project-specific and is what the sentinel is for.

Full write-up (classification rule, thresholds, evidence tables) lives in our repo's governance docs; happy to link or paste sections if useful.

JaneX8 · 13 days ago

Same issue on Windows. I need to manually clean it up, no single worktree can be removed by Claude Desktop.

seanparkross · 1 day ago

We need more control of worktrees in the Claude Code app. On a new session either:

  • Create new worktree (default)
  • Re-use unused worktree

And option to auto cleanup on archive or delete of the session.