branchtidy
Delete merged & stale git branches — safely. branchtidy finds the local (and
optionally remote) branches that are already merged, or haven't seen a commit in
N days, previews them, and deletes them in one batch. It is dry-run by
default, refuses to touch main / master / develop / your current branch,
and won't nuke unmerged work unless you explicitly ask.
Zero dependencies (pure Python stdlib). Zero config. No daemon, no account.
pipx run branchtidy
branchtidy local branches · default main · stale > 90d
BRANCH LAST COMMIT MERGED ACTION
feature/login 12d ago yes delete (merged)
feature/old-poc 210d ago no delete (stale 210d)
main 2d ago no keep (protected)
feature/wip 3d ago no keep (active)
Dry run. 2 branch(es) WOULD be deleted. Re-run with --delete to apply.
Nothing was deleted. That's the point — you read the table, then decide.
Why another branch cleaner?
Everyone reinvents this as a throwaway git branch --merged | grep -v ... | xargs
one-liner, and those one-liners are exactly how people delete branches they
wanted. branchtidy's whole pitch is safety + zero config:
- Dry-run is the default. No flags → it only prints what it would do.
- Real deletion is gated twice:
--deleteand an interactive confirm (skip the prompt only with--yes). - Protected branches are never candidates:
main,master,develop, the currentHEAD, plus anything you pass to--protect. - Merged vs unmerged is respected. Merged branches use the safe
git branch -d. Unmerged branches are only deletable with an explicit--force(which maps togit branch -D). - Remote deletion is double-gated: it requires
--remote --deleteand its own confirmation, and usesgit push <remote> --delete.
When in doubt, branchtidy keeps the branch.
Install
pipx run branchtidy # no install, run on demand
pip install branchtidy # or install the `branchtidy` command
There's an identical Node build too: npx branchtidy / npm i -g branchtidy
(see branchtidy). Both ports share one
selection-vector table, so they make byte-for-byte identical decisions.
Usage
branchtidy [options] # dry-run preview (default — deletes nothing)
branchtidy --delete # actually delete, after a confirm
| Option | Description |
|---|---|
--delete |
Perform deletion. Without it, branchtidy only previews. |
--yes |
Skip the interactive confirm (use with --delete, e.g. in scripts). |
--stale <dur> |
Staleness threshold. Default 90d. Accepts 30d, 2w, 12h, 45m, 30s, or a bare number (days). |
--merged-only |
Only delete merged branches; never delete on age alone. |
--remote [name] |
Operate on remote-tracking branches (default remote: origin). |
--protect <a,b> |
Extra branch names to never delete (comma-separated). |
--force |
Allow deleting unmerged branches (maps to git branch -D). |
--json |
Machine-readable output; never prompts, never deletes (preview only). |
--no-color |
Disable ANSI colors. |
-h, --help |
Show help. |
-v, --version |
Print version. |
Exit codes: 0 success/clean, 1 one or more deletions failed, 2 usage or
environment error (e.g. not a git repo).
Examples
# what WOULD be cleaned up, right now?
branchtidy
# stricter window, only merged branches, do it (with a confirm)
branchtidy --stale 30d --merged-only --delete
# clean up gone-stale remote branches on origin (double-gated + confirm)
branchtidy --remote origin --delete
# delete unmerged stale branches too — you have to ask for it
branchtidy --stale 180d --delete --force
# protect a couple of long-lived branches by name
branchtidy --protect release/v1,staging --delete
# pipe the plan somewhere
branchtidy --json | jq '.toDelete'
How it decides
For each branch branchtidy looks at: is it the current HEAD? is it protected?
is it merged into the default branch? how old is its last commit? Then:
- current branch → keep (
current) - protected (default set or
--protect) → keep (protected) - merged → delete (
merged) - otherwise, if older than
--stale→ delete (stale <N>d) - otherwise → keep (
active)
In --merged-only mode, step 4 is skipped entirely — age never causes a
deletion.
The default branch is resolved from origin/HEAD when available, otherwise it
falls back to main, then master.
Design notes
- One pure function at the core.
select_branches(branches, policy, now_ms)has no git, no fs, no clock — it's a pure data→data transform that returns{toDelete, toKeep}with a reason on every branch. The CLI is a thin git wrapper around it. That's what makes the Node and Python ports verifiably identical: they run the same vector table. - Time is integer math. Ages are computed from
committerdate:unixagainst a single capturednowin milliseconds — nodatetimeparity to worry about between languages. - Safe by construction. Protected and current branches are filtered out
before any staleness logic runs, the staleness test is a strict
>(a branch exactly at the threshold is kept), and deletion always passes through the safegit branch -dunless you opt into-Dwith--force.
License
MIT
Metadata
Release files for branchtidy 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| branchtidy-0.1.0.tar.gz | 14.5 kB | Without provenance | Details for branchtidy-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| branchtidy-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for branchtidy-0.1.0-py3-none-any.whl |
Total release size: 26.3 kB
Release files / branchtidy-0.1.0.tar.gz
| Download URL | branchtidy-0.1.0.tar.gz |
|---|---|
| Size | 14.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
50c24a9d0193594d001379f533dbd922213b706916d6ccb3028dd2f85d3a6a76
|
|
BLAKE2b-256 checksum How to use checksums |
a3fd3a38c3d74d44e02a2f1c6a540f596524af0748e427be8388d019c835dd6f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.7
|
Release files / branchtidy-0.1.0-py3-none-any.whl
| Download URL | branchtidy-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a026ba2f6bc46216047ace817e781d63658171de9150cec4944c5ef6cbb02ed2
|
|
BLAKE2b-256 checksum How to use checksums |
48fd3f56c0edcfa2a3d08f110c1a38e1fb32365159276a07faf6087bd4bad830
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.7
|
Security for branchtidy 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.