| 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:
- Tag the branch tip (
archive/<branch>-<timestamp>) — the work survives forever, recoverable via git checkout <tag>.
- Push the tag to
origin — durable across machines, survives the Codespace going cold.
- Write an audit-log entry with reason + evidence + the unmerged-vs-main commit list — the why is recorded, not just the what.
- 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).
- 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 "${CLAUDE_PLUGIN_ROOT}"/scripts/archive-branch.sh <branch> --reason "<one-line why>" [options]
| 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:
git checkout archive/<branch>-<date>
git switch -c <new-branch-name>
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.
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 and 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 (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 (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.