Skip to Content

worktree cleanup

Remove stale worktrees that are safe to delete.

Usage

worktree cleanup

Typical cleanup targets include:

  • worktrees whose remote branch no longer exists — the usual shape of a merged pull request — as long as they carry no unmerged commits and hold no uncommitted changes
  • local worktrees with no tracked remote that carry no unmerged commits and hold no uncommitted changes

What holds a worktree back

Uncommitted changes disqualify a worktree whatever its remote looks like. The count comes from git status -s, so untracked files hold a worktree back too.

Unpushed commits disqualify it too. A worktree whose directory still exists is only removed when its commit count was actually taken and came back zero — a count that could not be taken is never read as “no work”. This matters most for a branch you never pushed: it has no upstream to compare against, so the comparison base is origin/HEAD, falling back to defaultSourceBranch. If neither resolves the count cannot be taken, and the worktree is held back rather than swept.

That second rule is newer than the first, and the ordering is deliberate: committing work must never move a worktree from protected to disposable.

Commits that were merged do not count as unpushed, however many of them there are. A squash merge lands a branch as one new commit and a rebase rewrites every commit it touches, so after either one the branch’s own commits are nowhere in the base by identity while every line they changed is there by content. Counting alone would report such a worktree as carrying work for as long as it existed — on a repository that squash-merges its pull requests, that is every merged branch, and cleanup would find nothing to do ever again.

So a worktree that is ahead of its base is asked a second question before anything is decided about it: is the change it carries, taken as a whole, already applied in that base? Where it is, the worktree is stale and says which base it landed in. Where it is not — including a branch that was merged and has picked up a commit since — the commits are real work and hold it back exactly as before.

An agent session that is working holds one back as well — see below. One that is only idle does not.

What it prints

Every worktree cleanup offers to remove says why it qualified, so a line can never mean “no reason found”:

Found 4 worktree branches that are marked safe to remove. - feature/api-retries (Remote removed, no unpushed commits) - feature/bulk-actions (Remote removed, merged into origin/main) - feature/spike (No tracked remote, no unpushed commits) - feature/moved (Path does not exist)

Those four are the whole vocabulary. merged into <base> is the one that names a count rather than denying one: that worktree is ahead of origin/main, and every commit in it is already applied there. Path does not exist answers none of the checks above: a worktree whose directory is already gone is removed before its commits are considered, because there is no directory to count them in.

cleanup also reports what it held back, under three headings:

Skipped 1 worktree branch that has uncommitted changes: - feature/wip (Remote removed, 3 uncommitted changes) Skipped 1 worktree branch whose unpushed commits could not be counted: - feature/orphan (Unpushed commits unknown: no comparison base; set defaultSourceBranch or run git remote set-head origin --auto) Skipped 1 worktree branch with a live agent session: - feature/api (Remote removed, Agent: feature-api-1f)

The middle heading is the one that makes No stale worktree branches found. trustworthy in the way this matters: that line can no longer be produced by a worktree whose commits could not be counted. Such a worktree is always named.

Those three headings are the whole of what cleanup reports. A worktree carrying unmerged commits that it could count is not stale, so it is neither removed nor listed — it simply does not come up, the same way a worktree tracking a live remote branch never has. Use list to see every worktree and what each one is carrying; cleanup only ever discusses the ones it would sweep and the ones it stopped short of.

Flags

FlagDescription
-f, --forceSkip the confirmation prompt. A non-interactive run requires it: without it cleanup exits 2 with worktree: no default for confirmation to delete the stale worktrees; pass --force
--ignore-agentsRemove worktrees even when an agent session is living in them

Agent sessions

A worktree that an agent session is working in is never removed. cleanup asks the same two sources as list --agents — herdr agent list when herdr is installed, and the runtime in agent.command (or the herdr.agent kind when that is unset) — which sessions are running and where, and holds back any worktree one of them is working in. Where that worktree is one it would otherwise have swept, it is reported under this heading, naming both the worktree and the session sitting in it:

Skipped 1 worktree branch with a live agent session: - feature/api-retries (Remote removed, Agent: feature-api-retries-1f)

This covers your own terminal and an agent Herdr started, as well as an agent this tool dispatched with branch --agent. Both count as somebody working in the directory, and deleting it out from under either is the failure this check exists to prevent. Use list --agents to see which worktrees those are before running cleanup, rather than discovering them from the report.

A session that has finished does not hold anything back — the worktree goes back to being ordinary stale work. A session the runtime calls finished but Herdr still shows in a pane is judged by what Herdr says about it, and one whose process is still running is judged as any live session is, by its status below. The runtime has been seen to report a session finished while its process was still working in the worktree.

Nor does a session that is only idle, such as an agent pane left open at its prompt in a merged worktree. The runtime’s idle and Herdr’s idle both count, and so does Herdr’s done, a finished turn in a pane nobody has looked at since. When the runtime and Herdr both describe one session, every status they give has to say idle or done. A session that is working, one that is blocked on a prompt, one whose status is not recognised, and one that reports no status at all still hold the worktree back. Where a worktree holds several sessions, one that is working holds it back whatever the rest are doing. An idle session does not excuse anything else: uncommitted changes and unpushed commits hold the worktree back as before. With opener set to herdr, the idle pane closes with the worktree’s space.

--force does not override this. It answers the confirmation prompt, not the safety verdict. To sweep a worktree anyway, pass --ignore-agents, which skips the session lookup entirely and leaves cleanup deciding exactly as it did before agents existed. It has no short alias on purpose: overruling this one is worth spelling out.

--ignore-agents costs more than it used to. With opener set to herdr, a worktree removed this way also has its Herdr space closed — which takes down the panes the agent is working in, not just the directory underneath it. Before spaces were closed, an overruled agent at least kept its output on screen.

If neither source is available — herdr is not installed and no runtime is configured, or the runtime cannot be reached — no session data is gathered and cleanup behaves exactly as it always has.

Herdr spaces

With opener set to herdr, every worktree cleanup removes also has its Herdr space closed.

Only the ones it actually removed. A worktree held back — for uncommitted changes, a live agent, or unpushed commits it could not count — keeps its space, because its checkout is still there — cleanup closes what it deleted, never what it merely looked at. Each space is closed independently, so one failure does not stop the rest, and a failed close is a warning that leaves the exit code at 0.

With opener unset or set to editor, no Herdr process is started. See Herdr Spaces.

Last updated on