I ran git branch on a repo I'd been on for a year and counted forty-seven local branches. Maybe six were alive. The rest were feature/* that shipped months ago, fix/* whose PRs merged and whose remotes were deleted the same minute, and a couple of experiments I'd genuinely forgotten writing.
The usual advice is:
git branch --merged | grep -v '\*' | xargs git branch -d
I've pasted that snippet into a dozen shells. It has three problems: it misses branches whose upstream is gone (deleted remote, but the local sticks around forever), it cheerfully includes main if you forget the grep, and it makes you the safety check.
So I built branchreap — it finds the branches that are actually safe to reap, and only those.
npx branchreap # scan, read-only
# or
pip install branchreap
What it reaps — and what it refuses to
Reaps:
- merged branches (fully merged into your default branch)
- gone branches (upstream deleted), as long as they carry no unmerged commits
Never touches:
- the current branch
- the default branch (
main/master/ whateverorigin/HEADsays) - anything unmerged / unpushed, unless you explicitly pass
--include-unmerged(and even then it still won't delete current or default)
That safety floor is the whole design. Run it without reading a word of docs and the worst case is it deletes a branch git itself already considers merged.
What it looks like
$ branchreap
default branch: main
✗ feature/old-login merged merged into main
✗ fix/typo gone upstream gone · merged into main
────────────────────────────────────────────────────────
2 reapable (1 merged · 1 gone) · 4 kept
1 gone branch(es) held back (unmerged commits). Use --include-unmerged to reap.
$ branchreap clean # asks first
$ branchreap clean --yes # for a shell alias
--dry-run previews, --json pipes, --fetch runs a git fetch --prune first so the gone status is fresh (git only marks upstreams gone after a prune).
Why another branch cleaner?
There are a few good ones (git-sweep, git-trim), but most are single-purpose — merged-only or remote-only — and several are unmaintained. branchreap does merged and gone in one pass, leads with safety, and is genuinely zero-dependency: it just shells out to git and parses the output. Same logic ships as a Node package and a Python package (byte-for-byte aligned), so npx branchreap and pipx run branchreap both work with nothing to install.
Try it
npx branchreap # Node
pipx run branchreap # Python
- npm: https://www.npmjs.com/package/branchreap
- PyPI: https://pypi.org/project/branchreap/
- GitHub: https://github.com/jjdoor/branchreap
How many local branches are in your busiest repo right now? Run git branch | wc -l and tell me — I bet it's higher than you'd guess. 👇
Top comments (0)