Skip to content
Security
Command

/git-cleanup

Safely analyzes and cleans up local git branches and worktrees, categorizing them as merged, squash-merged, superseded, or active work before deleting anything.

From plugin
skills
7.9k8 skills30 agents8 commands1 MCP
> /plugin marketplace add trailofbits/skills

Command definition

git-cleanup.md
description: "Safely analyzes and cleans up local git branches and worktrees, categorizing them as merged, squash-merged, superseded, or active work before deleting anything."
argument-hint: "[repo-path]"
disable-model-invocation: true
allowed-tools: Bash Read AskUserQuestion Workflow

Git Cleanup

Clean up accumulated git worktrees and local branches. A dynamic workflow gathers the evidence in parallel and tries to disprove its own delete recommendations; you keep the two safety gates and run the deletions yourself.

Repository: `$ARGUMENTS` — when empty, use the current working directory.

Core Principle: SAFETY FIRST

**Never delete anything without explicit user confirmation.**

The split between the workflow and this session is the safety property, not an implementation detail:

| Runs in the workflow (subagents) | Runs here (main session) | |----------------------------------|--------------------------| | Read-only inspection of git state | Both user gates | | Merge-evidence investigation | Every `git branch -d/-D` | | Refutation of delete candidates | Every `git worktree remove` |

Workflow agents run in the background with no way to reach the user. Nothing destructive may move into the script — if it did, deletions would happen while the user was still being asked about them.

Phase 1: Run the Analysis Workflow

`${CLAUDE_PLUGIN_ROOT}` is set in the Bash tool's environment, not in this prompt's text — nothing expands it for you here. **Resolve it first**, in the same call that finds the repo root:

echo "$CLAUDE_PLUGIN_ROOT"
git rev-parse --show-toplevel

Then call the `Workflow` tool with `scriptPath` set to `<that plugin root>/workflows/analyze-branches.js` and this as `args`, both values substituted rather than passed as the literal `${CLAUDE_PLUGIN_ROOT}`:

{ "repoPath": "<absolute path to the repo>", "pluginDir": "<that plugin root>" }

This command being invoked is the opt-in that workflow needs.

It runs three phases — survey, investigate, refute — and returns:

| Field | Meaning | |-------|---------| | `deleteCandidates` | Recommended deletions. Each carries `evidence`, the exact `command` (`-d` or `-D`), `worktreePath` when a worktree holds the branch, `group` for related-branch display, and `verifyWith` on `SAFE_TO_DELETE` entries. | | `needsReview` | Remote gone, work not found in the default branch. Never recommend these. | | `keep` | Unpushed, local-only, or synced with a live remote — plus `PROTECTED` entries, excluded from analysis but still reported. | | `worktrees` | Path, branch, `dirty`, `dirtyFiles`, and whether the branch is stale. | | `unanalyzed` | Branches no verdict came back for. **Must be shown to the user.** |

**Exit criteria:** you hold a result object, or the workflow threw.

If it throws with "survey returned zero local branches", the inventory failed — say so and stop. Do not report a clean repository.

If it throws about an unreadable `scriptPath`, the path did not resolve — most likely `$CLAUDE_PLUGIN_ROOT` was empty or reached the tool unexpanded. Check what the `echo` above printed, and if there is no usable plugin root, take the inline fallback below rather than aborting the run.

**Fallback.** If the `Workflow` tool is unavailable, do the same analysis inline: read [merge-evidence.md](../references/merge-evidence.md), gather the state below, and apply the decision table in [Phase 2](#phase-2-check-the-workflows-work). It is slower and the refutation pass is on you, but the categories and the gates are identical.

# Assign, then default with ${:-}. Do not fall back with `... | sed ... || echo main`:
# a pipeline's status is the last command's, sed succeeds on empty input, so the ||
# never fires and default_branch ends up empty — every command below then silently
# operates on "".
default_branch=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)
default_branch="${default_branch#origin/}"
default_branch="${default_branch:-main}"
git fetch --prune
git branch -vv                        # tracking info and [gone] markers
git branch --merged "$default_branch"
git worktree list --porcelain
git log --oneline "$default_branch" | grep -iE "#[0-9]+" | head -40

Phase 2: Check the Workflow's Work

The workflow reports evidence so you can audit it, not so you can forward it unread. Before building the gate-1 table:

1. **Every `deleteCandidate` names specific evidence** — a PR number, a commit sha, or a superseding branch. "Similar name", "looks stale", or an empty evidence string is not a delete recommendation. Move it to needs-review. `SAFE_TO_DELETE` entries satisfy this by naming the tip commit; they also carry `verifyWith`, which phase 3 runs before deleting. 2. **No protected branch is a delete candidate.** The script filters long-lived integration and environment names (`main`, `master`, `develop`, `dev`, `staging`, `production`, `qa`, `uat`, `release/*`, `hotfix/*`, and similar), plus the repository's actual default branch and the current branch, programmatically. They do still appear — under `keep` with category `PROTECTED`, and with their unpushed count when they have one. That is deliberate: excluded from analysis is not the same as absent from the report, and a `staging` branch carrying unpushed commits must not vanish. If one reaches `deleteCandidates` or `needsReview` regardless, drop it and say so. 3. **`unanalyzed` is empty, or you list it.** A partial run must not read as a complete one. 4. **Dirty worktrees are flagged**, whatever their branch's category.

The categories, and what has to be true for each:

| Category | Meaning | Delete Command | |----------|---------|----------------| | SAFE_TO_DELETE | Reported by `git branch --merged`, re-checked by `verifyWith` at execution | `git branch -d` | | SQUASH_MERGED | Work incorporated via squash merge, PR or commit named | `git branch -D` | | SUPERSEDED | Work verified in main via PR, or contained in a named newer branch

Read more
@trailofbits
Ships withskills

Trail of Bits skills and plugins for AI-assisted security analysis, testing, and development. Install this marketplace in Claude Code or Codex, or import it into your ChatGPT workspace marketplace.

7.9k95 skills30 agents1 MCP
Get the whole plugin

Stars on GitHub

Momentum

stars in total
7.9k
Star history of trailofbits/skills on GitHub, a chart by star-history.com

Other commands on skills.

audit

audit

Command

Audit a file, directory, or whole repo for insecure default configuration: fallback secrets, default credentials, fail-open…

@trailofbits@trailofbitsInstall Command

More commands from other plugins.

deslop-reborn

deslop-reborn

Command

One iteration of the IronClaw Reborn de-slop loop — take ONE Reborn crate, fan out parallel review sub-agents (thermo-nuclear…

@nearai@nearaiInstall Command
arsenal

arsenal

Command

Show which external bug-bounty tools are installed on this machine and print install hints for the missing ones. Curated from…

@awarexone@awarexoneInstall Command
arsenal

arsenal

Command

Show which external bug-bounty tools are installed on this machine and print install hints for the missing ones. Curated from…

@shuvonsec@shuvonsecInstall Command