Four Enforcement Layers — Two of Them Block
Read this diagram for one thing first: which layers BLOCK. Two of the four do. The other two advise. A reader who takes "four layers of defence" to mean "four things that stop a defect" has the wrong model of their own pipeline — and that misreading is what lets an unverified change reach the default branch while everyone believes it could not.
In one line: Four layers, two of which block — post-edit lint and stop verification advise; the pre-push gate and the default-branch required checks refuse. A defect must slip past both blocking layers to merge.
Two things on the PR are easy to conflate, and only one of them blocks.
GitHub Actions runs the jobs; a ruleset decides which of those jobs a merge
requires. A workflow can be green, thorough and completely non-blocking — that is
the default, and it is why a job named — blocks on fixable crit/high blocked
nothing on a real repository for months. Naming the job is not configuring the
rule; see §7.6 for the manual steps and scripts/check-repo-config.sh for the
check that they were actually taken.
Bot review is a REVIEW, not a check. It posts findings on the diff and it is genuinely productive — it found real defects on essentially every PR in the session that produced this section, including defects inside the gates being written. But it reports no status, so it blocks nothing, and a bot that stalls or rate-limits looks exactly like a bot that found nothing. Waiting for it is discipline, not enforcement. Two traps worth carrying:
- GitHub re-anchors an older review comment onto a newer commit, so
commit_idtells you where a comment now points, not what it was written against. Filter onoriginal_commit_idor you will re-litigate findings you already fixed — in one session, 18 of 24 comments were stale re-anchors. - A silent bot is not an approving bot.
The dotted edges are the honest part, and they are the reason Layer 4 exists rather than being redundant with Layer 3:
- Layer 3 only sees a
git pushthe agent itself runs. A human pushing from a terminal, or merging through the web UI, never invokes it. It is a fast local gate, not a boundary. - A repository admin can merge past a red required check. Where that bypass exists, Layer 4 is strong but not unconditional — which is why the canon pairs it with a post-hoc observer that records merges which did not observe a green, finished gate. Prevention where available, observation where not; never a claim of prevention that the settings do not grant.
Layer 1: Post-Edit Linting. Runs automatically after edits — syntax, style, and type errors surface within seconds, while the code is still in context. Catches the cheapest defects: typos, missing imports, bad annotations, unused variables.
Layer 2: Stop Verification. Runs when the AI signals completion. A verification prompt asks whether tests ran, coverage held, and linting passed — catching the "it should work now" failure mode. It fires every time, whether or not anyone invoked /verification-before-completion. It is the safety net beneath the process layer.
Layer 3: Pre-Push Blocking. A hard gate before the remote — failing tests, sub-threshold coverage, or lint regressions block the push. Last line of defense, catching what slipped past Layers 1-2 (integration failures, coverage drops, merge-conflict lint regressions).
Defense-in-depth rationale: Each of the three core gates catches a different defect category at a different cost; the gates are redundant by design, so no single gate failure lets a defect reach the remote unchecked.
Optional security layer (Security Scanning). Beyond the three core gates, regulated systems add a security-scanning layer — for them a compliance requirement, not an optional improvement: a dev-time SAST/secrets/IaC scanner with an auto-remediation loop, plus a PR-time security-review GitHub Action posting inline findings with severity. For projects without a regulatory surface this layer is optional.
Evidence: Each layer is a configured mechanism: the post-edit layer is an LSP plugin catching type errors in real time; the Stop hook (verification prompt in .claude/settings.json) fires on every task completion; the pre-push gate is templates/hooks/pre-push-gate.sh. Reproducible: introduce a type error, an unverified claim, and a failing test, and watch each get caught at its own layer. See appendix-e-hooks.md.