Skip to content
Back to skills

Ai Git Cleanup

ASecurity

Removes finished local git work in one automatic pass, with no question and no name to type: dead worktrees first (`git worktree remove`, then `git worktree prune`), then branches already merged into the local `main`, then the branches a squash-merged pull request left behind (tree identical to `origin/main`, tagged before deletion). Trigger for "tidy up", "tidy up these branches", "tidy branches", "delete old branches", "start fresh", "limpieza de branches", "limpia branches", or by naming a...

  • 60 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 29, 2026
ai-agentsgoshellnodegit

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 7, 2026

npx -y skills add arcasilesgroup/ai-engineering --skill ai-git-cleanup --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ai Git Cleanup?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Ai Git Cleanup
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/arcasilesgroup-ai-git-cleanup/badge)](https://www.skillsdirectory.com/skills/arcasilesgroup-ai-git-cleanup)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: ai-git-cleanup
description: >-
  Removes finished local git work in one automatic pass, with no question and
  no name to type: dead worktrees first (`git worktree remove`, then `git
  worktree prune`), then branches already merged into the local `main`, then
  the branches a squash-merged pull request left behind (tree identical to
  `origin/main`, tagged before deletion).
  Trigger for "tidy up", "tidy up these branches", "tidy branches", "delete
  old branches", "start fresh", "limpieza de branches", "limpia branches", or
  by naming ai-git-cleanup. A branch that is not merged into `main` is left
  alone — unless its tree is byte-identical to `origin/main`, which is what a
  squash-merged pull request looks like afterward; the pass deletes those with
  the proof in hand, tags the tip, and never force-deletes anything else.
  Not for pushing, not for remote branches, not for opening a pull request.
license: Apache-2.0
---

# ai-git-cleanup — remove finished local work in one pass

Say a cleanup phrase and the pass runs itself. It deletes only work that is
finished — worktrees whose branch is merged into the local `main`, those merged
branches, and the branches a squash-merged pull request left behind (their
content provably already in `origin/main`) — and it never asks a question and
never takes a name. Anything unfinished, unprovable, or detached is left exactly
where it is. Run the steps in order, from the primary checkout, and report what
each one did.

## The pass

1. **Remove the dead worktrees.** The loop walks the session worktrees only —
   the primary checkout is never a candidate, since `git worktree remove <repo>`
   refuses it as a main working tree (and the pass leaves the tree you are
   standing in alone). For each worktree, read two facts before
   touching it: whether its HEAD is on a branch
   (`git -C "<path>" symbolic-ref -q HEAD`) and what uncommitted content it holds,
   ignored files included
   (`git -C "<path>" status --porcelain --ignored --untracked-files=normal` — the
   option is required: a `status.showUntrackedFiles=no` in the repository or the
   global config makes the bare command print nothing at all, so the read must
   force the listing itself; `normal` keeps a wholly ignored directory collapsed
   to one `!! dir/` line, so the report stays bounded and matches the list's
   `dir/` entries literally). Remove
   it with `git worktree remove -- "<path>"` only when the HEAD check names a branch
   that is already merged into the local `main`, the status read printed no
   warning, **and** every status entry is
   either absent or an ignored path on the regenerable list below. Otherwise skip
   it and report it kept with the reason:
   - **No branch (detached HEAD).** `git worktree remove` exits 0 on a clean
     detached worktree and the commits it holds become unreachable from every
     ref, so the pass never removes one: report it kept as "detached, no branch
     to prove".
   - **Uncommitted content that is not regenerable.** Tracked and untracked
     content always keeps the worktree, and so does any ignored path outside the
     list below — the removal is never forced, and git destroys ignored content
     silently (a `.env`, ignored screenshots). Report it kept and say what the
     status listed.
   - **Unreadable status.** `git status` exits 0 while warning on stderr when it
     cannot read a subdirectory or a path, so a read that warned is not a clean
     worktree: keep it and report the warning. The doubt is resolved by keeping,
     never by removing what the read did not see.
   - **Ignored content that is all regenerable.** Two groups, and nothing else:
     the paths this framework rebuilds — `node_modules/`, `dist/`, `coverage/`,
     `.stryker-tmp/`, `reports/`, `.ai-engineering/receipts/`,
     `.ai-engineering/cache/`, `.ai-engineering/workflow/playwright/` — plus the
     system and editor noise this repository already ignores in `.gitignore`:
     `*.bun-build`, `.claude/reviews/`, and `*.DS_Store` (macOS writes a
     `.DS_Store` into every directory a Finder window or an app touches, so a
     merged worktree whose only ignored content is one must still be removed).
     They are build, runtime, and machine output, never data, so they do not keep
     a worktree: remove it and name every ignored path dropped. The ceiling is
     that list — it is exactly what this framework regenerates and what the
     repository ignores as noise, so anything else ignored is treated as data and
     keeps the worktree.
     A match is anchored to the worktree root, never a substring or a bare name:
     the printed path must be the entry itself, or sit inside a listed directory,
     so a listed directory is taken whole (`dist/` covers `dist/out.js`, and
     `node_modules/` covers everything under it). A lookalike is not the entry:
     `packages/x/dist/out.js` is not `dist/`, and `reports-archive/` is not
     `reports/`. The two `*`-prefixed entries are the exception to the whole-path
     rule — they are suffix patterns matched against the path's last component at
     any depth, so `*.DS_Store` matches `sub/.DS_Store` and `*.bun-build` matches
     `app.bun-build`. The read collapses a wholly ignored directory to one
     `!! dir/` line, so the report names the directories it drops; the ceiling is
     that a listed directory goes whole — a hand-written file inside `dist/` is
     dropped with it, unlisted, and only the paths git prints are ever seen.
   Never force a removal.
2. **Prune the stale ones.** Run `git worktree prune` to drop registrations
   whose directory is already gone. It removes no live worktree.
3. **Delete the merged branches** (needs git ≥ 2.56.0). Run `git branch --delete-merged refs/heads/main` — it deletes only branches whose upstream is
   the local `main` and that are fully merged into it, and never the branch
   checked out here. The option exists only from git 2.56.0: on any older git it
   fails with an `unknown option` error, and that is not a pass failure — steps 1
   and 2 have already run — so report it and continue to step 4 for every merged
   branch.
4. **Fall back for a merged branch without that upstream.** List the candidates
   with `git branch --merged main`, then drop the current branch and any branch
   checked out in another worktree (the lines decorated `*` or `+`) and read each
   remaining name without its decoration. Delete each with
   `git branch -d -- "<branch>"`. The name goes in as one quoted argument that is
   data, never re-typed into a shell line: a branch name may legally carry
   metacharacters (`$(…)`, `;`, `|`, spaces), and an unquoted interpolation executes
   them. `--` ends the options, so a name like `-r` cannot be read as a flag. A
   refusal is a keep, and it is never retried: `-d`
   tests the branch against its own upstream, so a branch merged into `main`
   whose upstream points elsewhere (a lagging `origin/main`, a
   `refs/heads/behind`) is refused — report that branch kept and name the
   upstream mismatch as the reason.

5. **Reconcile branches a squash landed.** `git branch --merged main` misses the
   commonest end state of a pull request: GitHub squash-merges it into one commit
   whose ancestry contains nothing from the branch, so `git branch --merged main`
   never lists it and the branch looks "unmerged" forever. Detect it instead of
   guessing: a branch (never the current one, never one checked out in a worktree)
   whose tip `git diff --quiet <branch> origin/main` reports **byte-identical to
   `origin/main`'s tree** has landed — nothing the branch holds is missing from the
   remote. Tag the tip first (`git tag archive/<branch> <branch>`) — the tag is
   the receipt that the history stayed reachable — then delete with
   `git branch -D -- "<branch>"`. This is the pass's only force-delete, it is
   earned by the whole-tree comparison, and it never applies to a branch whose
   tree differs from `origin/main`: that one still carries something the remote
   does not have — leave it alone and report it as parked (never merge it, never
   re-open a pull request for it: that decision is the human's, on their ask).
   Also refuse the whole step when `origin/main` cannot be fetched or the remote
   is not configured: without a fetched `origin/main` there is no proof, and
   nothing deletable.
   After any deletion, check whether the local `main` can still be fast-forwarded:
   if a squash landed, the local `main`'s history was bypassed and only a human's
   reviewed `git reset --hard origin/main` levels it — report that state, never
   reset on the pass's own initiative.

## Safety invariants

- **Never force-delete anything without a proof in hand.** Worktree removals
  are never forced: a refusal is the correct outcome and is reported as kept,
  never retried with force. The one force-delete the pass may run is step 5's
  `git branch -D` on a branch whose tree is byte-identical to `origin/main`,
  tagged first — without that proof, `-D` does not exist for this pass.
- **An unmerged branch is never deleted — by ancestry.** Steps 3–4 delete only
  branches whose commits are in `main`'s ancestry; a branch with work not in
  `main` is left alone. The one exception is step 5's whole-tree proof: a branch
  whose tree is byte-identical to `origin/main` holds nothing the remote lacks,
  however unmerged its ancestry looks.
- **A worktree with no branch is never removed.** A detached HEAD has no branch
  to prove merged, so the pass keeps that worktree and reports it as detached:
  its commits never lose their only ref.
- **Uncommitted content keeps its worktree — except regenerable output and
  ignored noise.** Tracked, untracked, and ignored-but-not-regenerable content
  all keep it; only the named build/runtime paths and the system/editor noise the
  repository already ignores may be dropped, and every dropped path is reported.
- **`ai-eng worktree rm` is the explicit human path.** A person typed it, or the
  close ran it after the merge landed, so it removes what this pass keeps — a
  worktree holding ignored content is deleted by the verb, not by this pass — and
  it prints the ignored paths it drops. The pass is the automatic path and errs
  on keeping; the verb answers a named worktree its caller asked to remove.
- **No name is ever asked, and there is no interactive question.** The pass
  decides from git state alone; it never prompts for a branch or worktree name.
- **The local `main` is the only destination that matters.** No remote is
  read for the merge decision and nothing is ever pushed.
- **No deletion starts before the worktrees holding a merged branch are
  gone.** A branch checked out in a worktree cannot be deleted; removing that
  worktree first is what lets the pass finish.

## Lifecycle

Lane: any
Writes: nothing
Read by: the person who asked for the cleanup
Dies: after each pass; the report is a snapshot of that moment
Next: none

Files in this skill

  • SKILL.md9.9 KB
  • scripts/analyze.mjs22 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.