Approve, request changes, or admit you did not read it
Your review tool offers two buttons, and only one of them unblocks a colleague. When a reviewer has not read the diff, the tool leaves them with a choice between lying and being the obstacle. Give them a third option and the risk becomes visible.
A reviewer opens a pull request at 4:40pm. It is 900 lines, mostly generated, in a subsystem they know slightly. The author is blocked and has asked twice. The tests are green.
They have two buttons. Approve, which unblocks a colleague and asserts something untrue. Request changes, which asserts something equally untrue and makes them the person holding up the release for reasons they cannot articulate. There is no button that says what is actually the case, which is that this change is probably fine, nobody has read it, and the team should know that.
My position is that most rubber-stamping is not a failure of diligence. It is the predictable output of a two-state system where only one state unblocks. Add a third, honest state and you convert invisible risk into visible risk, which is the only kind you can manage.
The two buttons encode a world that no longer exists
Approve was designed to mean “I have read this and I am putting my name to it”. That meaning was sustainable when a reviewer received four changes a week, all written by colleagues whose reasoning they could reconstruct from the diff.
The volume assumption is gone. So is the reconstruction assumption: a generated change of 900 lines does not carry an author you can ask, and the pull request description was written by the same process that wrote the code.
LinearB’s 2026 benchmarks show what happens to a two-state system under that pressure. Pickup time on agentic pull requests is 5.3 times longer. Median review duration is up 441.5 percent. And pull requests merged with zero review are up 31.3 percent. That last figure is the system finding its own third state, in the only form available: skipping the process entirely and recording nothing about the decision.
Two states, under load
- Approve means "read and endorsed" or "did not read", indistinguishably
- Request changes is socially expensive, so it is reserved for real objections
- The honest position has no button, so it becomes a merge with no review at all
- Nothing downstream can tell the cases apart
Three states
- Approve keeps its original meaning and stays rare enough to mean it
- Request changes stays for genuine objections
- Accepted unread is available, cheap, and recorded
- The share of each is a number you can put on a dashboard
What the third state has to be
Three properties, and all three are load-bearing.
It has to unblock. If it does not merge the change, nobody will use it and everyone will go back to approving. This is the property people resist most and it is the reason the idea works. You are not building a gate, you are building an honest label on a decision that is already being made.
It has to be socially cheap. The reviewer using it is not confessing to negligence. They are recording a triage decision made under a queue they did not create. If the culture treats it as an admission of failure, usage goes to zero within a fortnight and you are back to two buttons. Leadership has to be the first to use it, visibly, on their own changes.
It has to carry a reason code, not an essay. Out of my area. Volume. Deadline. Low blast radius, sampled later. Four options in a dropdown. The moment it requires prose it becomes slower than approving, and speed is the whole point.
What it changes downstream
Once the state exists, three things become possible that were not before.
You can sample properly. Post-merge sampling is the only review model that survives volume, and it needs a population to sample from. “Everything merged unread in the payments module this week” is a sampling frame. “Everything approved” is not.
You can route. If a change was accepted unread because it was outside the reviewer’s area, that is a routing failure with a name, and it is fixable by changing the default reviewer list rather than by exhorting people to try harder.
And you can have an honest conversation about capacity. A quality slide showing 98 percent review coverage ends the conversation. A slide showing that 30 percent of merges in a critical subsystem were accepted unread starts one, and it starts it with a number rather than with a senior engineer’s uneasy feeling, which is the currency that has never once won a resourcing argument.
Where this breaks down
The strongest objection: you will legitimise not reading. Make the honest option cheap and available and some teams will drift to using it for everything, because it is faster and now blessed. The rubber stamp at least carried a residual sense that something was owed. I think this is the real risk, I do not think it is avoidable by design, and the mitigation is entirely cultural: the number has to be watched by someone senior and it has to have a ceiling in high-risk areas. If nobody watches it, this makes things worse.
Tools do not have this button. GitHub, GitLab and the rest give you approve and request changes. Everything I have described is a convention layered on labels, a comment template, or a merge-queue field, and conventions decay. A team that cannot hold a labelling convention for two quarters should not build a risk practice on one.
Some organisations cannot record this. In a regulated change-control regime, a field saying “merged without reading” is a document that will be read back to you in an audit, and your compliance function may reasonably refuse. The counter-argument is that the alternative is not less risk, it is undocumented risk, but I have watched that argument lose to a legal team and it will lose again.
The zero-review number may already be enough. If you are only going to manage one figure, the rate of merges with no review event is simpler, harder to game, and available today with no convention to maintain. My third state is more informative and more expensive to sustain. For a team of fifteen, the simpler number is probably the right call.
It assumes the reviewer knows they did not read it. The uncomfortable middle case is skimming: forty seconds, the diff scrolled, a genuine belief that it was reviewed. There is no button for self-deception, and the honest-option design does nothing about the largest category of shallow review.
The takeaway
Rubber-stamping is what a two-state system produces when volume rises. The reviewer is not being lazy; they are being handed a choice between an untrue statement and blocking a colleague, and they are choosing the one that keeps work moving.
Give them a third option that unblocks, costs nothing socially, and gets recorded. You will not reduce the amount of unread code merging next month. You will find out how much of it there is, which is the precondition for every other thing you might do about it.
If you take one thing into next week: ask three of your reviewers, privately, what fraction of their approvals last month involved reading the whole diff. Whatever they say, that is your real coverage number, and it is not the one on your dashboard.