Cleans up the git worktrees your coding agents leave behind — across every repo, including the ones git no longer knows about.
Coding agents create git worktrees. When they crash, get killed, or get
uninstalled, the worktrees stay. wtsweep finds them and removes the ones it can
prove are safe to remove.
$ wtsweep
🔍 wtsweep: DRY RUN, nothing will change. Idle > 7d, 1 repo(s)
📁 ~/work/main-app
would prune 1 stale registration(s)
⚠️ broken ~/work/main-app/.claude/worktrees/add-metrics [claude, dead pointer, 436d] -- needs --orphans
would remove ~/work/main-app/.claude/worktrees/fix-login [claude detached, 436d]
🔎 orphan directory sweep
none
──────────────────────────────────────
worktrees 1
orphan dirs 0
stale metadata 1
kept: recent 1
kept: dirty 1
kept: unpushed 1
kept: locked 0
kept: broken 1
Nothing changed. Re-run with --yes to apply.
1 broken checkout(s) skipped. Add --orphans to delete them.
Dry-run by default. Nothing changes without --yes.
- zsh — required, not optional. The script uses zsh arrays, glob qualifiers, and parameter expansions; it will not run under bash.
- git 2.36 or newer, checked at startup. The sweep reads
worktree list --porcelain -z, and NUL-delimited records are not a preference: parsing the newline format truncates any path containing a newline, and a truncated path points at something else. Older git is refused rather than silently degraded. - macOS or Linux. Both BSD and GNU
statare supported, verified at startup; CI runs the test suite on both. - Windows: under WSL only, and only for repos that live in the Linux
filesystem. Install zsh in the distro (
sudo apt install zsh); wtsweep needs it as an interpreter, so there is no reason to change your login shell. Repos under/mnt/care on a Windows filesystem, where two of the checks do not hold — see What is not checked. There is no native Windows support, and porting the script would not create any: the checks rest on POSIX filesystem semantics, not on the shell.
One file, and git pull is the updater. The better option if you want to read
what you are about to point at your disk.
git clone https://github.com/wouterkroes/worktree-sweep.git ~/repos/worktree-sweep
mkdir -p ~/.local/bin
ln -s ~/repos/worktree-sweep/bin/wtsweep ~/.local/bin/wtsweepmkdir -p ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/wouterkroes/worktree-sweep/main/bin/wtsweep \
-o ~/.local/bin/wtsweep
chmod +x ~/.local/bin/wtsweepbrew install wouterkroes/tap/wtsweepThe formula lives in wouterkroes/homebrew-tap and is pinned to a tagged
release, so it lags main by whatever is unreleased. brew install --HEAD wouterkroes/tap/wtsweep tracks main instead.
wtsweep --version # should print: wtsweep 0.1.1command not found means ~/.local/bin is not on your PATH:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && exec zshDo not define
wtsweepas a zsh function in.zshrc. Functions take precedence overPATH, so the script would never run.
wtsweep # report, change nothing
wtsweep --yes # remove what the report listed
wtsweep --verbose # also show what was kept, and why
wtsweep --days 14 # stricter: only worktrees idle over 14 days
wtsweep --days 0 # ignore age entirely
wtsweep --orphans # include broken checkouts (see below)
wtsweep ~/path/to/repo # specific repos instead of the configured list
wtsweep --helpRead the report, then re-run with --yes. In that order, every time — the tool
deletes directories and the report is the only review step.
Worktree managers own creation. Their cleanup, where it exists, asks "is this
branch merged into main?" — a question that says nothing about the
detached-HEAD worktrees agents actually produce. And no hook can fire for a
worktree whose owner is gone: a post-remove hook only runs when someone calls
remove.
wtsweep is not a manager. It creates nothing, switches nothing, and replaces
none of these. Run it next to whichever one you use.
wtsweep |
worktrunk | wtm | git-worktree-cli | agent-worktree | |
|---|---|---|---|---|---|
| Has a cleanup command | ✅ | ✅ | ✅ | ✅ | ✅ |
| Skips uncommitted changes | ✅ | ❓ | ✅ | ✅ | ✅ |
| Will not drop unpushed commits | ✅ | ✅ | ✅ | ❓ | ❓ |
| Decides on detached HEAD | ✅ | ❌ | ❌ | ❌ | ❌ |
| Idle-age threshold | ✅ | ✅ | ❌ | ❌ | ❌ |
| Multiple repos per run | ✅ | ❌ | ❌ | ❌ | ❌ |
| Finds unregistered checkouts | ✅ | ❌ | ❌ | ❌ | ❌ |
| Creates worktrees | ❌ | ✅ | ✅ | ✅ | ✅ |
Sourced from each project's own documentation, checked August 2026. ❓ means the
docs do not say. Corrections welcome as an issue — please link the page that says
otherwise.
Two rows carry the argument. Detached HEAD: all four alternatives decide on
integration status — worktrunk's wt step prune removes worktrees
"integrated into the default branch" (with --min-age, default one day, and
locked and main worktrees always skipped), wtm and git-worktree-cli ask whether
the branch merged. That question needs a branch, and agent worktrees are
frequently detached; it also answers "no" for work that is pushed and safe but
nowhere near main. wtsweep asks whether every commit exists on some remote,
which is defined for detached HEAD.
Unregistered checkouts: worktrunk also cleans stale worktree entries whose
directory is gone. That is git worktree prune's direction. The opposite case —
a directory still on disk that no repo registers any more — is the one nothing
else looks for, and the one that fills a disk after an agent is killed.
A worktree is removed only when all of these hold:
- not the main worktree, not locked, not the directory you ran from
- last activity older than
--days git status --porcelainis emptygit rev-list --count HEAD --not --remotesis0
That last check is the one that matters. It works on detached HEAD, where "is the branch merged" cannot answer, and it treats any remote as good enough — so a pushed-but-unmerged branch is still safe to sweep.
Age is the newest of the directory mtime and the git admin files (index,
HEAD, ORIG_HEAD). Directory mtime alone lies: an npm install in a
months-old worktree makes it look brand new.
Full reasoning, including what is deliberately not checked: docs/safety-model.md.
A broken checkout is a directory whose .git pointer is missing or resolves
to nothing. Git cannot reach it, so status and rev-list cannot run — the two
checks that protect you from losing work are unavailable.
These are reported by default and deleted only with --orphans. The report says
which of two shapes it found, because the evidence differs:
dead pointer— a.gitfile that resolves to nothing. This was a worktree.no .git— no pointer at all. It may have been a worktree, or it may be an unrelated directory sitting under a worktree root.
Read the list before using --orphans. Recovering work from one is possible but
manual: docs/rescuing-broken-checkouts.md.
Even with --orphans, four kinds of directory are refused outright and reported
as 🛡 protected: a repository (bare or not), a directory that still contains a
checkout, a symlink, and anything that resolves outside the root it was found
under.
git worktree prune cannot help here. It removes metadata for missing
directories — this is the opposite case, a directory with missing metadata.
Optional. With no config file, wtsweep sweeps the repo you are standing in and
looks for worktrees under .claude/worktrees/, .warp/worktrees/, and
.worktrees/ — the in-repo layouts, whose depth is known.
Machine-wide roots (Cursor's ~/.cursor/worktrees, a manager's workspace
directory) are opt-in, one line each. Nothing in $TMPDIR is touched unless you
name it. See the known layouts
table and the worked examples in
share/config.example.
To sweep every repo you own:
# ~/.config/wtsweep/config
WTSWEEP_REPO_GLOBS=(~/repos/*/*/*)- Create worktrees. Not its job. Use a manager.
- Delete branches. Only worktrees and directories.
- Run unattended. No cron. It deletes things; look at the report.
- Stop the source. Agents keep leaking worktrees; this sweeps up after them.
./test/smoke.zsh62 assertions over a throwaway repo built in mktemp -d. Ten worktree scenarios
— clean, dirty, unpushed, detached, detached-unpushed, locked, dead pointer,
missing pointer, a real clone, and a live worktree belonging to a repo that is not
being swept — plus the cases that exist only to catch a removed guard: the
repository itself surviving every apply run, a linked worktree passed as the repo
path, the caller's cwd inside a broken checkout, a symlinked worktree root, a root
configured one level too high, control characters in a path, and a newline in a
path. Nothing outside the temp directory is touched.
CI mutation-tests the suite against itself: it breaks eight named guards one at a time and requires an assertion to fail for each. That is the property worth having — a suite that stays green with a guard deleted asserts nothing — and it is why CONTRIBUTING.md asks for a test with every change to what gets deleted.
The claim is exactly those eight, not "every guard". One check, the
resolved-path containment in delete_blocked, has no test: with symlinked roots
already refused and symlinked candidates rejected, there is no reachable case left
to construct. It is kept as insurance against a future change to the glob, not
because it is exercised.
Bug reports and patches welcome — see CONTRIBUTING.md. Security-relevant reports, especially anything that loses work, go through SECURITY.md. Participation is covered by the Code of Conduct. Release history is in CHANGELOG.md.
MIT