Found by auditing every control command against the permission it enforces, then confirming both halves against a running instance.
runMRReview (internal/control/mr.go) resolves the repository with policy.CanRead and applies no further check, and reviewGates counts approvals and blocks without weighting them by permission:
for _, r := range reviews {
if r.Stale || r.Reviewer == mr.Author { continue }
latest[r.Reviewer] = r.Verdict
}
On a public repository that means any authenticated account — with no grant, no membership, nothing — can decide a merge gate. Both directions confirmed on a real instance:
- A stranger's approval satisfies
require-approvals. An account created seconds earlier, with no relationship to the repository, approved and the owner's merge went through. Four-eyes review is defeated by having two accounts, which on an instance withregistration = "open"is defeated by anyone. - A stranger's
--request-changesblocks the owner's merge.mallory requested changes on !1; resolve their review before merging, with no override: only the objector can withdraw it. A force-push stales it, so the repository owner's remedy for a drive-by objection is rewriting history.
The second is a nuisance; the first is the one that matters, because require-approvals is the control an operator turns on precisely when they want merges gated by a second person's judgement, and it currently assures nothing on a public repository.
Remedy: keep reviewing open to everyone — a drive-by review is worth having — but count only reviewers with write access (or named in CODEOWNERS) toward the gates. reviewGates filters, mr review stays CanRead. The narrower alternative is requiring CanWrite to submit any verdict other than --comment, which is simpler but discards outside review entirely.
Whichever is chosen, mr show should say which reviews count, or the gate becomes silently different from what the page displays.
closed by commit f84bca7807 by cmc: mr: only a writer's review decides a merge gate
2026-09-05 01:22 UTC