git-cleanup
Audits stale Git resources and cleans them with approval. Use when pruning branches, removing abandoned worktrees, or closing stale PRs and issues.
SKILL.md.agents-plugin/skills/git-cleanup/SKILL.md
GitHubgit-cleanup
Audit and clean up stale git resources — local branches, remote-tracking refs,
worktrees, stashes, tags, pull requests, and issues — across local checkouts,
GitHub, and GitLab. Before recommending any deletion, the skill cross-references
active code and project state to distinguish truly abandoned resources from
work-in-progress. Dry-run is the default; destructive actions require --apply
plus explicit user confirmation.
Arguments
Parse $ARGUMENTS:
- --dry-run (default): Analyze and report only. Never deletes.
- --apply: Enable destructive actions. Still requires interactive confirmation.
- --report-only: Write the audit report and stop; skip the decision prompt.
- --branches / --worktrees / --remotes / --stashes / --tags / --prs / --issues: Limit scope to the listed domains. Default is all local domains (branches, worktrees, remotes, stashes, tags) plus remote domains (prs, issues) when a host CLI is available.
- --stale-days=N: Age threshold for "stale" (default: 90).
- --host=github|gitlab|auto: Remote host.
auto(default) parsesorigin. - --protect=<pattern> (repeatable): Extra branch patterns that must never be
deleted. Always-protected:
main,master,develop, the default branch, any branch that is the base of an open PR, and — when the repo has aRELEASING.md— the state'strunkand everymaintenancebranch.
Questions: class confirm-destructive (Phase 4 decision gate, per-item prompts); headless (no question tool or chat turn): produce the dry-run report even with --apply, delete nothing, and print the interactive rerun. See ~/.agents/ycc-shared/references/question-policy.md.
Phase 0: Setup and Host Detection
- Verify working tree is a git repo:
git rev-parse --git-dir. Abort if not. - Determine default branch:
git symbolic-ref refs/remotes/origin/HEAD→ fall back tomain/masterif unset. - Read the release state: run
~/.agents/ycc-shared/scripts/release-state.sh getand parse itskey=valueoutput (format and rules:~/.agents/ycc-shared/references/branching-model.md).present=0(noRELEASING.md): release-model rules are off. Every later phase behaves exactly as described without the "Release model" notes.present=1: recordRELEASE_TRUNK(trunk),RELEASE_MAINTENANCE(maintenance, comma-separated) andRELEASE_FROZEN(frozen, comma-separated). These drive the protected set, the retired maintenance branch category, and the sync-merge findings below.- Exit code 2 (malformed state block): STOP. Show the parse error from
stderr and point the user at
/release-model --audit. Never guess a trunk or a protected set from a broken block.
- Detect host CLIs and MCP tools. Detection follows the bundle-wide
contract in
~/.agents/ycc-shared/references/forge-detection.md(forge_detect_provider origin→github/forgejo/gitea/gitlab/unknown;forge_cli→gh/tea/glab); the git-cleanup-specific operation map lives in~/.agents/skills/git-cleanup/references/host-detection.md(read when mapping a host operation to a CLI or MCP call). Summary:- Prefer
mcp__github__*tools if available for GitHub operations. - Else
gh auth statusfor GitHub,tea login listfor the Forgejo/Gitea family,glab auth statusfor GitLab. - If
--host=auto, parsegit remote get-url originvia the shared lib. A Forgejo/Gitea remote (matched againsttea login list) routes PR/issue cleanup throughtea(tea pull list/close,tea issues list/close). Unknown hosts skip the remote-domain audits with a loud notice.
- Prefer
- Check working-tree state: Capture
git status --porcelainandgit stash list. Record whether the current branch has unpushed commits. - Initialize progress tracking with the task tools (one entry per phase).
- Create audit directory:
.git-cleanup/at the repo root. Report and per-domain finding files land here. - If
--dry-runwas passed alongside--apply: Abort — they conflict.
Phase 1: Collect Raw Git State
Run only commands needed for the requested scope. Prefer porcelain / --format
flags that emit machine-readable output.
Branches:
git for-each-ref refs/heads/ --format='%(refname:short)|%(upstream)|%(upstream:track)|%(committerdate:iso8601)|%(objectname)'Remote-tracking prune candidates:
git remote prune <remote> --dry-runfor each remote fromgit remote.Worktrees:
git worktree list --porcelain. Flag entries whoseworktreepath no longer exists, whose branch is[gone], or which sit under a legacy global ycc root (~/.claude-worktrees/) instead of a repo-local managed root. The managed roots under the repo root are.claude/worktrees/,.opencode/worktrees/,.codex/worktrees/,.cursor/worktrees/,.omp/worktrees/and.agents/worktrees/; every runtime checks all of them. Entries under~/.omp/wtare OMP-managed (omp worktree clear): report only, never delete.Stashes:
git stash list --date=iso. Parse the date from each entry.Tags:
git for-each-ref refs/tags/ --format='%(refname:short)|%(creatordate:iso8601)|%(objectname)'thengit ls-remote --tags <remote>to find local-only tags.PRs (when host available): List merged-but-branch-still-local and stale open PRs authored by the current user. Example GitHub via
gh:gh pr list --state=all --author=@me --json number,title,state,headRefName,baseRefName,updatedAt,isDraft,mergedAt,closedAt.Issues (when host available):
gh issue list --state=open --author=@me --json number,title,updatedAt,labelsfiltered byupdatedAt < now - stale-days.Release model (only when
present=1):Release branches: every local
release/*branch (git for-each-ref refs/heads/release/ --format='%(refname:short)') and every remote one (git for-each-ref refs/remotes/ --format='%(refname:short)', keeping names whose part after<remote>/starts withrelease/).Sync-merge candidates: branch names (local or remote, excluding
RELEASE_TRUNK) containingsync, and, for every other local branch and remote-tracking branch, merge commits newer thanstale-daysthat are not on the trunk and whose subject reads like a trunk sync:git log --merges --since="<stale-days> days ago" --format='%h|%s' -E \ --grep="^Merge (remote-tracking )?branch '?([A-Za-z0-9._-]+/)?(main|master|trunk|develop|<RELEASE_TRUNK>)'? into" \ "origin/<RELEASE_TRUNK>..<branch>"Rules and rationale (read when classifying a branch, stash, or worktree as active or deletable):
~/.agents/skills/git-cleanup/references/active-code-rules.md(R9, R10).
Phase 2: Active-Code Analysis
This is the gating phase. Never recommend deletion of a resource that
Phase 2 flags as active. Read
~/.agents/skills/git-cleanup/references/active-code-rules.md before
classifying any candidate; it holds rule rationales, edge cases, and the full decision matrix.
For each candidate branch, worktree, PR, and issue, mark as active if any of the following is true:
- Recent commits: Tip commit is newer than
stale-daysdays. - Unpushed work: The local branch has commits not in its upstream (or no upstream is configured and commits are ahead of the default branch).
- Uncommitted / stashed changes: For worktrees — dirty working tree
(
git -C <worktree> status --porcelainnon-empty) or a stash attached to the branch. - Referenced in open PR: The branch is the
headRefof any open PR, OR thebaseRefof an open PR (never touch a base branch). - Referenced in active code: Grep the tracked working tree for the branch
name —
git grep -l -w <branch-name>— in docs, issue templates, CI config, release notes, and comments. A textual reference in tracked files means the branch is load-bearing. - Linked to active issues: PR/issue body references an open issue updated
within
stale-days, or has thestatus:in-progress/status:needs-infolabel family defined by this repo's taxonomy. - Protected name: Matches
main|master|develop, the default branch, or any--protect=<pattern>. Whenpresent=1, alsoRELEASE_TRUNKand every branch inRELEASE_MAINTENANCE(local or remote). - Recent reflog activity:
git reflog show <branch>shows motion withinstale-days.
Candidates that fail every check become stale — eligible for cleanup. Record the reason per candidate so the user can audit the decision.
Release model (only when present=1; see R9 and R10 in the rules file):
- Retired maintenance branch: a local or remote
release/*branch that is not inRELEASE_MAINTENANCEand not protected (rule 7, or rule 4 as an open-PR base) is classified as retired, never as Stale — even when rules 1–8 would call it stale or active (RELEASING.mdmentioning a frozen branch does not make it active). Branches listed inRELEASE_FROZENare retired too and are flagged "frozen by RELEASING.md". - Sync-merge anti-pattern: branches named like
*sync*and merge commits collected in Phase 1 are informational findings. They never change a candidate's classification and never produce a cleanup command.
Phase 3: Produce the Audit Report
Write .git-cleanup/report.md with sections keyed by domain:
- Protected — always-kept resources and why (default branch, open-PR bases, user-protected patterns).
- Active (skip) — candidates gated out by Phase 2, each with the specific rule that matched.
- Stale (eligible) — candidates safe to clean, grouped by domain, with the
exact
git/gh/glabcommand that would remove each one. - Ambiguous (ask) — edge cases (e.g., branch has unpushed commits and
hasn't been touched in 6 months; worktree with clean tree but branch
[gone]). - Retired maintenance branch (only when
present=1) —release/*branches outsideRELEASE_MAINTENANCE, each marked local or remote, with "frozen by RELEASING.md" on those listed inRELEASE_FROZEN. For each, show the archive command to run first and the deletion command, for example:git tag archive/release/0.3 release/0.3thengit branch -d release/0.3; for a remote branch,git tag archive/release/0.3 origin/release/0.3,git push origin archive/release/0.3, thengit push origin --delete release/0.3. - Anti-patterns (informational) (only when
present=1) — sync-named branches and sync-merge commits (hash, subject, branch), with a pointer to the branching-model rule "Never merge one long-lived branch into another" and the fix: rebase the topic branch ontoorigin/<RELEASE_TRUNK>instead, and move fixes to maintenance branches with/backport. - Host-API notes — PRs/issues surfaced from GitHub/GitLab with current state and decision rationale.
Also emit a one-screen summary to stdout: counts per category and the path to
the full report. If --report-only, STOP here.
Phase 4: User Decision Gate
Present the summary via AskUserQuestion. Offer structured choices:
- View full report — print
.git-cleanup/report.mdin chunks. - Apply all stale (skip ambiguous).
- Apply all stale + resolve ambiguous interactively.
- Choose per domain — branches only, worktrees only, etc.
- Cancel — exit without changes.
For ambiguous items, re-prompt per item with the evidence summary and the exact command that would run.
Retired maintenance branches are never part of "Apply all stale". Each one
needs its own explicit confirmation, and the prompt offers to create the
archive/<branch> tag first (default: yes). Anti-pattern findings have no
action to confirm.
If --dry-run is active (default), STOP here — do not invoke Phase 5 even
if the user clicks "Apply". Tell them to rerun with --apply.
Phase 5: Execute (only with --apply)
For each approved item, in this order (safest first):
- Remote-tracking prune:
git remote prune <remote>. - Worktrees:
git worktree remove <path>. If missing on disk, usegit worktree prune. - Local branches:
git branch -d <name>(never-Dunless the branch has been merged to the default branch or its upstream is[gone]AND the user explicitly confirmed in Phase 4). For a confirmed retired maintenance branch, create and (for a remote branch) push thearchive/<branch>tag first when the user accepted it; delete a remote one withgit push <remote> --delete <branch>only after that tag exists. - Local-only tags:
git tag -d <name>. - Stashes:
git stash drop stash@{N}. Drop highest-indexed first to keep indices stable during the sweep. - PRs: close via host CLI / MCP (
gh pr closeor GitLab equivalent). Add a closing comment referencing staleness if repo conventions require one. - Issues: close with a stale-label comment or add a
wontfix/ stale label from the repo's taxonomy — never invent ad-hoc labels.
Record each action (success / failure / skipped) in .git-cleanup/actions.log.
On any individual failure: log and continue. On systemic failure (e.g., auth
error): abort and report.
Phase 6: Verification and Summary
- Re-run Phase 1 collection for the targeted domains and diff against the pre-execution snapshot. Confirm each approved item is gone.
- Print final summary: counts removed per domain, any skipped items with
reason, and the paths to
.git-cleanup/report.mdandactions.log. - Do NOT commit anything automatically — cleanup output lives under
.git-cleanup/which should be added to.gitignoreby the user.
Host-Specific Notes
- GitHub: Prefer
mcp__github__*tools when present. Fall back togh. Usegh pr list --searchfor richer stale-age filters when supported. - GitLab: Use
glabwith equivalent subcommands (glab mr list,glab issue list). Self-hosted GitLab URLs resolve viaglabconfig. - Neither CLI available: Skip
--prsand--issuesdomains with a loud notice; local cleanup still proceeds.
Subagent Delegation
For large repos where Phase 2's active-code scan is expensive, delegate to
git-cleanup agent via the Agent tool. The agent performs the read-only
audit (Phases 1-3) and returns the structured report inline without writing
files; this skill writes it to .git-cleanup/report.md (Phase 3) and
orchestrates Phases 4-6. Pass the Phase 1 collection commands inline in the dispatch
prompt, along with the invocation options and Phase 0 release-state.sh get output,
so the agent can collect each in-scope domain and apply the same protected set and
release-model rules (a malformed state has already stopped the run in Phase 0).
Important Notes
Dry-run by default. Destruction requires
--applyand interactive approval.Never touch the default branch, protected branches, or open-PR bases. With a
RELEASING.md, that includes the state's trunk and maintenance branches.Retired maintenance branches are never auto-deleted. Suggest the
archive/<branch>tag first and require per-item confirmation.A malformed
RELEASING.mdstate block stops the audit with the parse error and a pointer to/release-model --audit.Never
git branch -Dwithout explicit per-item confirmation.Worktrees under any repo-local managed root (
.claude/,.opencode/,.codex/,.cursor/,.omp/or.agents/followed byworktrees/) are fair game per this repo's convention. Legacy worktrees under~/.claude-worktrees/should be reported and, if the user confirms, relocated or removed. Never delete worktrees under~/.omp/wt; OMP manages them (omp worktree clear).Respect the repo's label taxonomy. Do not invent stale/wontfix labels.
No secrets. Do not log tokens or API responses containing credentials.
Ambiguous items are always surfaced, never auto-deleted.