npm.io
0.2.0 • Published 13h agoCLI

gh-worktree-gc

Licence
MIT
Version
0.2.0
Deps
0
Size
16 kB
Vulns
0
Weekly
0

gh-worktree-gc

Prune merged and stale git worktrees safely — clean-only, so it can never lose work. The companion to gh-issue-lease: that one stops work stampedes, this one stops worktree bloat.

When a fleet of agents (or one busy human) spins up a worktree per task, they pile up fast — dozens of merged branches with dead worktrees hanging off them. gh-worktree-gc cleans them up without ever touching anything that could hold unsaved work.

npx gh-worktree-gc            # safe dry-run: shows the plan
npx gh-worktree-gc --apply    # execute

Zero runtime dependencies. One small file. gh is optional (used only to catch squash/rebase merges).


The safety invariant

Automation only ever removes CLEAN worktrees. A clean worktree has no uncommitted changes, so removing it cannot lose anything — the code is safely on a branch. Anything dirty or detached is BLOCKED — listed for you to handle by hand, never auto-removed (unless you pass --force and mean it).

Worktree state Action on --apply
merged, clean remove worktree + delete its local branch
stale (old), clean, unmerged remove worktree, keep the branch (work is parked, not lost)
dirty / detached BLOCKED — reported, never touched (needs --force)
protected (primary checkout, current worktree, bare, locked, /tmp) never touched

Why gh matters here

"Merged" is detected two ways and OR'd together:

  1. git ancestry (git branch --merged <base>, one batched call) — fast, local, no network.
  2. merged PR head branches from gh pr list --state merged — when gh is available.

That second signal is the important one: a squash or rebase merge creates new commits on the base that are not ancestors of your branch, so a pure git check thinks the branch was never merged and leaves the worktree behind forever. Pulling the merged-PR list from GitHub catches exactly those. If gh isn't installed or authed, it silently falls back to ancestry-only (pass --no-gh to force that).


Install & use

npx gh-worktree-gc              # one-off, no install
npm i -D gh-worktree-gc         # or as a dev dependency

Requires git. gh (authenticated) is optional but recommended.

Flag Default Effect
(none) Dry run. Print the plan, change nothing.
--apply off Execute the removals.
--merged-only off Ignore stale age; only prune merged worktrees.
--stale-days <n> 14 Age (days, by last commit) at which an unmerged clean worktree is "stale".
--base <ref> remote default branch What "merged" is measured against (auto-detects origin/HEAD).
--force off Also remove dirty/detached worktrees. Dangerous — can lose uncommitted work.
--no-gh off Skip PR-merge detection; use git ancestry only.
--no-fetch off Don't git fetch first.
--include-external off Don't protect /tmp, /private/tmp, /var/folders worktrees.
--json off Machine-readable plan.
--quiet off Suppress the report (for hooks/cron).
Programmatic API
import { classify, parseWorktrees, resolveDefaultBase } from "gh-worktree-gc";
// classify() is the pure decision function; the rest are the git helpers.

Automate it

Post-merge hook (opt-in) — GC after a local merge into the default branch:

mkdir -p .githooks
cp node_modules/gh-worktree-gc/hooks/post-merge .githooks/post-merge
chmod +x .githooks/post-merge
git config core.hooksPath .githooks
git config worktreeGc.enable true

Most merges happen on GitHub (which a local hook can't see), so the reliable catch-all is a periodic run — a cron, or after each git fetch:

gh-worktree-gc --apply --quiet

Because it uses gh to find merged PR branches, this reaps the squash/rebase-merged worktrees that a local hook would never notice.


Guarantees & edge cases

Case Behaviour
Uncommitted changes in a worktree BLOCKED — never removed without --force. The core safety guarantee.
Squash/rebase-merged branch Detected via gh merged-PR list (git ancestry alone would miss it).
gh offline / not installed Falls back to git-ancestry merge detection; still safe, just misses squash/rebase merges.
Current worktree / primary checkout Protected — never touched.
Detached HEAD BLOCKED (could hold commits not on any branch).
Stale but unmerged Worktree removed, branch kept — parked work is never deleted.
Bare / locked worktrees Protected.
Very large merged-PR history gh pr list is capped at 300 by default; older merges fall back to ancestry.

License

MIT Mackenzie Robertson

Keywords