Skip to main content

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: --delete and an interactive confirm (skip the prompt only with --yes).
  • Protected branches are never candidates: main, master, develop, the current HEAD, 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 to git branch -D).
  • Remote deletion is double-gated: it requires --remote --delete and its own confirmation, and uses git 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:

  1. current branch → keep (current)
  2. protected (default set or --protect) → keep (protected)
  3. merged → delete (merged)
  4. otherwise, if older than --stale → delete (stale <N>d)
  5. 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:unix against a single captured now in milliseconds — no datetime parity 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 safe git branch -d unless you opt into -D with --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)

Source distribution for branchtidy 0.1.0
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)

Table of built distributions (wheels) for branchtidy 0.1.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page