The pre-PR gate that stands in for a team
With no second engineer to read a diff, every pull request passes scanners, a model review that blocks on security, and a local gate that signs a receipt.
Nobody else reads my pull requests before they merge. In a team, a reviewer catches the authorisation check that is subtly wrong, asks whether a new dependency is safe, and refuses to approve a branch that never ran its tests. Working alone, each of those has to be something that runs, fails loudly and cannot be skipped by accident. On 2026-08-28 three changes landed that gave the gate its current shape: scanners that ask whether the repository is safe, a model review that blocks on anything it files under security, and a local gate that signs a receipt the pull request cannot be opened without.
Asking whether the repository is safe#
The gate was good at asking whether the code was correct, through formatters, linters, type checkers and the test suites, and had no way to ask whether the repository was safe. Nothing looked for a committed secret, nothing read a lockfile against an advisory database, nothing looked at a base image, and nothing audited the CI workflows, which is where the repository holds an app key and two triggers that run with write access.
Most of the new scanners are pre-commit hooks, because CI runs the same hooks over every file. A hook is therefore a gate twice: at commit time against the staged diff, and in CI against the whole tree, for no new job. The secret scanner is the exception that needs both halves explicitly, since its hook reads the staged diff and nothing is staged in CI, where it would inspect nothing and pass. Running it over the tree before switching it on showed it would have failed on the tree it was being added to: six hits, all test fixtures and throwaway CI signing keys, which are now allowed by name while a planted token is still caught.
I adopted Semgrep in the opposite way to how it is usually adopted. Ruff already covers Python's security lints, Semgrep cannot parse a Vue single-file component, and cross-file taint analysis is a paid feature, so the registry packs would mostly re-report what I already had. What Semgrep can do that nothing else here can is state an invariant of this codebase as a pattern, across both languages. Each of the local rules describes a mistake actually made here once, such as building a browser-facing URL from the request's Host header. The registry packs still run, report-only and over changed files alone, and a rule that proves itself there gets copied into the local set, where it becomes a gate.
Auditing the workflows meant first making them clean. Third-party actions are pinned to commits, while GitHub's own stay on tags: that is nine pins rather than forty-four, because a tag is a mutable pointer and a third-party action runs whatever its owner last pushed inside a job holding the repository's token. Every workflow got a read-only permissions default, and seven jobs that had no timeout at all got one, since without it a job holds its runner for up to six hours. Dependabot arrived in the same change rather than after it, because pins are only safe while something moves them.
Before any of this blocked, I measured it. The dependency scanner was clean across 2230 packages and both Dockerfiles were clean. All three base images carried fixable high-severity advisories that no pull request could act on, because their tags float and the remedy is upstream rebuilding them, so that step reports rather than blocks.
A model review that blocks where a false negative is expensive#
Every pull request gets a review from Claude, run in CI against a written rubric with eight categories: business logic, security, performance, payments, authentication, tests, maintainability and prose. A second, deeper pass runs on a larger model whenever the change touches the security or authentication code. The findings land as one comment on the pull request, and inline where a line can carry them.
The gate that reads those findings used to block on severity alone. Both rubrics tell the reviewer to prefer a warning when it is unsure, which is right for most categories and exactly backwards for the one where a missed defect is expensive: a real authorisation weakness the reviewer was not quite sure about would have merged. A finding now blocks when its severity is error, when its category is security or authentication at any severity, or when the deep pass produced it. The rubrics changed with it, because that is the half that carries the weight. Filing a finding under security is now the decision to block, and a hardening idea or a structural preference goes to maintainability, which stays advisory.
The same change fixed the one real defect in the old rule. When both passes reported the same finding, the deduplicated record kept whichever pass came first, and the next incremental run re-derived blocking from that record. A deep-pass finding therefore stopped blocking one run after it was found, which looks exactly like having been fixed. The record now prefers the blocking pass.
Reviewing locally before CI does#
The same rubric runs on my machine before a branch is pushed, and it took three bad days to learn how to run it. In mid-August a branch with four features grew to sixty-five commits over three days of review, fix, commit and review again. Tracing every line each fix commit modified back to the commit that wrote it, twelve of twenty-four fixes were fixes of that branch's own earlier review fixes, and none of the last seven touched code older than the branch. The loop had stopped finding bugs.
The cause was that CI's review remembered what it had already reviewed and the local one did not. Each local pass re-read the whole branch, and over eighty-odd files that is a sample rather than a sweep, a different handful each time. The local review now keeps a ledger of the commit it last reviewed, and falls back to a full review on any doubt, such as a rebase or a moved base. Its first pass over a branch fans out to one reviewer per file with a mandate to list every defect rather than stop at the first. The first run of that shape returned 154 findings, ten of them errors, in a single pass where whole-branch passes had been returning about five each. The stopping rule is written down too: errors block, warnings never do, and the review is done when one pass over the latest changes adds no new error.
A quick gate, and a receipt to attest it#
Before opening a pull request, the local gate used to run everything: the full API and frontend suites, a production build and some three hundred end-to-end tests. That is tens of minutes of waiting for an answer CI computes anyway, since every suite runs on every pull request there. What the local pass uniquely owns is the fast deterministic checks, the tests the change can actually reach, and the review that precedes them.
The quick gate is that. Choosing the tests is a judgement written into the instructions rather than a script, because the script that used to derive them from changed paths was deleted for being a place a real test could be skipped by accident. The gate refuses a run that selects nothing, since otherwise the easiest command to type would be the weakest gate. A change that genuinely reaches no test says so with an explicit flag, and the claim lands on the receipt where a reader can see it.
The receipt is keyed to the commit it attests, and it records which gate ran and over what. A hook refuses to open a pull request unless a valid receipt exists for the current commit, so skipping the gate is not something that can happen quietly. Two details in the build tooling mattered here. The variable naming the inner gate is fixed so neither the environment nor the command line can redirect it, or a full run could be stamped as the quick one. And the test runner's own "changed files" option turned out not to be a shortcut: under this repository's configuration it selected all 358 frontend spec files against a change that touched no frontend code at all.
What stays human#
The prose in comments and documents goes through its own deterministic gate for characters, tense and length, and the review judges the rest: redundant, stale, or written in the wrong register. What none of this does is decide whether a change was worth making. That judgement is still mine, and the gate's job is to make sure everything else has been asked before I make it.