Worktree Cleanup Skill (reap landed worktrees, safely and reversibly)
A Claude Code skill that bulk-cleans the git worktrees left behind by finished work, deciding what has landed by content rather than by branch name so it also catches squash, rebase, cherry-pick and replay landings, and saving uncommitted work plus a recovery SHA before it deletes anything.
What it is
A Claude Code skill (/worktree-cleanup) that bulk-cleans the git worktrees left behind by finished work — and, on request, the local branch refs that have no worktree. It fills the gap between "my session ended and the work shipped" and "that worktree is still sitting on disk eating space." It is the companion to merge-all-ready, which lands the branches this then reaps. These are the same worktrees an isolated session runs in — see Session isolation.
It decides "has this landed?" by content, not by branch name and not by a plain ancestor check. It compares each commit's patch-id against the integration branch, so it also catches work that landed by squash, rebase, cherry-pick, or an auto-lander replay — landings a merge-base --is-ancestor test misses entirely. The built-in scheduled cleanup makes the same call: the vendored reaper classifies a worktree landed by ancestry, by squash-identical content (merge-tree --write-tree), or by every commit's patch already being in master (cherry-clean), and records <branch>\t<sha>\t<path>\t<how> to a recovery ledger plus a patch of any uncommitted changes before it deletes anything. What this skill adds is the interactive half — a human-approved plan, the squash-trap symbol check that needs judgment, and the optional deeper passes below — not a stronger detector.
It is pure git. GitHub PR status is used as one extra signal when gh and a GitHub remote happen to be there, and is never required — the skill works in any repo, with no remote and no gh auth.
Where to find it
It is a skill, not a screen: in any Claude Code session working in the project you type /worktree-cleanup or say "clean up worktrees," and the skill does the work through git. Its unattended mode is what the Dev Pipeline's daily maintenance job invokes. The one in-app surface is the Worktree Cleanup dashboard inside the Dev Pipeline panel, which surfaces and controls the PowerShell cleanup scripts and carries the Run cleanup now button.
How it behaves
How to use it
In any Claude Code session working in the project, type
/worktree-cleanupor say "clean up worktrees."The skill discovers every worktree, applies the never-touch guards, classifies the rest by content, and presents a plan with three buckets:
- Landed — provably in the integration branch. Safe to reap.
- Keep-viable — real, unique, unlanded commits (or an open PR). Left alone.
- Ambiguous — can't prove landed, can't prove viable. Kept and reported, never auto-removed.
A machine auto-save tip is judged by the commit under it, not by itself. When a session is abandoned, the app saves its uncommitted work by committing
chore: auto-save WIP before worktree cleanuponto the branch tip — and the lander refuses by design to land that snapshot, so it can never be an ancestor of the trunk. Asking "is this tip landed?" therefore answered no forever for a worktree whose real work was entirely on the integration branch, and the worktree was reported Keep-viable indefinitely (measured 2026-09-23: three worktrees, each reading as one unlanded commit). The floors now ask their own landed question about the authored commit the snapshot sits on, so such a worktree lands in Landed where it belongs. The snapshot itself is still preserved — the lander refuses to land it, and its content is archived under a durable ref before any branch is deleted.Review the plan and tell the AI which to remove (all landed ones, specific picks, or none).
The skill saves any uncommitted work first, logs a recovery SHA for every branch it deletes, removes the approved worktrees, prunes stale git admin entries, and reports what it did and where the recovery files are.
It also runs unattended — see Modes below — which is what the Dev Pipeline's daily maintenance job uses.
Modes
Interactive (the default, a human is present). Classifies everything, presents the plan, and acts only on your approval. This mode gets the full detection — including the squash-trap check, which needs judgment — and it is the only mode that can run the optional deeper passes below.
Unattended (
--auto). The AI does not run the reap procedure in this mode — it delegates and stops.POST /worktree-cleanup/run-now(empty body) on the local control server, pollGET /worktree-cleanup/statusuntillastRunAtadvances, then readGET /worktree-cleanup/runs(the rows are underdata.items) — that IS the report. The route drives the same hardened, vendored PowerShell reaper as the in-app supervisor and the nightly task, with every floor, the reversible retire-to-trash and the recovery SHA. Read the status rather than assuming 200: a started pass answers202(the route does not wait for it), and a200carryingdeferredmeans the app is HOLDING the reap because the machine is under load — report "held for load" and stop. A409means a pass is already running,422means this host cannot run one,503means the tool is missing or the spawn failed. Never passforce(that override belongs to a person) and never poll the route in a loop.Never fall back to
git worktree remove,git branch -D, or a hand-rolled reap. That fallback IS the hazard this mode exists to remove:--autois judgment-free by definition — reap only provably-landed, skip everything ambiguous, never wait — so there is nothing for a model to decide and everything for it to get wrong. On 2026-08-26 an improvised unattended reaper deleted 4+ LIVE dev-pipeline worktrees. The procedure further down this page is the interactive path only, where a human approves each removal.Two reporting rules that go with it: state the count you actually REMOVED, never the count you were going to remove (on 2026-09-02 a run reported 12 on a night it removed 1, because it counted the eligible list), and always state the backlog — how many worktrees are still open and how many are held for uncommitted work, unsaved files or unlanded commits. "Nothing was eligible" on its own hides an unbounded pile.
The Dev Pipeline's daily maintenance run drives cleanup through exactly those three route calls — its prompt tells it not to open this skill and not to run git — and it never lands anything. See dev-pipeline-maintenance.md.
An optional --idle <duration> (e.g. --idle 12h) treats anything touched more recently than the threshold as possibly-live and leaves it alone — unless it is provably landed. The default is no threshold: the skill guards on state, not age.
The safety floor (never violated, in either mode)
- Never lose committed work. Before deleting any branch,
<branch>+ its tip SHA are appended to a recovery log, so it can be restored withgit branch <name> <sha>. A branch is deleted only when its work is provably in the integration branch. - Never lose uncommitted work. Before removing any worktree with uncommitted changes, they are saved to a patch (
git -C <wt> diff HEAD > <recovery>/<slug>.patch) plus a copy of its non-junk untracked files. Phantom dirt (CRLF / stat-cache noise,.env,node_modules, build output, a lockfile with no manifest change) is recognised as junk; real source / test / doc changes are always rescued. - Never touch live work. A locked, mid-operation, actively-marked, or main/bare worktree is skipped. When unsure, it keeps.
- Never touch a worktree a run is parked in. A worktree whose
.claude/pipeline/state.mdsays an unfinished run is live is kept, because a run waiting at a gate is clean and commitless — the dirty check cannot see it, so this file is its only protection. Two things end that hold, and both are things the file says about itself: a finished status, or a finished phase (PHASE_6_GITPREP/COMPLETE, which the pipeline writes before it tags). A file that is not a state file at all — an agent's own notes left at that path — is not held; that is the 2026-09-21 fix for a worktree that could otherwise never be cleaned up. A half-written file is still held, always. - Reversible by construction. Worktree removal keeps the branch unless the branch is provably landed; branch deletion is SHA-logged; dirty state is patched first.
<recovery> is a directory outside the repo — your OS temp dir is fine ($TMPDIR/worktree-cleanup on macOS/Linux, %TEMP%\worktree-cleanup on Windows). It is created once at the start of a run and its location is named in the final report.
How it decides what has landed
Strongest signal first, per candidate branch B against the integration branch (main if it exists, else master, else the remote's default; the origin/<INT> ref is preferred when a remote exists):
- Ancestor —
git merge-base --is-ancestor B <INT>exits 0 ⇒ every commit is already on the integration branch. Landed. This is the fast-forward / merge-commit case. - Patch-identical — else
git cherry <INT> B: if every line is prefixed-, each commit's patch-id already exists on the integration branch. Landed. This is what catches cherry-pick, rebase, and replay landings (an auto-lander that replays a branch's commits under new SHAs). Patch-id equality is strong enough to be trusted unattended. - Squash-trap (interactive only — needs judgment) — a squash rewrites patch-ids, so
git cherryshows+for work that IS on the integration branch. For a+commit the skill pulls a distinctive added symbol / filename / string and looks for it on the integration branch (git grep -F <token> <INT>/git cat-file -e <INT>:<path>). Present ⇒ squash-landed. Genuinely absent and the change is real ⇒ not landed, keep. This guess is never run unattended — a+that isn't proven present stays "not landed." - GitHub PR (optional extra signal) — if
ghand a GitHuboriginexist: aMERGEDPR ⇒ landed; aCLOSEDPR with an automated "already on the integration branch" comment ⇒ landed;OPEN⇒ keep. Noghor no remote ⇒ this step is skipped. That is expected, not an error.
The automatic reaper decides with a longer ladder of its own. The four signals above describe the interactive path. The PowerShell reaper that --auto, the hourly supervisor and the nightly task all delegate to runs its own ordered checks instead — and its first one is not on that list at all:
landed-as— on by default. If the branch description carries alanded-as:<sha>marker (provenance the auto-lander itself wrote), the reaper asks only whether that sha is an ancestor of the integration branch, and stops there — every content signal below is skipped. Kill switch:AMC_DISABLE_LANDED_AS_CHECK=1. This is auto-lander provenance rather than a branch-name guess, but it is not one of the four content signals above.- Ancestor, then cherry-clean — the ancestry test, then every commit's patch already being on the integration branch.
squash-clean— a squash-identical result, checked withgit merge-tree --write-tree.subject-landed— opt-in (-TrustSubjectMatch). Declares a branch landed when every commit subject is present on the integration branch. That is exactly the "subject alone" test this page opens by ruling out, which is why it is opt-in and never the default.- Two caller-supplied bypasses.
-TrustLandedskips the landed test altogether for worktrees the caller has already content-verified;-KeepBranchretires the folder but preserves the branch. Neither is set by the normal flow.
Guards come before all of this. A worktree is skipped outright — never removed, in either mode — if it is locked, has an in-progress git op (MERGE_HEAD / a rebase or cherry-pick in flight / a live index.lock), carries an active status marker in its branch description, is dirty with real uncommitted work, is the main or bare checkout, or was touched more recently than an --idle threshold you supplied. The one exception to the idle threshold: provably-landed work is safe to reap regardless of age.
A lock is not an absolute promise — read this before you rely on one. The guards above are what this tool honours, but a lock is also cleared by a separate background job that runs on its own (The stale-lock reaper, below). So a lock protects a worktree while its owning session is alive — it does not protect it forever after that session ends. If you want a worktree kept regardless of any staleness rule, pin it. The pin is the product's own keep marker, and it outranks every staleness rule in this subsystem; nothing described on this page clears it.
The stale-lock reaper — the background job that clears locks
A lock is the strongest runtime guard the cleanup honours, so the product ships a second, separate job whose only purpose is to clear the ones that have gone stale and are now blocking that guard — and git worktree prune along with it. It runs every 30 minutes, it is on by default, it has no user setting, and it raises no inbox card (the notice it writes is one of the self-heal FYIs, silenced unless you turn on show self-heal notices). Its only switch is the AMC_DISABLE_WORKTREE_LOCK_REAPER environment variable.
It clears a lock in exactly these cases, each with its own floor:
- a leftover
initializinglock — the provisioning lock a killed or restarted dependency setup never released — once it is over an hour old; - a
dev-pipeline active runlock whose owning session is provably finished, idle for over six hours; - a lock of any reason whose worktree is provably owned by a session that has ended, idle over six hours;
- a lock whose owner the worktree ledger still records but the session list no longer contains at all, with no claim on the path and nothing uncommitted in the tree, idle over six hours.
It never touches a pin, the auto-lander's own recovery lock, a lock whose owner is still alive, a lock with no recorded owner and no claiming session, or any tree holding uncommitted work.
If something went wrong — how to recover
Nothing either path deletes is gone — but where the undo lives depends on which reaper ran, so settle that first.
The automatic reaper (--auto, the in-app supervisor, the nightly task) is the hardened PowerShell scrubber, and it writes its recovery into a directory of its own: %LOCALAPPDATA%\AMC\wt-cleanup, falling back to C:\tmp\wt-cleanup when there is no LOCALAPPDATA. Its ledger of deleted branches is wt-deleted-recovery.tsv in that directory. This is the one that ran if a worktree vanished overnight with nobody in the loop — look there before concluding the work is gone. <recovery> below is not that directory.
The interactive path (a human approving each removal) writes its two undo files into <recovery> — the temp directory named under The safety floor above:
| What you lost | Where it is | How to restore |
|---|---|---|
| A deleted branch | <recovery>/reaped-branches.tsv — one <branch>\t<tip-SHA>\t<reason> row per reap |
git branch <name> <sha> (git reflog also still has it) |
| Uncommitted work from a removed worktree | <recovery>/<slug>.patch, plus copies of the non-junk untracked files |
git apply <recovery>/<slug>.patch in a fresh worktree on that branch |
| An archived (but not deleted) branch | still on the branch — only the worktree went | POST /worktrees/create with the archived branch as baseBranch (never as branch) — full recipe in orphan-worktree-archive.md |
Optional deeper passes (interactive, opt-in)
None of these run unless you ask for them:
- Archive — reclaim disk from idle unmerged worktrees without losing the work: rescue any uncommitted changes, remove the worktree, and keep the branch. Restore it any time. Inside Omniscio that is one call —
POST http://127.0.0.1:19519/worktrees/create { repo, baseBranch: "<the archived branch>", branchPrefix: "recover", taskTitle: "<what you are doing>" }, which makes a new folder on a new branch forked from the archived one and registers an owner, then provisions its dependencies in the background — readreadinessWarningon the reply before working in the new folder, because a200does not mean usable and a202 status: 'creating'means it is still being built (orphan-worktree-archive.md); rawgit worktree add <path> <branch>is the repo-agnostic fallback and leaves the worktree owned by nobody. The archived branch goes inbaseBranch, never inbranch—branchnames the branch to create, so passing the archived name there is refused400 branch-exists, and following that message would put you on a brand-new empty branch and leave the preserved commits behind. The skill verifies each branch survived and reports N / N preserved. - Bare branches — apply the same landed detection to local branch refs that have no worktree, and delete the ones that are provably landed (SHA-logged first). Branches checked out by a worktree, and protected ones (
main/master/release*/ a deliberate keep), are off-limits. A+commit that isn't proven present stays a keep. - Clear stale guards — a guard can itself be stale: a status marker from a session that died days ago, or a lock left by a crashed
git worktree add. For a protected worktree idle well past the threshold, this pass content-verifies it is landed (or is pure scratch), then clears the guard and reaps. It never clears a guard on a worktree with real unmerged work, uncommitted WIP, or an explicit do-not-delete.
Why a cleanup request can be held
Since 2026-09-06 the "run cleanup now" request that agents send through the control server
(POST /worktree-cleanup/run-now) obeys the same load gate as the hourly cleanup: while the
computer is busy — the user-harm verdict (or the CPU reading while that verdict is blind), a
saturated worktree drive — the request is held, not run. The reply says so plainly:
200 { started: false, deferred: { reason, gate } }, where gate names what held it (harm,
cpu, or volume), and nothing is spawned. A calm computer answers exactly as before.
- Your own click always runs. The Run cleanup now button in the Dev Pipeline panel (desktop or phone) is a person asking, so it is never held.
- Only a person can force it from the command line.
force: trueis honoured only for the operator's own token sent from a plain terminal; an agent session that passesforcegets a403and nothing runs. An agent that is held should report "held for load" and stop — never retry in a loop, never force. - Nothing is queued for later. The hourly cleanup fires on its own once the box is calm, so a held manual request needs no follow-up.
Why a create can be deferred
Cleanup has a twin on the other end of the lifecycle. Since 2026-09-04 a create-admission authority sits in front of every new working folder, and it can hold one back. This is what you are seeing if a session or an agent reports that a working folder is "waiting its turn".
Your own new sessions are never held by it. A create you started — a new session in the app, or your own shell calling the create API without a session id — skips the budget and the cap entirely. The single exception is a drive that is physically below its floor, and that message names the drive, because no amount of waiting fixes a full disk.
What can be held, and why:
- The budget. Working folders may not be created faster than the box reclaims them, plus a small allowance (20/hour by default). This is the limit that did not exist on 2026-09-04, when six unattended jobs created ~90 an hour between them against a cleanup path retiring ~50-60, and the overflow filled the drive holding the repository itself.
- The cap. Live working folders stay under
max(50, 1.5 × running sessions), so the ceiling rises with a busy fleet instead of throttling it. - A drive under its floor. The only condition that holds everyone.
It tries to fix the problem before it reports it. When a create is over budget or a drive is low, the authority asks the landed-retire drain for one bounded batch of cleanup — up to 60 seconds — and re-decides once. Very often that is enough and the create simply proceeds. It never loops and never waits indefinitely.
How to see it. A defer or refuse is never silent:
- the caller gets the reason back, with how long to wait — not a generic error;
- one row lands in
~/.amc/worktree-placement-refusals.jsonlrecording who asked, why, on which drive, and what the cleanup pass managed to free; node scripts/ops/fleet-status.mjsprints awt admissionline whenever anything was deferred in the last hour (silent otherwise);- if deferrals run unbroken for ten minutes, one inbox card says so and names the kinds of work most affected.
Turning it off. AMC_DISABLE_WORKTREE_CREATE_BUDGET=1 admits everything. The thresholds are
AMC_WORKTREE_CREATE_BUDGET_HEADROOM, AMC_WORKTREE_LIVE_CAP_FLOOR and
AMC_WORKTREE_LIVE_CAP_MULTIPLIER. It also fails open on its own: if it cannot read the disk or
the ledger, it admits the create rather than becoming an outage of its own.
When there is nowhere to put it at all
A stronger version of the same hold. If the drive your working folders are configured to live on is full, Omniscio will not quietly put them on the drive holding your code instead. That drive is where Git keeps the project itself, and filling it stops everything — it is the failure this rule exists to prevent, and it happened twice before the rule existed.
So when no working-folder drive has room, Omniscio:
- cleans up first — it retires finished working folders on the configured drive and looks again. Most of the time that is enough, because finished folders pile up faster than anyone notices.
- refuses clearly if that did not help — you get a specific "no drive has room" answer rather than a mysterious failure, plus one notice in your inbox with a button that shows you which folders are holding the space.
That notice appears only after the cleanup has already run and come up empty, so it means something real: nothing on this machine can fix it by itself, and freeing space is the next step.
For agents
How it works (for AI agents / contributors) — the INTERACTIVE path
The skill is a pure instruction file at .claude/skills/worktree-cleanup/SKILL.md. It contains no application code — it's a set of steps the AI follows with git (and gh only when it happens to be available), with a human approving each removal. Every git call is git -C "<repo-root>", so it runs from anywhere. Unattended --auto never executes this sequence — it delegates to POST /worktree-cleanup/run-now and stops (see Modes above).
- Discovery:
git worktree list --porcelainto enumerate all worktrees, thengit worktree pruneto clear admin entries whose directory is already gone. - Guards: skip anything locked, mid-git-op, actively marked, dirty with real WIP, the main/bare checkout, or newer than an
--idlethreshold. - Landed check:
git merge-base --is-ancestor <branch> <INT>, thengit cherry <INT> <branch>for patch-id equality, then (interactive only) a token lookup on the integration branch for the squash case. - GitHub (optional):
gh pr list/gh pr viewas one more signal. Absentghor remote ⇒ skipped, not an error. - Rescue BEFORE any removal:
git -C <wt> diff HEAD > "<recovery>/<slug>.patch"plus a copy of the non-junk untracked files (git -C <wt> ls-files --others --exclude-standard), then append<branch>\t<tip-SHA>\t<reason>to<recovery>/reaped-branches.tsv. - Cleanup:
git worktree remove "<path>"—--forceonly after that rescue has run; never over un-rescued real WIP — thengit branch -D "<branch>"only when the branch is provably landed, thengit worktree pruneafter the batch.
Do not shortcut the removal step. An unconditional
git worktree remove --forcedestroys uncommitted work irreversibly, and it is exactly what the skill's safety floor forbids. Rescue first, then remove; force only over dirt you have already saved.
This skill is complementary to the in-app Worktree Cleanup dashboard (in the Dev Pipeline panel), which surfaces and controls the PowerShell cleanup scripts. Whether anything reaps on a timer at all depends on the worktreeCleanupEnabled setting, which ships OFF, and on a default install the answer is nothing — no in-app schedule and no OS task. With the toggle off, the app schedules no reap, and it only ever removes the legacy "AMC Worktree Cleanup" Windows Scheduled Task — it never creates one, so no such task exists on a default install to be "the runner." Switch the toggle on and the app schedules the reap itself (hourly, plus a startup catch-up) and best-effort deletes that legacy task if it happens to be there. Those scripts already do the content-based landed detection and the reversible rescue/recovery (see the top of this page); the skill adds the human-in-the-loop plan and the optional deeper passes.
Related
Its neighbours in the worktree lifecycle are:
- dev-pipeline-maintenance.md — the daily automatic run that invokes this skill unattended, alongside
merge-all-ready - dev-pipeline-panel.md — the Dev Pipeline panel, which includes the worktree cleanup dashboard
- use-skills.md — how to browse, install, and manage skills in Omniscio
Last verified 2026-09-26