A merged pull request does not mean the local branch on your laptop is empty.
I kept learning that the hard way. GitHub showed the PR as merged. The remote branch was already gone. git branch --merged looked clean enough. Then I found two commits that had never left my machine; written after I clicked merge, or left behind by a squash.
git branch -D feature/foo deletes that branch. That is all it does. It does not tell you whether the work is gone, and it does not find the other twelve leftovers sitting next to it. You can pass more names, but you still have to choose them.
So I built GitDeclutter: a small Go CLI that finds stale local branches, tells you which ones are safe, and can delete that SAFE set in one pass.
Know what's safe to delete before you delete it.
Official repo: github.com/kunmi02/git-declutter
This is not the older Python project of the same name. That one turns a folder of copied files into a Git repo. This one classifies stale local branches.
The failure mode git branch --merged misses
Squash-and-merge is the usual trap. GitHub records a new commit on main. Your local feature branch still has the original commits. Ancestry no longer matches, so a naive "is this merged?" check is either too aggressive or too timid.
The other trap is quieter:
PR merged at C
A──B──C
Local branch later becomes:
A──B──C──D──E
The pull request is done. Commits D and E may exist only on your laptop. Deleting the branch throws them away.
GitDeclutter treats a merged PR as evidence, not as permission.
What it actually does
git declutter scan never deletes anything. It reads local Git history, remote-tracking state, and — when available — GitHub or GitLab pull request metadata. Then it puts every local branch in one of four buckets:
- SAFE — strong evidence that deleting it will not lose unique work
- REVIEW — it looks stale, but the evidence is incomplete
- KEEP — unique or active work is still there
- PROTECTED — current branch, default branch, a worktree, or a configured pattern
False negatives are acceptable. False positives are not. If it is unsure, it will not recommend automatic deletion.
A scan looks like this:
$ git declutter scan
GitDeclutter
Repository: payments-api
SAFE TO REMOVE 12
✓ feature/oauth
PR #418 merged · remote deleted · no local changes
NEEDS REVIEW 3
⚠ experiment/cache
Remote deleted · no merged PR found
KEEP 4
✕ feature/refunds
3 commits exist only locally
PROTECTED 3
🔒 main
🔒 release/*
If you disagree with a row, ask for the reasons:
git declutter why feature/refunds
You get the signals, not a yes/no from a script you cannot audit.
Cleanup is optional, bulk, and recoverable
git branch -D is a delete command. GitDeclutter is a decide-then-delete command.
When you are ready, git declutter clean lists every SAFE branch and lets you remove them in one pass — interactively, or all at once with --safe-only --yes. That is the bulk path: you are not typing twelve branch names into git branch -D. REVIEW, KEEP, and PROTECTED never go on that path.
Before it deletes, it stores a recovery ref (refs/git-declutter/recovery/...) for 30 days by default. git declutter restore <branch> brings it back. Use --permanent only when you mean it.
It also re-checks the branch immediately before deletion. If the tip moved after the scan, that branch is skipped.
Install
You need Git. You do not need Go after the binary is on your PATH.
go install github.com/kunmi02/git-declutter@latest
git declutter scan
GOBIN (or $(go env GOPATH)/bin) must be on your PATH. Prebuilt macOS, Linux, and Windows archives will live on GitHub Releases once version tags are published.
GitHub token via gh or GITHUB_TOKEN, GitLab via glab or GITLAB_TOKEN. Offline mode exists if you do not want provider calls:
git declutter scan --offline
There is no GitDeclutter account and no telemetry. Provider APIs are used only for repo metadata (owner, branch names, SHAs, PR state) — not for source, diffs, or commit messages.
Why I did not just wrap git branch -D
git branch -D is the right tool when you already know the name and accept the loss. It will not scan the repo, classify leftovers, or give you a recoverable bulk cleanup of only the stale ones.
The gap is the decision, then the list. I wanted something that would rather leave a stale branch sitting there than pretend a squash merged, locally-extended branch was empty and, when the evidence is strong, let you clear the SAFE ones in one go.
If that is a problem you have, start with a scan:
git declutter scan
Source, issues, and install: https://github.com/kunmi02/git-declutter
Top comments (0)