Safely retire a local branch by tagging its tip first, then deleting the branch. The sanctioned escape hatch from guard-destructive.sh's blanket `git branch -D` block — turns silent-loss-of-unmerged-work into a reversible, audit-logged operation. Use when an agent or maintainer wants to clean up old feature branches that represent abandoned approaches, stale plans, or superseded work.
Installs into .claude/skills of the current project.
Are you the author of Branch Archive?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/mcorbett51090-branch-archive)
---
name: branch-archive
description: "Safely retire a local branch by tagging its tip first, then deleting the branch. The sanctioned escape hatch from guard-destructive.sh's blanket `git branch -D` block — turns silent-loss-of-unmerged-work into a reversible, audit-logged operation. Use when an agent or maintainer wants to clean up old feature branches that represent abandoned approaches, stale plans, or superseded work."
---
# Branch archive — the safe path past `guard-destructive`
The marketplace's `guard-destructive.sh` hook blocks `git branch -D` blanket because it can't tell *valuable unmerged work* from *stale planning state* — both look identical to a regex. The block is correct policy: an agent who force-deletes the wrong branch can silently lose hours of work, and there's no observable trace.
This skill is the sanctioned escape hatch. The pattern:
1. **Tag the branch tip** (`archive/<branch>-<timestamp>`) — the work survives forever, recoverable via `git checkout <tag>`.
2. **Push the tag to `origin`** — durable across machines, survives the Codespace going cold.
3. **Write an audit-log entry** with reason + evidence + the unmerged-vs-main commit list — the *why* is recorded, not just the *what*.
4. **Delete the local branch** via `git update-ref -d refs/heads/<branch>` — the lower-level primitive the guard doesn't pattern-match (correctly, because the script's preconditions are what make this sound).
5. **Optionally delete the remote branch** if `--delete-remote` is passed and the branch is still on origin.
The script aborts before step 4 if any of steps 1-3 fail. There is no "force" variant — the safety preconditions are what make the script's use of the lower-level primitive defensible. **Do not use `git update-ref -d` directly to bypass the guard for any other reason.**
## When to use
- An old feature branch whose work was folded into another PR via a squash merge (the branch's commits aren't on main as identical SHAs).
- A planning / spike / WIP branch whose approach was superseded and the team chose a different path.
- A branch whose remote was deleted but a local copy lingers.
- A long-running branch that's drifted to the point where rebasing costs more than redoing the work in a fresh branch (record the abandonment with evidence).
- Any branch where the answer to "what would we lose if this disappeared?" is "nothing of remaining value — and we have a corroborating reference for that."
## When NOT to use
- The work on the branch is genuinely needed and unmerged. **Open a PR or rebase first.**
- You're not sure whether the work is needed. **Check the patch-IDs against main first** (`git log <branch> --not main`), open the PR list, ask. Don't archive what you don't understand.
- The branch is currently checked out. The script refuses; `git checkout main` first.
- The branch is `main` / `master` / `HEAD`. The script refuses.
- A branch the team is collaborating on. Archive is for solo / abandoned work; multi-author branches warrant a team conversation first.
## Invocation
```bash
# Shipped path (canonical, inside the plugin) — invoked via `bash` so no execute bit is required:
bash "${CLAUDE_PLUGIN_ROOT}"/scripts/archive-branch.sh <branch> --reason "<one-line why>" [options]
# If ${CLAUDE_PLUGIN_ROOT} is unset (some hosts), resolve from the plugin cache (mirrors bin/rc):
# bash "$(find ~/.claude/plugins/cache -name archive-branch.sh -path '*ravenclaude-core*' | head -1)" ...
```
| Option | Effect |
|---|---|
| `--reason "..."` | **Required.** One-line justification. Records to the audit log. |
| `--evidence "..."` | Recommended. PR number, decision doc, commit SHA, anything that lets a future reader verify the reason. |
| `--skip-push` | Don't push the tag to origin (offline / disconnected work). The work is recoverable locally; lost if the machine dies. |
| `--delete-remote` | Also delete the branch on `origin` if it's still there. |
| `--yes` | Skip the final "Proceed?" confirmation. Use only when scripted. |
The script always prints a plan first (branch, tip SHA, commits about to be archived, archive tag name, reason, evidence, operator), and unless `--yes` is passed, asks `Proceed? [y/N]`. The default is **No**.
## Recovery
The archive tag is durable. To resume work on the archived branch:
```bash
git checkout archive/<branch>-<date> # detached HEAD at archived tip
git switch -c <new-branch-name> # recreate a working branch
```
The tag is on `origin`, so the recovery works on any machine — the work isn't tied to one Codespace.
## Worked example
PR #221 (`fix/install-ergonomics-copilot-path`) merged a Copilot-bridge ergonomics fix to `main`. A stale local branch `fix/copilot-installer-status-project-flag` from May 27 carried three commits whose content overlapped with PR #221 — patch-ID check vs main was inconclusive (the squash flattened the commits), but reading `plugins/ravenclaude-core/CLAUDE.md` confirmed the feature lives on main.
```bash
scripts/archive-branch.sh fix/copilot-installer-status-project-flag \
--reason "Stale May 2026 branch; remote was deleted; current Copilot bridge state on main supersedes." \
--evidence "Commits 81c3a92 + 0358b2a (patch-id checked vs main: not present, but feature lives in main's Copilot bridge — confirmed via plugins/ravenclaude-core/CLAUDE.md)" \
--yes
```
Result: tag `archive/fix-copilot-installer-status-project-flag-2026-06-03-002017` pushed to origin; audit log at `.ravenclaude/runs/branch-archive/2026-06-03-002017-fix-copilot-installer-status-project-flag.log`; local branch gone; work fully recoverable.
## Why this is sound
The marketplace's destructive-action discipline (in [`plugins/ravenclaude-core/CLAUDE.md`](../../CLAUDE.md) and [`AGENTS.md`](../../../../AGENTS.md)) draws a line between **irreversible** and **reversible** operations. `git branch -D` is irreversible — once executed, the branch's commits are no longer referenced and will be garbage-collected. `archive-branch.sh` is reversible — the tag is a permanent reference, the audit log captures the reasoning, and the recovery path is documented.
`guard-destructive.sh`'s `git branch -*D*` regex is blanket because the guard can't read intent. This skill's contract — tag first, push tag, audit-log, then delete via the lower-level primitive — encodes the intent the regex can't infer. Bypass via `git update-ref -d` *outside* this script is not sanctioned; the script's preconditions are what make its use of that primitive defensible.
## Routing
| When | Who | What |
|---|---|---|
| Agent wants to clean up old branches at end-of-session | The agent itself | Run the script with reason + evidence; default to `--no` confirmation prompt unless the agent is sure |
| Maintainer doing repo hygiene | The maintainer | Run the script; recover via the tag if a wrong call is later spotted |
| Bulk cleanup (>5 branches) | The agent | Use the script per-branch (not a wrapper that batches) — each archive needs its own reason + evidence |
| A real-work branch was archived in error | Anyone | `git checkout <tag>` → `git switch -c <branch>` → push. The recovery is normal-Git, no special tooling needed |
## Provenance
- Sanctioned 2026-06-02 after the agent hit the guard during routine branch cleanup. The branch in question (`fix/copilot-installer-status-project-flag`) had real unmerged commits that *should* have been preserved — the guard was correct to block, and the archive pattern is the right next step instead of bypass.
- Script: [`${CLAUDE_PLUGIN_ROOT}/scripts/archive-branch.sh`](../../scripts/archive-branch.sh) (ships inside the plugin; a thin shim at the marketplace-root `scripts/` keeps the marketplace's own references resolving).
- Pattern: tag-first / audit-log / lower-level-delete-primitive. Mirrors the pattern from [`reset-plugin-cache.py`](../../scripts/reset-plugin-cache.py) (snapshot before mutate; atomic swap; recoverable rollback).
- Not (yet) wired into audit-gates — script's behaviour is well-bounded but a future enhancement could ship `bad-attempt-without-reason.fixture` + `good-with-all-args.fixture` for bidirectional verification.