worktree cleanup
Remove stale worktrees that are safe to delete.
Usage
worktree cleanupTypical 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
| Flag | Description |
|---|---|
-f, --force | Skip 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-agents | Remove 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.