Most days now, I merge pull requests without reading the diff.
That sentence used to describe someone I would not have hired. So let me be precise about what changed, because it isn't confidence and it isn't recklessness. It's that I moved the things review was catching to somewhere that catches them earlier.
Here's the honest version of how that happened.
The problem was arithmetic, not philosophy
I run several coding agents in parallel. That produces more diff per day than I can read. Not "more than I feel like reading" — genuinely more than fits in a working day.
When that happens you have exactly two options:
- Generate less, so it fits what you can read.
- Make it safe to not read.
I picked the second one. Not because I'm brave, but because option 1 means throwing away the reason I set this up.
The uncomfortable part: option 2 is not a mindset. It's a list of specific things that have to be true. Here's mine.
1. The rules live in a file, not in review comments
Every code review I've ever done, the majority of my comments were mechanical. This function is too long. This nesting is too deep. Why is this any?
Machines can say all of that. So I made them say it, as errors:
"max-lines-per-function": ["error", { max: 60, skipBlankLines: true }],
complexity: ["error", 20],
"max-depth": ["error", 4],
"max-nested-callbacks": ["error", 4],
Enter fullscreen mode Exit fullscreen mode
Plus eslint-plugin-sonarjs with cognitive-complexity as an error, and @typescript-eslint's strict preset — any banned, non-null assertions banned.
Nothing here is novel. What's different is the next part.
2. The rules are stricter than a human team would tolerate
This is the part I find genuinely interesting.
If you put those thresholds on a human team, you get a PR relaxing them within a week. Not because engineers are lazy — because "this function is 63 lines and splitting it makes it worse" is sometimes true, and arguing about it every time is exhausting.
Lint strictness has always been a trade-off between machine correctness and human patience. And loosening the rules was never really a technical decision. It was a social one.
An agent doesn't get annoyed. It reads the rule, splits the function, moves on. It has no opinion about being told to do it again tomorrow.
So the social cost went to zero, and once that happens the trade-off only tips one way. I turned everything up until it hurt, and nobody complained, because nobody was there to complain.
3. Exceptions live in the config with a reason, never inline
The moment rules get strict, real exceptions appear. If you allow // eslint-disable-next-line, the rules are dead within a month — that comment is invisible in review and permanent in practice.
So exceptions go in the config file, one line per reason:
files: [
"src/components/Sidebar.vue", // @keyframes — the "thinking" spinner ring
"src/components/GuiPanel.vue", // `.frame + .frame` sibling-combinator spacing
"src/components/FilesOverlay.vue", // :deep into CodeMirror's injected root
],
rules: { "vue/no-restricted-block": "off" },
Enter fullscreen mode Exit fullscreen mode
Every entry says why. And the comment above the block says: delete the entry when the reason goes away.
The difference is visibility. An inline disable is invisible. A growing allowlist in a config file is a thing you can look at and be embarrassed by.
4. The rules themselves are pure functions, and they're tested
This is the part most setups skip.
If a rule decides something — which file extension goes where, how a path is normalised, what counts as a valid session id — that decision is code, and code that is only exercised through a UI is code nobody tests.
So decisions get pulled out into pure functions in a shared module, and those functions get tested against the awkward cases: empty, null, boundary, wrong-cased, wrong-platform. That project currently has a few thousand test cases, and the vast majority are testing small pure functions rather than flows.
The point is not the count. The point is: when the rule is a pure function, the rule can be tested. When it's embedded in a component, it can only be reviewed. And I stopped reviewing.
5. CI runs on the OS your users have, not the one you have
Our whole team is on macOS. Our users are not.
So CI runs Linux and macOS on every PR, and Windows on a nightly schedule (it's slow, and daily is enough to catch drift).
This one paid for itself immediately, and not in the way I expected. It's the only environment where I can reproduce a Windows bug report at all. Before, a Windows issue meant asking the reporter to test my guesses. Now I push a branch.
We also write Windows-specific test cases deliberately — path separators, realpathSync behaviour, fs.watch differences — so that fixing one doesn't quietly break the others.
6. Something else reads the code — and it isn't the thing that wrote it
Claude Code writes. Then Codex reviews, and CodeRabbit reviews, and the loop runs until they stop objecting.
The mechanism that matters here isn't "AI review is good." It's that the writer and the reader are different models. A model reviewing its own output shares its own blind spots. Two different ones don't, mostly.
This is the closest thing to a replacement for what I stopped doing. It isn't as good as a careful human reviewer. It is much better than a tired human reviewer at 11pm on the fortieth PR of the day, which was the realistic alternative.
What I still look at
I want to be honest about the boundary, because "I don't review anything" would be a lie:
- UI changes. Nothing in the list above can tell me a layout is ugly or a flow is confusing.
- Anything I couldn't verify by running it. If the change is about behaviour under conditions CI doesn't reproduce, I go look.
- Anything touching auth, permissions, or data loss. The blast radius is wrong for automation.
Everything else, I let through on green.
The honest cost
Three things I'd want to know if I were reading this skeptically:
It front-loads a lot of work. None of the six items above is free. If you set up two of them and stop, you have strictness without a safety net, which is worse than neither.
It only works if the stack is uniform. The reason my per-repo config files are nearly empty is that every project uses the same language, the same test runner, the same CI shape. If your repos disagree with each other, you'll be writing the same rules over and over. Fix that first; it's cheaper.
Speed is not correctness. Things do get shipped fast and fixed fast. What this setup buys is not "no bugs" — it's that the bugs that survive are the ones review wouldn't have caught either.
The thing I actually took away
I set all this up to save time, and it did. But that isn't the interesting part.
The interesting part is that lint strictness, test coverage, CI breadth — the whole category of "engineering discipline we know we should do but don't" — was never really blocked on knowing better. It was blocked on how much friction a human team will absorb before it starts negotiating.
That constraint just got removed. Not gradually. It's gone.
I don't think most of us have updated for that yet.
Everything above runs in the open: my global config
and the project it runs on, both MIT.
Copy whatever's useful.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.