Background session cannot be deleted after a squash merge: "worktree is not clean" on a clean worktree
Summary
Deleting a background session from claude agents (ctrl-x, ctrl-x) fails with "cannot delete because the worktree is not clean" whenever the worktree's checked-out commit is not reachable from origin/main — even when the working tree is spotless and the work has been merged.
Because squash merging rewrites a branch's commits into one new commit on main, the branch's original commits are reachable from nowhere afterwards. So for anyone using squash merge — GitHub's default, and a repo-wide requirement in ours — every session that successfully lands its work becomes undeletable. The failure is the normal outcome of a completed task, not an edge case.
Environment
- Claude Code 2.1.237
- Linux 6.6.87.2-microsoft-standard-WSL2
- Session started with
claude --bg, working in a git worktree under.claude/worktrees/
Reproduce
- Start a background session that creates a worktree and branch.
- Commit, push, open a PR, and squash-merge it. Let GitHub auto-delete the remote branch.
- Confirm the worktree is clean:
git status --porcelainreturns nothing. - In
claude agents, select that session's row and press ctrl-x twice.
Expected: the session is deleted — the work is merged and the worktree has no uncommitted changes.
Actual: refused with "worktree is not clean".
Evidence that the tree really is clean
Three separate worktrees, all reporting zero uncommitted changes, all refused:
| Worktree | Uncommitted changes | Commits not in origin/main | Delete |
|---|---|---|---|
| A | none | 24 (squash-merged as one commit) | refused |
| B | none | 2 (squash-merged) | refused |
| C | none | at least 1 (squash-merged) | refused |
The discriminator is the commit count, not cleanliness. Ruled out along the way, each verified separately and none of them the cause:
- untracked files — none, in either a sandboxed or unsandboxed view
- a dangling upstream after the remote branch is auto-deleted — clearing it with
branch --unset-upstreamremoves the status warning and changes nothing about the refusal - a stale
config.lockin the shared git directory — real, and worth fixing separately since it blocks config writes across every worktree sharing the repo, but removing it did not make the delete succeed
Workaround, which also confirms the cause
In the stuck session, detaching HEAD onto origin/main makes the delete succeed immediately. Only HEAD moves; the branch ref keeps its commits, and checking the branch out again restores the previous state. That the fix is exactly "make HEAD reachable from main" is what identifies the check.
Removing the worktree with git directly also works at any point — it objects to a dirty tree but never to unreachable commits, which suggests the teardown check is stricter than git's own.
Suggested fix
The check appears to treat "commits unreachable from main" as "unsafe to discard". Sound instinct, wrong test under squash merging, where unreachable-but-merged is the expected end state. Options, roughly in order of preference:
- Ask the forge, not the graph. Querying the host for a merged PR on that branch answers the question correctly under squash merges, where ancestry cannot. Reachability is still a valid fast path: if HEAD is already an ancestor of main, no lookup is needed.
- Warn rather than refuse. "This branch has N commits not on main — delete anyway?" leaves the decision with the user. The branch ref survives worktree removal regardless, so deleting the session does not actually lose the commits.
- Fix the message. "The worktree is not clean" sent us to
git statusrepeatedly, which kept reporting a clean tree. Saying "the branch has commits that exist nowhere else" would have turned an hour of diagnosis into two minutes.
Point 3 stands on its own even if the behaviour is intentional: the current wording describes a different condition from the one being tested.