/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.
> /plugin marketplace add trailofbits/skillsHow it fires
How this command gets triggered: by you, by Claude, or both.
- Fires itselfClaude auto-loads it when your prompt matches the work.
- You can call itInvoke it directly when you want it.
- Slash command
/git-cleanup
Context preview
What this command does when you run it.
Safely analyzes and cleans up local git branches and worktrees, categorizing them as merged, squash-merged, superseded, or active work before deleting anything.
Command definition
git-cleanup.mddescription: "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 -40Phase 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
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 -40Phase 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
A Claude Code plugin marketplace from Trail of Bits providing skills to enhance AI-assisted security analysis, testing, and development workflows. Codex can load this marketplace through its Claude marketplace compatibility.
Other commands on trailofbits-skills.
audit
Audit a file, directory, or whole repo for insecure default configuration: fallback secrets, default credentials, fail-open switches, weak crypto, permissive…

