· 7 min read
A Practical Guide to Reviewing AI-Generated Code Before You Merge
By T. Yang
- tools
Review AI-generated code by verifying behavior and boundaries before you debate style: read the task, inspect the diff, trace inputs to side effects, then run a test that could actually fail. Don’t merge because the agent produced a clean-looking patch and CI is green; merge when you can explain what changed, why it is safe, and what would catch it if it were wrong.
Start with the requested outcome, not the implementation
An agent is very good at producing code that resembles an answer. That is not the same thing as satisfying the ticket. Before opening the diff, write one sentence in your own words: “When X happens, Y should happen, while Z must remain unchanged.” If you can’t write that sentence, you’re not ready to approve a patch—especially one written quickly by a model.
Then read the PR description, linked issue, and acceptance criteria. GitHub’s own review guidance starts with understanding the motivation for the pull request, which sounds obvious until you are staring at 400 lines of plausible TypeScript. A concrete intent statement lets you spot the classic AI failure mode: it solves the nearby problem instead of the requested one.
For example, a request to reject expired password-reset tokens may yield a patch that checks token age in the controller. That might pass a happy-path test while leaving the API endpoint, background worker, or alternate reset flow unchanged. Your job is to find every route to the behavior, not admire the new conditional.
Get a map of the patch before reading it line by line
Don’t begin with the first changed line. First ask what files moved, what dependencies changed, and whether the patch has touched a trust boundary: auth, billing, persistence, file handling, permissions, secrets, deployment configuration, or outbound HTTP. AI agents often make broad edits because they optimize for completing the request, not preserving your team’s preferred seam between layers.
From a checkout of the branch, these are useful first commands:
git diff --stat origin/main...HEAD
git diff --check origin/main...HEAD
git diff --name-only origin/main...HEAD
gh pr checks --requiredgit diff --stat tells you whether “add a validation” quietly became a 19-file rewrite. git diff --check catches whitespace errors. gh pr checks --required shows the required CI checks for the current branch; add --watch if you want it to wait, using its default 10-second refresh interval. These commands don’t establish correctness, but they stop you from reviewing a fictional scope or approving a branch whose required checks have not finished.
On GitHub, review one file at a time and mark it viewed. That is mundane, but it prevents the easy mistake of treating a long, generated diff as one visual object. Also inspect manifest and lockfile changes directly. Dependency review can surface known vulnerabilities, but GitHub explicitly notes that it may not capture every relevant source-diff change or every dependency format.
Trace data, authority, and side effects
Now read the changed code as a chain: where does data enter, what transforms it, what policy is applied, and where does it end up? You are looking for mismatches between the new behavior and the system’s existing contracts.
Spend most of the review time at boundaries. Ask: Is this value user-controlled? Is validation server-side? Does an authorization check happen for this specific resource, not merely for the account? Does a retry create duplicate writes? Does logging expose a token or an email address? Does an error path leave partially completed work behind? OWASP’s secure-review guidance recommends exactly this sort of source-to-sink and trust-boundary analysis because automated checks often cannot determine whether application-specific authorization and business logic are correct.
This is where generated code is often weakest. It may use the right library call with the wrong ordering, accept a default that conflicts with your policy, or add an authorization check at the UI layer while the API remains callable. It can also preserve an old bug because it copied the closest existing pattern. “It matches the surrounding code” is evidence of consistency, not correctness.
Run one test designed to disprove the patch
A green full suite is useful, but it is not a review strategy. Pick the most likely wrong assumption and try to break it locally. If the patch adds tenant filtering, call the endpoint as a user from tenant A using an ID from tenant B. If it changes a parser, feed it malformed input and an oversized input. If it adds a retry, force the first request to fail after the side effect begins.
Use the project’s real test command rather than inventing a parallel harness. In a JavaScript repository that may be npm test -- --runInBand; in a Go service, it may be go test ./internal/payments/...; in a Python project, a focused pytest tests/api/test_tokens.py -k expired is usually more informative than a broad rerun. The point is not the command. The point is that the test must distinguish the intended fix from a convincing fake.
If the generated change has no test, request one when the behavior could regress silently. That includes permission checks, parsing, money, destructive operations, migrations, caching, concurrency, and bug fixes. Don’t demand tests for every rename or mechanical cleanup; that turns a good rule into review theater.
Use the agent as a second pair of eyes, not as the approving authority
It is reasonable to ask an AI tool to summarize the diff, list changed call paths, propose edge cases, or explain an unfamiliar library. It is not reasonable to accept “no issues found” as evidence. The model that generated the patch has no privileged view of your production traffic, unstated business rules, or the weird incident from eight months ago that led to a particular guard clause.
Give it bounded questions instead: “List every new database write in this diff,” “Which changed paths accept untrusted input?”, or “Propose three tests that would fail if cross-tenant access were possible.” Then verify the answer against the code. This is faster than asking for a generic review because you retain control of the hypothesis.
Leave comments that name the risk and the proof you need
Avoid comments like “Can we simplify this?” when the real concern is behavior. Write: “This checks membership after loading the invoice by ID. Can an authenticated user enumerate invoices outside their organization? Please enforce the organization constraint in the query and add a cross-org test.” The author—human or agent operator—now has a testable request.
Use “request changes” for issues that can cause incorrect behavior, data exposure, security regressions, broken upgrades, or operations pain. Use a normal comment for questions and optional improvements. GitHub supports line comments, suggested edits, and explicit approve/request-changes decisions; branch protection or rulesets are what make a requested change block merging, so confirm your repository settings rather than assuming the button alone is a gate.
The practical bar is simple: you should be able to describe the patch’s behavior, name its critical boundary, and point to a test or check that would catch its most expensive failure. If you can’t, the patch is not ready—regardless of whether it came from a teammate, a coding agent, or 30 seconds of autocomplete.