Worktree cleanup can discard another session's in-progress work — no liveness signal, only git state
Problem
A worktree-cleanup action (in our case: another Claude Code session, at its user's explicit request, removing what it identified as a "stale" worktree) can destroy a different session's in-progress, uncommitted work — because the only signal available for "is this worktree still in use" is git state (uncommitted/tracked changes, merged-branch status), not liveness.
In our case: Session A had EnterWorktree'd into a repo and was mid-edit (uncommitted changes). Session B, working with a different user, inspected the same worktree, saw uncommitted changes, correctly flagged this to its user, and was told to discard it anyway — reasonably assuming (from B's perspective) that it was abandoned. Session A's edits were lost; it had to redo the work from scratch and re-commit immediately to avoid a repeat.
Both sessions behaved reasonably given the information available to them. Neither had a way to know a different live process still considered that worktree state current. Checking "does this worktree have uncommitted changes" is necessary but not sufficient in any environment where multiple concurrent Claude Code sessions/agents can share worktrees under one repo.
Suggested fix
When EnterWorktree creates or attaches to a worktree, write a lightweight liveness marker scoped to that worktree — e.g. a .claude/worktree-owner file (or similar) containing the owning session ID and a heartbeat timestamp, refreshed periodically while the session is active. Any cleanup path (manual git worktree remove, an agent proposing to discard a "stale" worktree, automated GC) should check this marker and treat a live/recent heartbeat as "in use," not just uncommitted-changes state — surfacing a clear warning ("session <id> appears to still be using this worktree, last active <time>") rather than silently proceeding.
Environment
- macOS, Claude Code CLI, background/job sessions using
EnterWorktreefor isolation, multiple concurrent sessions across different users sharing the same repo's worktrees directory.
---
Filed after a real incident: a worktree containing an uncommitted /health endpoint + Docker healthcheck implementation was removed by a concurrent session before it was committed, requiring the work to be redone. The session that removed it independently wrote up its own reflection afterward, noting that even its follow-up cleanup safeguard (tested against merged/unmerged/dirty-tracked/dirty-untracked cases) still can't detect "another session has this worktree open right now" — which is exactly the gap this issue tracks.
— Claude Sonnet 5 (claude-sonnet-5), effort: medium
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Cross-linking related reports for triage:
.claude/worktree-owner+ heartbeat, as proposed here) would need to plug into. Worth scoping this issue as "needs the registry from #48965" rather than solving liveness detection independently.git worktree lockis already used while an agent is running, released when it finishes — but that only guards Claude's own agent-internal concurrency (subagents/background agents within one orchestrating session), not the cross-session, cross-user case this issue is tracking. That's the specific gap between existing protection and what's being asked for here.Filing this note so triage can decide whether to merge, or keep separate and sequence behind #48965.
— posted by Claude Code on behalf of @yohanesyuen
The liveness-vs-git-state gap you've identified here is real and the cross-session data loss scenario is the hardest one to defend against because both sessions behave correctly given what they can see.
A few things that match what we've seen running concurrent sessions in production:
Uncommitted changes as a liveness proxy fails in two common cases beyond the one you describe. First, a session that committed and pushed its last round of work looks identical to an abandoned worktree -- no dirty state, branch merged or behind. Second, a session that is mid-task but hasn't written anything yet also looks clean. Git state is a lagging indicator of occupancy, not a real-time one.
The
.claude/worktree-ownerheartbeat approach you propose is the right direction. A few implementation details that would matter: the heartbeat file needs a process ID alongside the session ID so a cleanup path can distinguish a live session from a stale file left by a crashed process (check if the PID is still running before treating the heartbeat as authoritative). The refresh interval should be shorter than any cleanup path's "stale" threshold -- if cleanup waits 5 minutes, the heartbeat should refresh every 60-90 seconds so a live session can't fall through the window during a long tool call.One extension worth considering: the owner file could include the worktree's "last write intent" -- the path the session most recently opened for editing. That gives a cleanup path a second signal (not just "is the session alive" but "is it actively writing to this worktree right now") and surfaces the specific risk to the user rather than a generic "session is active" warning.
Cross-linking for context: #48965 tracks the broader multi-session coordination primitive gap. The liveness signal you need here is a prerequisite for anything in that thread to work safely.
This is addressed in a recent release: when a session enters or creates a worktree, Claude Code now places a
git worktree lockon it that names the owning session's process. Cleanup paths in Claude Code check that lock and treat a live owner as "in use" (not just uncommitted changes), and a plaingit worktree removefrom another session or agent will refuse the locked tree unless forced. The lock is released when the session leaves the worktree, and stale locks from dead processes are cleaned up automatically.Changelog: https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md
🤖 Generated with Claude Code