Skip to content

Repository files navigation

worktree-sweep

CI

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.

Requirements

  • 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 stat are 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/c are 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.

Install

From a clone

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/wtsweep

Single file, no clone

mkdir -p ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/wouterkroes/worktree-sweep/main/bin/wtsweep \
  -o ~/.local/bin/wtsweep
chmod +x ~/.local/bin/wtsweep

Homebrew

brew install wouterkroes/tap/wtsweep

The 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.

Verify

wtsweep --version     # should print: wtsweep 0.1.1

command not found means ~/.local/bin is not on your PATH:

echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && exec zsh

Do not define wtsweep as a zsh function in .zshrc. Functions take precedence over PATH, so the script would never run.

Usage

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 --help

Read the report, then re-run with --yes. In that order, every time — the tool deletes directories and the report is the only review step.

How it compares

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.

What survives

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 --porcelain is empty
  • git rev-list --count HEAD --not --remotes is 0

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.

Broken checkouts and --orphans

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 .git file 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.

Configuration

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/*/*/*)

What it does not do

  • 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.

Tests

./test/smoke.zsh

62 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.

Contributing

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.

License

MIT

About

Cleans up the git worktrees your coding agents leave behind — across every repo, including the ones git no longer knows about.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages