AI Coding

How to Review AI-Written Code Before It Merges

The pull request is twenty-seven lines long. The diff is clean, the tests pass, and the author is a language model. Somewhere between the green checkmark and the merge button, a human being has to decide whether those twenty-seven lines are correct. That decision is the whole job now, and most teams are doing it badly.

Code review has always been a social act dressed up as a technical one. A senior engineer reads a junior's work, catches the obvious bug, asks a question that reveals a missing edge case, and signs off. The reviewer carries context the author lacks: the weird legacy constraint, the customer who complained last month, the reason the retry logic exists. AI-written code breaks that arrangement in a quiet way. The author has no context to share, no intent to explain, and no shame to feel when the reviewer pushes back. The code arrives fully formed, confident, and plausibly wrong.

The result is that reviewers apply the wrong lens. They look for what is broken, the way they would with a human's draft. With machine code, the broken things are often invisible until production, and the things that look wrong are frequently fine. The review has to change shape.

The code that compiles and lies

A model generates code that satisfies the prompt as written. The trouble starts with what the prompt left out. Ask for a function that parses a date string, and the model returns something that handles ISO 8601 perfectly and throws on everything else. It is not wrong. It is narrow, and the narrowness is invisible in the diff because there is no diff for the cases the model never considered.

Human authors leak their assumptions into code in recognizable ways. They leave a comment about the timezone problem. They pick an ugly variable name because the domain term is ugly. They add a defensive check that seems paranoid until the reviewer remembers the incident that caused it. Machine code carries none of this baggage, which makes it look cleaner than it is. The reviewer reads a tidy function and assumes a tidy mind produced it.

The first practical move is to stop reading the diff as a finished artifact and start reading it as an answer to a question nobody wrote down. The reviewer needs to reconstruct the prompt, or ask for it if the author is another engineer who pasted a request into a chat window. What was asked for, and what was not. The gap between those two is where the bugs live.

Ask what the model was never told

A useful habit is to scan the new code for the assumptions it makes about the world outside its own function. Does it assume the input list is non-empty? Does it assume the network call succeeds? Does it assume the database row exists? Each assumption is a place where the model filled a gap with the most common pattern from its training data, which is the average of the internet, which is not the same as the specific system this code will run inside.

Reviewers who have spent years in one codebase carry a mental list of the ways this particular system is weird. The queue can deliver duplicates. The auth service returns a 200 with an error body. The timezone is stored as local time in one table and UTC in another because of a decision made in 2014 that nobody has had the courage to reverse. A model has never seen that list. It will write code that is correct in general and wrong here, and the reviewer's job is to spot the specific weirdness the code ignores.

This is tedious, and it does not scale to a diff of two thousand lines. Which is why the size of the change matters more than ever. A model can produce a large, coherent, plausible patch in seconds. A reviewer cannot verify it in seconds. The asymmetry is the problem. Teams that let the model generate large changes and then ask a human to bless them are running a lottery with a green checkmark stapled to it.

The diff size is the review budget

There is a hard limit on how much code a person can actually review in a sitting, and it is smaller than most teams admit. Beyond a few hundred lines of unfamiliar logic, attention flattens and the reviewer starts approving on vibes. The model does not get tired. The human does. Any workflow that treats generated code as free output and human review as an unlimited resource will collapse under its own asymmetry.

The fix is to constrain generation to reviewable chunks. Ask for one function, not a module. Ask for a fix to one bug, not a refactor plus a feature plus a test suite. The smaller the output, the more likely a reviewer will read every line and catch the quiet assumption. This runs against the grain of how people use these tools, which reward big sweeping prompts, but the merge gate is where the reward turns into risk.

A twenty-seven line diff can get a real review. A two thousand line diff gets a theatrical one. The reviewer scrolls, nods, maybe runs the tests, and clicks approve. The model generated the code in eleven seconds and the human verified it in ninety, and everyone involved knows the verification was a formality. That is the failure mode to design against.

Tests generated alongside the code inherit its blind spots

The most seductive part of the generated pull request is the tests. They pass. They look thorough. They cover the happy path, a couple of edge cases, and a sad path that returns the expected error. It feels like the model did the reviewer's job for them.

The tests are written by the same process that wrote the code, against the same understanding of the problem. If the model misunderstood the requirement, the tests encode the misunderstanding. They assert that the function returns what the function returns. Green tests prove the code is internally consistent, and internal consistency is cheap. A function that returns the wrong answer for the right reason will pass every test its own author wrote.

Reviewers need to read the tests as claims about the world, not as evidence of correctness. Does this test assert a behavior the business actually wants? Does the edge case list match the incidents the team has actually experienced? A generated test suite is a draft of the specification, and the reviewer's job is to check it against reality, not against the code.

One concrete tactic: pick a requirement from the ticket and write a mental test for it before reading the generated tests. If that test is missing from the diff, the suite has a hole regardless of how many green checkmarks it produces.

The confident wrong comment

Models write comments with the same fluency as code, and the comments are the most dangerous part of the diff. A human's comment is a gift to the future reader, usually written because the author struggled and wants to spare the next person. A generated comment explains the code at face value, in a tone of complete authority, whether or not the explanation is true.

A comment that says a lock is held for the duration of the transaction, attached to code that releases it early, is worse than no comment. It teaches the next reader something false. Reviewers skim comments because they trust them as metadata. With generated code, the comments need the same scrutiny as the logic, and the specific thing to check is whether each claim about side effects, ordering, or concurrency matches what the code actually does.

The reverse also happens. Code that is doing something subtle and correct arrives with no comment at all, because the model had no reason to flag it. A human would have left a note. The reviewer sees clean code, assumes it is simple, and moves on.

Name the author in the commit

Accountability is the quiet infrastructure of code review. When a person's name is on a pull request, that person has a reason to be careful, and the reviewer knows whose judgment to calibrate against. Generated code scrambles this. The commit says the name of whoever ran the prompt, but the actual author is a model, and the human's role was closer to transcription than authorship.

Teams should be explicit about this in the commit message or the pull request description. Not to shame anyone, but to give the reviewer accurate information about how much human thought went into the change. A reviewer reading a patch that a person wrote and understood will ask different questions than a reviewer reading a patch that a person prompted and skimmed. The merge gate needs to know which one it is looking at.

There is a second-order effect too. When generated code causes an incident, the investigation needs to reach the prompt and the context that produced it. A commit that says a model wrote this, prompted by a named engineer, with a link to the request, is a commit that can be debugged. A commit that pretends a person wrote it from scratch is a dead end at the worst possible moment.

The reviewer who reads the prompt first

The best reviewers of generated code have developed a habit that looks almost backwards. They read the prompt before the diff. They read the ticket, the chat transcript, the instructions the engineer gave the model, and they form an opinion about what the code should do before they see what it does. Then they read the diff and look for the places where the model's interpretation and their own diverge.

This is slower than reading the diff cold, and it catches the errors that matter. The model is very good at sounding like it understood. The reviewer's leverage is knowing what understanding was actually requested, and comparing that against the confident, tidy, plausibly correct code that arrived in its place.

Twenty-seven lines still take real work. The green checkmark does not mean the code is right. It means the author, whoever or whatever that was, believed it was right, and belief is exactly the thing under review.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button