Background/bridge sessions leak .claude/worktrees worktrees: no cleanup or stale-lock reaping on the daemon kill/settle path
Environment
- Claude Code 2.1.203–2.1.206 (observed across daemon self-upgrades), macOS (Darwin 25.5.0)
- Sessions involved: claude.ai bridge sessions (
bridgeSessionId: cse_*) hosted byclaude daemon, background jobs, and Agent-tool subagents withisolation: worktree
Summary
The only built-in cleanup for a worktree a session is parked in is the interactive session-exit keep/remove prompt. Non-interactive sessions (bridge sessions, background jobs, subagent orchestrators) can never reach that prompt. When such a session is closed from the job list, the daemon logs bg settled <id> (killed) and performs no worktree action whatsoever (the daemon log contains zero worktree events). The worktree directory, its branch, and — for EnterWorktree(name) worktrees — a git worktree lock naming a now-dead pid are all orphaned.
On one machine, roughly half of all worktrees ever created had leaked (12 leaked vs ~12 cleaned; the cleaned ones were removed only because the agent happened to run git worktree remove itself, not by any harness guarantee). One killed parallel-agent run leaked 9 subagent-isolation worktrees at once, all locked by the same dead orchestrator pid.
Reproduction
- Start a background/bridge session in a git repo; have it call
EnterWorktree({name: "x"})and do some work (merge or not — doesn't matter). - Close the session from the job list without the agent calling
ExitWorktree. - Observe:
daemon.logshowsbg settled <id> (killed); the worktree, branch, and.git/worktrees/x/locked(naming the dead worker pid) remain indefinitely.
Why the existing safety nets don't catch it
cleanupPeriodDayssweep skips worktrees with untracked files — but real worktrees almost always havenode_modules/,__pycache__/, or scratch files — and it cannot remove a locked worktree. In practice the sweep can never reap these leaks.- No stale-lock reaping: the lock names a pre-spawned spare/worker pid + start time. Nothing ever revalidates it after the process dies (docs explicitly leave kill/crash behavior undefined). A crash or kill converts the lock into a permanent one.
- Job-record destruction: closing a job purges its
state.json(which heldworktreePath), and bridge sessions never have a jobs record at all — so no metadata survives to drive retroactive cleanup. EnterWorktree(path)(enteredExisting: true) is contractually exempt from all auto-removal, so a session that creates a worktree with rawgit worktree add(e.g. becauseEnterWorktreefails when the session was launched from a non-repo cwd) opts out of lifecycle management entirely — even a graceful exit leaves it behind.
Suggested fixes (in root-cause order)
- On the daemon settle/kill path, when the dying session's cwd/state is inside a
.claude/worktrees/worktree: release the lock, and remove the worktree if its branch is merged and it has no meaningful uncommitted work (or at minimum unlock + record it for the next sweep). - Reap stale locks whose recorded pid+start-time no longer matches a live process — the daemon already has worker adopt/respawn machinery at upgrade takeover, which is a natural place for this.
- Let the
cleanupPeriodDayssweep reap merged worktrees whose only dirt is ignorable cruft and whose lock pid is dead. - When a background session is launched from a directory that is not a git repo, allow
EnterWorktree(name)to target a repo nested under the cwd instead of failing — the current failure pushes agents into the unmanaged raw-git worktree addpath that can never be auto-cleaned.
Evidence available on request
daemon.logexcerpts (bg settled … (killed)with no worktree events; spare-pool spawn lines matching lock pids to the second)- Session transcripts showing
EnterWorktreewith zeroExitWorktreecalls - Census: 12 leaked worktrees across 3 repos, incl. 9 from one killed parallel-agent run (single dead orchestrator pid on all locks)
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗