Skills

stale-branch-cleaner

Development v1 Emdly recommended
@launifycorp 4 installs updated today MIT license

Stale Branch Cleaner

Audits the branches of a git repository, ranks them by how likely they are to be dead, presents the candidates for deletion, and deletes only what the user explicitly confirms — locally, on the remote, or both.

Deletion is destructive and, on a remote, effectively public. This skill NEVER deletes anything before an explicit confirmation, and never widens the scope it was given.

When to use

  • "clean up my branches", "which branches can I delete", "prune stale branches"
  • "my repo has 60 branches and I don't know which are dead"
  • Before a repo handover, an audit, or a release cleanup.

Do not use it to delete a branch the user already named — that is a one-line git command, not an audit.

Inputs to establish first

Ask only for what you cannot detect. Detect the rest.

  • Repository path — default: the current working directory. Refuse to continue if it is not a git work tree.
  • Staleness threshold — default: 90 days since the last commit. State the default; let the user override.
  • Protected branches — never proposed for deletion under any circumstance: main, master, develop, dev, staging, production, prod, release, release/*, HEAD, the current branch, and the remote's default branch. Add anything the user names.

Steps

  1. Refresh the view of the remote (read-only, safe): git fetch --all --prune --quiet --prune only cleans up local tracking refs for branches already deleted on the remote; it deletes no branches. Say so if the user asks.

  2. Collect the facts in one pass, so every judgement rests on real data:

    git for-each-ref --sort=committerdate \
      --format='%(refname:short)|%(committerdate:iso8601)|%(authorname)|%(upstream:track)' \
      refs/heads refs/remotes
    

    For each branch also determine:

    • days since last commit — from the date above;
    • merged or not: git branch --merged <default-branch> (local) and git branch -r --merged origin/<default-branch> (remote);
    • unmerged commits that would be lost: git log --oneline <default-branch>..<branch> | wc -l;
    • whether a local branch still has a live remote counterpart, and vice versa.
  3. Classify each non-protected branch:

    Class Meaning Default proposal
    MERGED fully merged into the default branch propose deletion at any age
    STALE older than the threshold, has unmerged commits propose, flagged with the commit count
    GONE local branch whose upstream is gone ([gone]) propose local deletion
    ACTIVE newer than the threshold keep, do not list among candidates

    Never treat "no commits recently" as "abandoned" on its own — a long-lived release or hotfix branch can be both old and load-bearing. Anything with unmerged work is flagged, not quietly bundled in.

  4. Present the list and stop. Group by class, most confident deletions first. For each branch show: name, age, last author, merged status, unmerged commit count, and where it exists (local / origin / both). Show the totals. Then state explicitly that nothing has been deleted yet.

  5. Ask for the scope — this is a separate question from which branches, and it has no default:

    • local only — safe, reversible, affects nobody else;
    • origin only — affects everyone who uses the repo;
    • both.

    Also ask whether to delete all proposed branches or a subset, and accept a subset by name or number.

  6. Confirm before acting. Restate the exact branch names and the exact scope, then wait for a clear yes. Treat silence, "ok whatever", or an ambiguous reply as "no". If the scope includes origin, say plainly that remote deletion affects other people and that recovery requires someone to still hold the commit.

  7. Delete only what was confirmed, one branch at a time, reporting each result:

    • local, merged: git branch -d <branch>
    • local, unmerged (only if the user confirmed knowing the commit count): git branch -D <branch>
    • remote: git push origin --delete <branch>

    Before deleting anything unmerged, print its tip SHA so it can be recovered with git checkout -b <name> <sha>. Recovery is possible while the commit is still in the reflog or held by another clone.

  8. Report: deleted, skipped, failed (with the error). If any deletion failed — branch protection rules, no push rights, an open PR — say which and why, and do not retry with a more forceful command.

Rules

  • Never delete without an explicit confirmation naming the scope. No exceptions, including when the user says "just clean it up".
  • Never git push --delete a protected branch, even if asked; say why and stop.
  • Never force-delete (-D) a branch with unmerged commits unless the user confirmed after seeing the commit count.
  • Never rewrite history, reset, or touch the working tree — this skill only fetches, reads refs, and deletes branches.
  • If the repo has uncommitted changes, say so, but do not block: branch deletion does not touch them.
  • If a branch has an open pull request, mention it and exclude it from the default proposal.

Output format

Repository: <path>   Default branch: <name>   Threshold: <N> days
Scanned: <N> local, <N> remote   Protected: <list>

MERGED — safe to delete (<n>)
  feature/login-form      142d   J. Novak    merged      local+origin
  ...

STALE — unmerged work (<n>)
  spike/graphql           210d   M. Dvorak   3 commits ahead   origin only
  ...

GONE — upstream deleted (<n>)
  fix/typo                 88d   J. Novak    [gone]      local only

KEEPING: <n> active branches

Nothing deleted yet. Delete which branches, and where — local only, origin only, or both?

After deletion:

Deleted (local): <list>
Deleted (origin): <list>
Skipped: <list with reasons>
Failed: <list with errors>
Recovery: <branch> tip was <sha> — git checkout -b <branch> <sha>

License

MIT