My agents left 6 worktrees behind. I built the janitor that judges them — and refuses to clean.
Every AI coding session that runs in its own git worktree leaves the worktree behind when it ends. Run enough parallel agents and git worktree list turns into a page of good intentions: a merged experiment from Tuesday, a detached HEAD from a run you killed, one worktree git reports as locked because a headless session never unlocked it, one whose folder you already deleted (the registration lingers, prunable).
The pile is genuinely risky in both directions. Each worktree pins a branch. One of them may hold the only copy of some piece of work — uncommitted changes, an untracked file, a commit that was never pushed. Delete blindly and you lose work. Never delete and the repo rots around you. The manual ritual every worktree guide prescribes — list them, then walk each folder checking status, merged state, and unpushed commits by hand — is a chore that survives exactly one cleanup.
So I built worktree-janitor: the ritual as one read-only pass, in dependency-free Python.
pipx install git+https://github.com/jasveerai88/worktree-janitor worktree-janitor scan # verdicts, with reasons worktree-janitor plan # the commands — printed, never run The verdicts are git's facts, not vibes
Per worktree, the tool gathers: branch or detached HEAD; dirty/clean with staged, unstaged, and untracked counts; merged into the default branch or not; unpushed commits; last-commit age from git's own committer timestamps; locked and prunable states with git's recorded reasons. Then:
- SAFE_TO_REMOVE — clean, merged, no unpushed commits, not locked, not the main worktree, not the worktree the scan ran from. A prunable registration (directory already gone) is also SAFE — clearing it cannot lose files.
- REVIEW — something in it exists nowhere else, or cannot be verified: uncommitted changes, untracked files, an unmerged branch, unpushed commits, a detached HEAD, an unreadable status.
- KEEP — the main worktree, a locked worktree, the worktree you ran the scan from, a bare repo.
The discipline that matters: unknown is REVIEW, never SAFE. If a check cannot be made — no default branch can be determined, an upstream ref isn't present locally — the field says so, the check is named under CHECKS SKIPPED, and the worktree is REVIEW. Even the unpushed count names its basis: with an upstream it's upstream..branch; without one it's counted against the default branch, and the report says which (in JSON, unpushed_basis).
The plan is text. That is the whole design.
worktree-janitor plan prints the exact commands — git worktree remove for each SAFE worktree, one git worktree prune, git branch -d for stale branches (merged, checked out nowhere, tip at least 30 days old by default) — in order, with the reason under each. Branch deletions are always lowercase -d, so git itself refuses if a branch turns out unmerged. There is no -D anywhere, deliberately.
And nothing executes them. There is no --execute flag and no code path from the plan to a shell: the commands are assembled as strings and printed. The tool's own git calls — only worktree list, status, branch, log, stash list — all funnel through a gate that refuses any argv carrying a mutating verb or flag before git starts, and run with GIT_OPTIONAL_LOCKS=0, so git does not even refresh the index. The test suite records every argv of a full scan + plan run and asserts each passes the gate, and snapshots the whole fixture repo — git's listings plus every working-tree file — around both commands, requiring exact equality.
Why so stubborn? Because the audience includes agents. A cleanup tool an agent can invoke is a deletion tool with extra steps. An auditor whose plan only a human can run keeps the human in the loop and the irreversibility manual. (The demo GIF ends with git worktree list after the plan: all six sample worktrees still there.)
One sequencing subtlety worth the detour: a merged, old branch can be checked out only in a prunable registration. It isn't stale yet — it's checked out — but it becomes deletable the moment the prune clears the registration. The tool flags it stale_after_prune and sequences its git branch -d after the prune in the plan. Small thing; exactly the kind of thing the manual ritual gets wrong at 1 a.m.
Not a manager — on purpose
Full worktree managers are having a moment, and deservedly: as I write this (2026-11-04), worktrunk — a Rust CLI for worktree management built for parallel agent workflows — is up on GitHub Trending. Managers create, switch, and remove worktrees; that's real value when you're running a fleet. worktree-janitor does none of it. It is the audit step before anyone removes anything: what is this pile, what is safe, what needs a human's eyes. Different job, deliberately smaller.
Checkable, not just claimed
The repo ships a sample generator (examples/make_sample.py) that builds the sprawl locally — 6 worktrees (main + locked KEEP, clean/merged + prunable SAFE, dirty/unmerged + detached REVIEW), 8 branches with exactly 1 stale (tip dated 2020-01-02), 1 stash entry. The captured scan, plan, and JSON in examples/ are real runs, and the 36-test suite — real git repositories built with the git CLI in temp dirs, expectations worked out by hand in the test header — asserts the same shape. Exit codes: 0 nothing to clean, 1 candidates or REVIEW items, 2 error (never a traceback; in --json mode, errors are {"error": ...} objects, argparse's included).
Limits, plainly: git-visible state only (an editor's private worktree stash outside the repo is invisible to git and to this); no disk sizes in v1.0.0; local branches only; and a plan is a snapshot, not a transaction — re-run scan after executing one.
It's the third of a small read-only trio I built for agent-heavy setups: skill-vet vets a skill pack before you install it, permission-audit audits a Claude Code allowlist chain, and worktree-janitor audits what the agents leave behind in git. All MIT, all stdlib-only, all judge-and-report — none of them touch the thing they audit.
Feedback I'd actually use: a worktree shape it misjudges. That's the bug report that improves the verdict table.