Skip to content

Push permission guard is an enumeration that has leaked three times — normalise the destination ref instead #628

Description

@bess-product-owner

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions