# Tag Audit

> Triage and prune accumulated git safety tags using sort-by-SHA grouping and a per-tag decision tree (duplicate / branch-ref / intermediate-waypoint). Read-only by default; deletes only after explicit user confirmation. Use when stacked-rebase or surgical-fix sessions have accumulated multiple safety tags across namespaces (safety/*, pre-*, post-*, backup-*, stack-*), when /stack-cascade's Step 2 collision check blocks reuse of an OP_ID, or when auditing inherited tag soup. Pairs with /stack-cascade as post-success aftercare. Covers scenarios like: 'clean up safety tags', 'too many backup tags', 'prune accumulated tags', 'tag audit', 'tag-audit', 'safety tag cleanup', 'rollback anchor cleanup', 'which tags can I delete', 'tag namespace collision', 'OP_ID already has tags'.

- Skill: `voxpelli/tag-audit` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add voxpelli/tag-audit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/voxpelli/tag-audit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- License: MIT
- Author: voxpelli (https://skillmd.com/u/voxpelli)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/voxpelli/tag-audit

---


# Tag Audit — triage accumulated safety tags

Stacked-rebase sessions accumulate safety tags fast — each surgical operation
deserves its own rollback anchor, but the cost of regret is high. Once an
operation is validated and pushed, those tags decay into redundancy: they
either duplicate another tag, point at the same SHA as a current branch ref,
or mark an intermediate waypoint no one will ever `git reset` back to.

This skill audits the safety-tag namespace, surfaces duplicates by sorting
on SHA (not name), proposes deletions with reasons, and deletes only after
explicit confirmation.

## When to Use

- After a successful + pushed + CI-green stacked-rebase or surgical-fix
  session, when the operation's safety tags are no longer load-bearing.
- When `/stack-cascade` Step 2 refuses to proceed because the `OP_ID`
  namespace is non-empty.
- When `git tag -l 'safety/*' 'pre-*' 'post-*'` returns more entries than
  feels healthy and you want to know which ones still earn their keep.
- When inheriting a workspace from a past session and the tag namespace
  is unfamiliar.

## When NOT to Use

- During an in-flight cascade or surgical fix — tags are still load-bearing
  rollback anchors.
- When the operation hasn't been validated end-to-end (CI green, PRs
  reviewed, no surprises from teammates) — the rollback window is still
  open.
- On release tags or other semantically-meaningful tags outside the
  safety-tag pattern set — narrow the pattern set first.

## The Survivor Heuristic

A safety tag earns its keep only when it points at a **unique**,
**semantically-meaningful** SHA AND that SHA represents a state you might
genuinely need to `git reset` back to. Anything else is rollback theater
— the deletion candidate. Step 3 implements this as a three-question
decision tree.

## Step 1 — Enumerate tags, sorted by SHA

Sort by SHA (not name). Name-grouped output hides duplicates; SHA-grouped
output exposes them — tags pointing at the same commit cluster together,
making redundancy visible at a glance.

```bash
# Default is conservative: only the canonical `safety/*` namespace.
# Keep in sync with Step 2 below.
PATTERNS=('safety/*')
(
  set -euo pipefail
  count=$(git tag -l "${PATTERNS[@]}" | wc -l | tr -d ' ')
  echo "# Matched $count tag(s) across patterns: ${PATTERNS[*]}" >&2
  [[ "$count" -gt 0 ]] || { echo "# (nothing to audit — check pattern typos or working directory)" >&2; exit 0; }
  git tag -l "${PATTERNS[@]}" | while IFS= read -r t; do
    # Capture-then-print: bare $(...) inside printf's arg list does NOT trigger
    # set -e on substitution failure (the outer printf wins with exit 0).
    # Capturing to a variable lets set -e fire if rev-parse fails.
    # The ^{commit} peel returns the commit SHA for both lightweight AND
    # annotated tags (bare rev-parse on an annotated tag returns the tag-object
    # SHA, which won't cluster with branch SHAs in Step 2).
    sha=$(git rev-parse --short "$t^{commit}") \
      || { echo "rev-parse failed for tag '$t' — aborting audit" >&2; exit 1; }
    printf '%s  TAG    %s\n' "$sha" "$t"
  done | sort
)
```

The default `('safety/*')` is intentionally conservative — it matches only
the canonical namespace `stack-cascade` writes to under the current
convention. Widen it explicitly for cleanup that covers other namespaces:

- Legacy `stack-*-start/*` from cascades before the convention change.
- Hand-named anchors from ad-hoc surgical sessions: `pre-mr/*`,
  `pre-port/*`, `pre-fix-*`, `post-fix-*`, `pre-split/*`.
- Other safety-tag conventions: `backup-*`.

```bash
PATTERNS=('safety/*' 'pre-*' 'post-*' 'backup-*' 'stack-*')   # broad audit
```

The audit is read-only; widening the pattern is cheap. But **leave the
widened pattern in place for Step 5's deletion** only after the KEEP/DROP
review confirms no release tags or ad-hoc semantic tags slipped in
(`pre-1.0`, `pre-release-*`, `backup-2024-05`, `safety-audit-2026-q1`).
See "When NOT to Use" above.

## Step 2 — Cross-reference branch tips

Enumerate current branch tips by SHA, then interleave them with Step 1's
output so SHA collisions between tags and branch refs are visible
adjacently:

```bash
# Use the same PATTERNS as Step 1 (default safety/* OR your widened set).
PATTERNS=('safety/*')
(
  set -euo pipefail
  {
    git tag -l "${PATTERNS[@]}" | while IFS= read -r t; do
      # Same capture-then-print + ^{commit} peel as Step 1 — see comments there.
      sha=$(git rev-parse --short "$t^{commit}") \
        || { echo "rev-parse failed for tag '$t' — aborting audit" >&2; exit 1; }
      printf '%s  TAG    %s\n' "$sha" "$t"
    done
    git for-each-ref refs/heads --format='%(objectname:short)  BRANCH %(refname:short)'
  } | sort
)
```

Any `TAG` line whose SHA matches an adjacent `BRANCH` line is a deletion
candidate — the branch ref already serves as the anchor.

## Step 3 — Decision tree per tag

For each tag in the audit output, apply these three questions in order:

1. **Does another tag point at this exact SHA?** → drop one; keep the
   more recent or more descriptive name.
2. **Does a current branch ref point at this exact SHA?** → drop the
   tag; the branch ref already anchors that SHA.
3. **Is this an intermediate waypoint that was useful during operation
   but adds no information now?** → drop unless it marks a uniquely
   meaningful state worth permanently anchoring (root snapshots,
   pre-cascade origin SHAs, anchors referenced in narrative docs).

A tag survives only when it has a unique SHA AND no equivalent branch
ref AND marks a state genuinely worth `git reset`-ing back to.

## Step 4 — Propose KEEP/DROP with reasons before deleting

Always show the proposal before any deletion. Example output (with
widened patterns covering an ad-hoc `pre-*` / `post-*` session):

```
Found 21 safety tags. After audit:

KEEP (5 — unique semantically-meaningful SHAs):
  3be7b11a  pre-split/improved-disabled        [root anchor]
  4a9fac77  pre-split/pr10                     [pre-cascade origin]
  e5f1dd7d  pre-split/pr11                     [pre-cascade origin]
  aa71726b  pre-split/pr12                     [pre-cascade origin]
  f675fe31  pre-split/pr13                     [pre-cascade origin]

DROP (16):
  pre-mr/pr10-cascade        [duplicate: same SHA as pre-fix-pr11ci/pr10]
  pre-mr/pr11-cascade        [duplicate: same SHA as pre-fix-pr11ci/pr11]
  pre-fix-pr11ci/pr10        [branch-ref: SHA matches branch pr10]
  post-fix-pr11ci/pr11       [branch-ref: SHA matches branch pr11]
  post-fix-pr11ci/pr12       [branch-ref: SHA matches branch pr12]
  post-fix-pr11ci/pr13       [branch-ref: SHA matches branch pr13]
  pre-mr/pr9.6-post-triage   [intermediate-waypoint]
  pre-mr/pr9.6-post-c5       [intermediate-waypoint]
  ... (etc)

Proceed with deletion? [y/N]
```

Reason codes: `[duplicate]`, `[branch-ref]`, `[intermediate-waypoint]`,
or a one-line free-text reason for KEEP entries (`[root anchor]`,
`[pre-cascade origin]`, `[narrative anchor]`).

## Step 5 — Confirm and delete (local)

Two-pass verify-then-mutate under `set -euo pipefail`. Use quoted array
iteration — bash unquoted-var word-splitting fails silently under zsh
(see `references/shell-portability.md`).

```bash
TAGS_TO_DELETE=(
  "pre-mr/pr10-cascade"
  "pre-mr/pr11-cascade"
  "pre-fix-pr11ci/pr10"
  # ... full list from Step 4 DROP
)
(
  set -euo pipefail
  # Verify pass: confirm every tag exists before deleting any
  for t in "${TAGS_TO_DELETE[@]}"; do
    git rev-parse --verify "refs/tags/$t" >/dev/null 2>&1 \
      || { echo "Tag '$t' not found — aborting (no tags deleted)" >&2; exit 1; }
  done
  # Mutation pass: delete each
  for t in "${TAGS_TO_DELETE[@]}"; do
    git tag -d "$t"
  done
)
```

The verify pass is a pre-flight check: a missing tag name aborts before
any deletion, so the namespace can't be left half-cleaned by a typo or a
stale paste from Step 4. (Concurrent tag mutation by another process
between the verify and mutate passes is still possible — this is shell,
not a transaction. The window is narrow if you're the only writer.)

## Step 6 — Remote tag cleanup (opt-in only)

Most safety tags are local-only: `git push` does not push tags by
default. Skip this step unless the tags were explicitly pushed (e.g.,
via `git push --tags` or a CI workflow that forwards tags).

When remote cleanup is needed, push deletion refspecs in a single atomic
operation so a partial failure doesn't leave the remote half-cleaned:

```bash
TAGS_TO_DELETE=(
  "pre-mr/pr10-cascade"
  "pre-mr/pr11-cascade"
  # ... same list as Step 5
)
(
  set -euo pipefail
  # Guard against empty array: `git push --atomic origin` with NO refspecs
  # falls back to push.default and pushes the current branch — exactly the
  # opposite of "delete zero remote tags".
  [[ ${#TAGS_TO_DELETE[@]} -gt 0 ]] \
    || { echo "TAGS_TO_DELETE is empty — refusing to push" >&2; exit 1; }
  refspecs=()
  for t in "${TAGS_TO_DELETE[@]}"; do
    refspecs+=(":refs/tags/$t")
  done
  git push --atomic origin "${refspecs[@]}"
)
```

`:refs/tags/<name>` is the deletion refspec; `--atomic` makes the
multi-ref push all-or-nothing.

## Common Failure Modes

- **Bash idiom failing silently under zsh** — `for t in $unquoted_var;
  do git tag -d $t; done` passes the whole string as one arg to `git tag
  -d`, which complains about a single-revision-not-found error. The
  loop "completes" without deleting anything. Always use quoted array
  iteration (`for t in "${arr[@]}"`) or `xargs`. See
  `references/shell-portability.md` for the full mitigation set.

- **Too-broad pattern matches a release tag** — `git tag -l '*'` is
  obviously dangerous, but `safety-*` accidentally matches a hand-named
  `safety-audit-2026-q1` tag, or `backup-*` matches a release naming
  scheme. The proposal-before-delete gate at Step 4 is the safety net —
  read the KEEP/DROP list before confirming.

- **Deleting a tag whose SHA equals a branch tip — but the branch
  itself is the one you wanted to restore** — if the cascade's pushed
  state turns out to be wrong and you want to roll back, the safety tag
  was the rollback anchor. Branch tips can move; safety tags do not.
  This is why "rollback window closed" is a precondition: don't audit
  until you're committed to the current state.

- **Audit run mid-cascade** — running this skill while another git
  operation is in progress in the same repo (a rebase paused on
  conflict, a cherry-pick mid-application) can delete tags the paused
  operation needs for `--abort`. Check `git status` first.

## Pairing with `/stack-cascade`

`/stack-cascade` creates safety tags under
`safety/${OP_ID}/before/<branch>` (and historically under
`stack-${PURPOSE}-start/<branch>` before the convention change). Step 2
of `/stack-cascade` blocks if its OP_ID namespace is non-empty — that
collision-check error names this skill as the remediation. The error
shows a literal `git tag -l 'safety/${OP_ID}/*'` preview command;
running this skill against that pattern set produces the targeted
DROP list. For legacy `stack-*-start/*` tags, widen `PATTERNS` to
include `'stack-*'` in Step 1.

`/stack-cascade` and `/rebase-validate` compose tightly along the cascade
flow: cascade executes, validate verifies meaning preservation.
`/tag-audit` is a separate utility that *pairs* with cascade's tag
namespace as post-success aftercare, but operates independently — it's
useful for any safety-tag accumulation, not just cascades, and its
preconditions (rollback window closed) decouple it from the cascade's
session.

