Problem
The git push permission guard in .claude/settings.json is an enumeration of command shapes, matched against the raw command string with prefix globbing and no normalisation. It has now leaked three times, and each hole was invisible to the round that preceded it:
| Round |
Spelling that slipped through |
| 1 |
git push origin main --force — marker not adjacent to push (prefix-anchored rules) |
| 1 |
git push origin +beta-release-9.9 — force via refspec |
| 2 |
git push origin HEAD:main — protected ref via colon refspec |
| 3 |
git push origin refs/heads/main — bare source-side refspec: the character before main is /, not the space * main needs nor the colon *:main needs |
| 3 |
git push --all / git push --branches origin — pushes every local branch including main; its neighbours --mirror/--prune/--tags were all covered |
Rounds 1–2 were fixed in #620; round 3 was found by the Stage 4 review of #620 itself and fixed in the same PR.
scripts/quality-check.sh pins every known shape by command string, and that gate is genuinely good — it is what caught the regressions and what proves each fix. But it can only prove "the strings we thought of are covered", never "no valid git push spelling reaches a protected branch", which is the property the rule actually wants. Three rounds of the same category is enough evidence that enumeration will not converge on its own.
Proposal
Normalise before matching: resolve a push command's effective destination ref using git's own refspec semantics (git rev-parse / git for-each-ref, or git push --dry-run parsing) and gate on that, rather than pattern-matching the raw argument string.
That turns every spelling in the table above into the same question — "does this push write refs/heads/main on the remote?" — which has one answer regardless of whether it was written as origin main, HEAD:main, refs/heads/main, --all, or a spelling nobody has thought of yet.
Expected vs actual
- Expected: a push that would write a protected ref prompts, however it is spelled.
- Actual: it prompts only if its exact lexical shape appears in the
ask list.
Constraints worth respecting
deny has no override, so an over-broad normalised rule blocks documented work rather than prompting for it. The existing git stash / git prune notes in CLAUDE.md are the precedent — this must stay in ask.
- Read-only inspection must remain unattended (
MUST_NOT_BE_GUARDED already pins this).
- Whatever replaces the enumeration has to keep the command-string gate, not retire it — the gate is the regression net, and the property it checks is still worth checking.
- A permission hook was tried for a related problem and removed after six false positives in one day (CLAUDE.md, "The two
Bash permission hooks that used to shape this are gone"). Any normalisation that needs a hook has to answer that history first.
Not urgent
The enumeration is currently complete against every spelling anyone has found, and MUST_BE_GUARDED now carries 103 shapes. This is about stopping the fourth round, not fixing a live hole.
Problem
The
git pushpermission guard in.claude/settings.jsonis an enumeration of command shapes, matched against the raw command string with prefix globbing and no normalisation. It has now leaked three times, and each hole was invisible to the round that preceded it:git push origin main --force— marker not adjacent topush(prefix-anchored rules)git push origin +beta-release-9.9— force via refspecgit push origin HEAD:main— protected ref via colon refspecgit push origin refs/heads/main— bare source-side refspec: the character beforemainis/, not the space* mainneeds nor the colon*:mainneedsgit push --all/git push --branches origin— pushes every local branch includingmain; its neighbours--mirror/--prune/--tagswere all coveredRounds 1–2 were fixed in #620; round 3 was found by the Stage 4 review of #620 itself and fixed in the same PR.
scripts/quality-check.shpins every known shape by command string, and that gate is genuinely good — it is what caught the regressions and what proves each fix. But it can only prove "the strings we thought of are covered", never "no validgit pushspelling reaches a protected branch", which is the property the rule actually wants. Three rounds of the same category is enough evidence that enumeration will not converge on its own.Proposal
Normalise before matching: resolve a push command's effective destination ref using git's own refspec semantics (
git rev-parse/git for-each-ref, orgit push --dry-runparsing) and gate on that, rather than pattern-matching the raw argument string.That turns every spelling in the table above into the same question — "does this push write
refs/heads/mainon the remote?" — which has one answer regardless of whether it was written asorigin main,HEAD:main,refs/heads/main,--all, or a spelling nobody has thought of yet.Expected vs actual
asklist.Constraints worth respecting
denyhas no override, so an over-broad normalised rule blocks documented work rather than prompting for it. The existinggit stash/git prunenotes in CLAUDE.md are the precedent — this must stay inask.MUST_NOT_BE_GUARDEDalready pins this).Bashpermission hooks that used to shape this are gone"). Any normalisation that needs a hook has to answer that history first.Not urgent
The enumeration is currently complete against every spelling anyone has found, and
MUST_BE_GUARDEDnow carries 103 shapes. This is about stopping the fourth round, not fixing a live hole.