Skip to content

Commit b1a9268

Browse files
johanzanderclaude
andcommitted
fix: guard source-side tag pushes, the one hole no branch protection covers
`git push origin refs/tags/v1.2.3` published a release tag unattended. The guard was spelled `*:refs/tags/*`, which only ever saw the destination-side form — the same colon-shaped blind spot that let `refs/heads/main` through, one line up in the same list. Both are now spelled without the colon (`*refs/tags/*`, `*refs/heads/main*`), each subsuming its destination-side form rather than sitting beside it. The maintainer found this one by hand and named it on the PR; it was left open by the previous commit, which read the reviews and not the thread. That reading gap is fixed separately in `fix/resume-reads-comments`. This entry is deliberately settled ahead of the open question about the protected-BRANCH patterns, because it does not depend on it: a tag is not a branch, so no `enforce_admins` setting covers publishing one, and the guard is required whichever way that argument goes. `git push origin v9.9.0` — a tag without the `refs/` prefix — is caught by `* v*`, which stays. Verified both new pins are holes against the pre-fix settings and guarded after, that the destination-side spelling `HEAD:refs/tags/v1.2.3` still matches (so dropping the colon regressed nothing), and that `git push origin my-feature` and `git push -u origin fix/issue-620` stay unattended. Gate now checks 105 shapes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012LExo6fcbup75vtc9NfoAR
1 parent e9943be commit b1a9268

3 files changed

Lines changed: 20 additions & 2 deletions

File tree

.claude/settings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
"Bash(git push *--all*)",
101101
"Bash(git push *--branches*)",
102102
"Bash(git push * +*)",
103-
"Bash(git push *:refs/tags/*)",
103+
"Bash(git push *refs/tags/*)",
104104
"Bash(git push * v*)",
105105
"Bash(git push * main)",
106106
"Bash(git push * main *)",
@@ -129,7 +129,7 @@
129129
"Bash(git -* push *--all*)",
130130
"Bash(git -* push *--branches*)",
131131
"Bash(git -* push * +*)",
132-
"Bash(git -* push *:refs/tags/*)",
132+
"Bash(git -* push *refs/tags/*)",
133133
"Bash(git -* push * v*)",
134134
"Bash(git -* push * main)",
135135
"Bash(git -* push * main *)",

CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ git push origin --delete release-X.Y # destroys a shared ref
313313
git push origin v9.9.0 # publishes a release tag
314314
git push origin HEAD:main # protected ref via colon refspec
315315
git push origin refs/heads/main # bare source-side refspec: no space, no colon
316+
git push origin refs/tags/v1.2.3 # same, for a TAG: `*:refs/tags/*` saw only the colon form
316317
git push --all # every local branch, main included
317318
git push --branches origin # exact synonym for --all
318319
gh api repos/o/r/pulls/N/merge -X PUT # merges, bypassing `gh pr merge`
@@ -332,6 +333,12 @@ simply was not; `--branches` is its documented synonym (`git push --help`:
332333
"--all, --branches") and was added in the same pass rather than waiting to
333334
become the fourth leak.
334335

336+
`refs/tags/` had the same colon-shaped blind spot as `refs/heads/`, and it is
337+
the one entry here that **branch protection can never replace**: a tag is not a
338+
branch, so no protected-branch setting covers publishing a release tag. Both
339+
guards are now spelled without the colon (`*refs/tags/*`, `*refs/heads/main*`),
340+
each subsuming its destination-side form rather than sitting beside it.
341+
335342
The marker sits at an arbitrary argument position. The first enumeration was
336343
**prefix-anchored**`Bash(git push --force*)` — which genuinely cannot reach
337344
position 3, so it leaked every line above and was replaced by a blanket

scripts/quality-check.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,17 @@ MUST_BE_GUARDED = [
310310
# its neighbours --mirror/--prune/--tags were all covered. `--branches` is
311311
# a synonym for it (git 2.50 `git push --help`: "--all, --branches"), so
312312
# enumerating one without the other would have been the next leak.
313+
# A TAG push, source-side. `*:refs/tags/*` only ever saw the
314+
# destination-side spelling, so `git push origin refs/tags/v1.2.3` -- the
315+
# ordinary one-sided form -- published a release tag unattended. The guard
316+
# is now `*refs/tags/*` without the colon, covering both sides.
317+
#
318+
# This one is guarded regardless of how the protected-BRANCH argument is
319+
# settled: a tag is not a branch, so branch protection never covers it, and
320+
# `git push origin v9.9.0` (no refs/ prefix) is not covered by this pattern
321+
# either -- `* v*` is what catches that, and it stays.
322+
"git push origin refs/tags/v1.2.3",
323+
"git -C x push origin refs/tags/v1.2.3",
313324
"git push --all",
314325
"git push --all origin",
315326
"git push --branches origin",

0 commit comments

Comments
 (0)