Clean Git Repo
Report and clean up local branches, linked worktrees, stale remote-tracking
refs, worktree metadata, and tags. Run from either:
- a worktree or Git directory to handle one repository; or
- a worktree container whose immediate child directories are worktrees.
In container mode, the script groups children by their resolved Git common
directory and handles each repository once. It does not recurse beyond immediate
children. A bundled script does the deterministic detection and deletion; you
orchestrate the safety gates.
Resolve
scripts/clean-git-repo.sh to an absolute path once and run it from
the target repository or worktree container, not the skill directory. In Claude
Code:
SCRIPT="${CLAUDE_SKILL_DIR}/scripts/clean-git-repo.sh"
.
bash
"$SCRIPT" # report only (dry run)
"$SCRIPT" --apply # delete the SAFE tier
The script's
lists every flag; the key ones appear below.
Safety model: confirm unless the action is safe
| Tier | Actions | Confirmation |
|---|
| Safe | (remote-tracking refs), , delete merged branches (tip provably in the merge), first removing an attached clean linked worktree with only disposable ignored files and without | none (apply + report) |
| Confirm | delete MERGED-but-advanced (), GONE (), or STALE () branches and any attached safely removable linked worktrees; matching remote branches whose remote tip is identical or provably merged (); local tags absent from the remote (); () | ask the user first |
| Never | the current branch, default branch, matches, and main, dirty, locked, detached, unavailable, or submodule-containing worktrees; worktrees with other ignored files | excluded by the script |
A linked worktree follows its branch's tier when it has no tracked or untracked changes, submodules, or non-disposable ignored files. Ignored content in
and
directories at any depth is disposable; other ignored files remain protected.
On apply, the script rechecks those conditions, runs
without
, and deletes the branch only if removal succeeds. Disposable ignored files are removed with the worktree. The script never force-removes a worktree.
A branch is
merged when GitHub (
) reports its PR merged, or its tip is
an ancestor of the base, so squash- and rebase-merges are caught, not just
fast-forwards. A squash-merged branch auto-deletes only when its tip is
contained in the merged PR head; if the local tip advanced past it (extra
commits that never reached the PR, or a merged head not fetched locally), it
drops to the
confirm tier instead of being force-deleted.
Workflow
Copy this checklist and work through it:
Step 2: report
bash
"$SCRIPT" # default 90-day stale threshold
"$SCRIPT" --age-days 180 # adjust the threshold the user confirms
Read the output. It groups branches as
SAFE TO DELETE — merged,
CONFIRM —
merged, local tip advanced,
CONFIRM — gone upstream,
CONFIRM — stale >
Nd,
CONFIRM — matching remote branches,
REMOTE SKIP,
CONFIRM — local
tags absent from <remote>,
PROTECTED, and a kept
ACTIVE count.
upstream alone is
not proof of a merge: it can also be a
closed-unmerged PR. GONE is therefore always a confirm tier; each gone entry
shows how many commits it holds ahead of the base, so you can see what a delete
would discard. Each stale entry carries its push-state (
never pushed — local only
,
, or
), so you can see
at a glance whether deleting it discards work that exists nowhere else. With
, review both remote sections: remote branches delete only when the
remote-tracking tip is identical to the local branch being deleted or
independently proven merged; otherwise they are reported as skipped. A branch
entry with
also removes that safely removable linked
worktree when its tier is applied. Container mode prints a header for each
distinct Git common directory.
Step 4 & 5: apply
bash
"$SCRIPT" --apply # SAFE only (merged branches)
"$SCRIPT" --apply --force-merged # + delete merged branches whose tip advanced past the PR
"$SCRIPT" --apply --force-gone # + delete GONE branches
"$SCRIPT" --apply --stale --age-days 180 # + delete STALE branches
"$SCRIPT" --apply --remote # + delete matching remote branches after lease check
"$SCRIPT" --apply --prune-tags --gc # + prune listed local tags absent from the remote, reclaim space
Other flags:
to override default-branch detection,
to protect extra branches (repeatable),
for offline,
to skip GitHub lookups.
Graceful degradation
- No / not authenticated → squash-merge detection falls back to
+ ancestor checks; the report says so. Branches squash-merged with
their remote still present may land in ACTIVE instead of SAFE. Note this to
the user.
- No remote → fetch and are skipped; only local-merged and stale
detection run.
- Protected worktree: main, dirty, locked, detached, unavailable, and submodule-containing worktrees are kept, as are worktrees with non-disposable ignored files.
- No repository at the current directory → immediate child worktrees are
grouped by Git common directory. If none exist, the script exits without
acting.
- Base-remote fetch fails (offline, or auth error on the remote that backs
the tiers) → the run continues on local refs and the summary prints
⚠ fetch failed — gone/stale tiers computed from possibly-stale local refs
. Failures
of other remotes (e.g. a dead fork) are ignored and never trip that warning.
- The script is idempotent: re-running after an apply is safe and shows the new
state.
Verification
The dry run is the review gate. Always run it before
. After applying,
re-run with no flags; the tiers you acted on should be empty and
protected/active branches and worktrees unchanged.
and
git worktree list --porcelain
independently confirm deletions. In container mode,
verify each repository header from the original report; a repository with no
remaining child worktree no longer appears in a later container scan, so run
verification from its recorded Git common directory.