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-cascadeStep 2 refuses to proceed because theOP_IDnamespace 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.
# 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-*.
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:
# 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:
- Does another tag point at this exact SHA? → drop one; keep the more recent or more descriptive name.
- Does a current branch ref point at this exact SHA? → drop the tag; the branch ref already anchors that SHA.
- 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).
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:
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; donepasses the whole string as one arg togit 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[@]}") orxargs. Seereferences/shell-portability.mdfor the full mitigation set.Too-broad pattern matches a release tag —
git tag -l '*'is obviously dangerous, butsafety-*accidentally matches a hand-namedsafety-audit-2026-q1tag, orbackup-*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. Checkgit statusfirst.
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.