git-cleanup

A zero-dependency CLI that scans your local & remote Git branches, cross-references them with pull-request state, and safely prunes what is genuinely dead — merged branches, abandoned work, stale PRs. Everything below runs the real decision engine, live in your browser.

Node ≥ 18 · zero npm deps git only — never touches .git internals GitHub + GitLab PR enrichment 248 tests · CI on Linux / macOS / Windows squash/rebase-aware detection recoverable deletions (git bundles) 📖 docs

01Live playgroundLIVE

A simulated repo running the real decision engine — src/engine.mjs, bundled into this page, pinned to the CLI by a parity test. Drag the thresholds and watch every branch re-classify instantly.

← oldest · last touch (180d) 150d 120d 90d 60d 30d now

02The CLI sees exactly that

The playground state rendered as the real scan table — same verdicts, same reasons.


  

03Why squash detection is the hard part

Most cleanup tools only check ancestry (git branch --merged), so branches whose commits were rewritten by a squash or rebase merge look unmerged forever. git-cleanup fingerprints the branch's final tree and finds it in base history — with a merge-base guard that ignores net-empty no-op branches.

✓ squash-merged — detected

main squash commit feature/x (commits rewritten away) tip tree found in main history → PRUNE

Commits were squashed into main, so no ancestor link exists — but the tip tree does.

✗ genuinely divergent — kept

main feature/y (own commits) tree never appears in base history → stale/keep

Real work in progress or abandoned — never auto-deleted, always surfaced for review.

04Commands

$ git-cleanup scan
# PRUNE / stale table for every branch,
# with age, merge state, PR status, reason
# --json  --check (exit 2 if dirty)
$ git-cleanup prune
# deletes nothing without confirmation
# merged  -> git branch -d
# squash  -> -D after bundling a backup
# --remote -> git push --delete
$ git-cleanup prs
# stale open PRs (>30d no activity)
# --close closes them with a comment
# opt-in twice: config + flag

05Safety model

VerdictMeaningNeeded to delete
PRUNEmerged into a base branch (ancestry or content) and past the age threshold, or matched an explicit ruleprune + confirmation
staleunmerged & untouched for a long time, or an abandoned PRnever automatic — review in scan
keptprotected, checked out, a base branch, an open PR, or too young

Protected by default: main master develop dev release release/** staging qa trunk. Unmerged work is never deleted unless it matches a mode:"any" force rule you wrote. Any deletion that loses unique commits (squash -D, force rules, remote deletes) is written to a git bundle first — one git fetch restores it.

06Glob matcher

07Configuration

// .gitcleanup.json — found walking up from cwd, merged over
// ~/.config/git-cleanup/config.json, then built-in defaults.
{
  "protected": ["special/release", "vendor/**"],
  "deleteMergedAfterDays": 21,
  "warnUnmergedAfterDays": 45,
  "rules": [
    { "match": "feature/ci-*", "mode": "merged", "minAgeDays": 7 },
    { "match": "tmp/**", "mode": "any", "minAgeDays": 1 }
  ],
  "pr": { "track": true, "staleAfterDays": 30, "closeStaleAfterDays": 60 },
  "remote": { "pruneMerged": true, "deleteAbandonedAfterDays": 0 },
  // Safety net: bundles refs before -D / remote deletions; retainDays > 0 sweeps old ones
  "backup": { "enabled": true, "dir": null, "retainDays": 0 },
  "repos": ["../other-project"]
}

08Run it yourself

$ npm i -g @maliqkara/gitcleanup   # adds `git-cleanup` to PATH
$ cd ~/your/repo && git-cleanup scan
$ git-cleanup prune            # asks before deleting anything
$ npm test                     # 248 unit + integration tests