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.
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.
The playground state rendered as the real scan table — same verdicts, same reasons.
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.
Commits were squashed into main, so no ancestor link exists — but the tip tree does.
Real work in progress or abandoned — never auto-deleted, always surfaced for review.
$ 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
| Verdict | Meaning | Needed to delete |
|---|---|---|
| PRUNE | merged into a base branch (ancestry or content) and past the age threshold, or matched an explicit rule | prune + confirmation |
| stale | unmerged & untouched for a long time, or an abandoned PR | never automatic — review in scan |
| kept | protected, 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.
// .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"] }
$ 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