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.