Protection on main
main cannot be committed to or pushed into — locally or on the web.
main moves only when a pull request merges. Two layers enforce that, and both are in
version control.
The server
.github/rulesets/main.json is the protection itself. It is a file so that what guards
the branch is reviewable, diffable and reappliable — a protection configured by clicking
in a web form has no history, and nothing disagrees with it when it drifts.
npm run protect # apply the ruleset, then verify
npm run protect:check # verify only; change nothing
| Rule | Effect |
|---|---|
pull_request |
a direct push is refused; main moves by merge only |
non_fast_forward |
no force-push |
deletion |
main cannot be deleted |
required_linear_history |
no merge commits, so squash or rebase |
No required status checks, on purpose
Actions are blocked org-wide, so a required check would never report and main would be
permanently unmergeable. A gate that can never go green is not stricter; it is broken.
The gates run locally instead — npm run check — and the pull request template asks for
their output rather than trusting a claim. Read the comment at the top of
scripts/protect-main.sh before adding a required check.
The clone
The server refuses a push. It cannot refuse a commit, so without a local hook the work
still lands on the local main and has to be moved afterwards — which is the moment people
reach for a bypass.
| Hook | Refuses |
|---|---|
pre-commit |
a commit made while main is checked out |
pre-push |
a push whose destination ref is main or master |
commit-msg |
a subject that is not one Conventional Commit type, or is over 72 characters |
pre-push reads the destination ref from git's stdin rather than the current branch,
because git push origin HEAD:main is the spelling that actually causes the accident and
a current-branch check misses it entirely.
Verify, do not assume
npm run protect:check reads the effective rules for refs/heads/main from the API —
what git will actually evaluate — rather than confirming that a ruleset object exists:
gh api repos/akwlabs-ai/akw-factory-floors/rules/branches/main --jq '[.[].type]|sort'
tests/hooks.test.ts asserts each hook in both directions: that it blocks, and that it
stays quiet otherwise. A guard that blocks everything looks identical to a working guard
from the passing side.
The one legitimate override
AKW_ALLOW_MAIN_COMMIT=1 lets pre-commit through, for replaying history during a repair
you have already decided on. There is no override for pre-push, and none for the server.