When a team speeds up writing code with AI, the time to write goes down and the time to read does not. There are more pull requests, they are larger, the review queue grows, and the two most senior people on the team start spending half the day reading diffs. After a while those two get tired as well and a 900-line PR collects an "LGTM". The step that decides the quality of AI-produced code gets skipped at exactly the moment the least attention is available.
Our position: an AI review is a filter that runs before the human review and never stands in for it. Its job is to clear the mechanical layer before a person opens the PR: the swallowed error, the token in a log line, the missing test, the inconsistent name. That leaves the human's limited attention for design and intent. This article covers what a pre-review catches reliably, what it cannot catch, how to set it up, and how it can quietly damage a team's habits. The examples use a hypothetical travel booking app.
Why the review queue clogs first
Code generation parallelises: five developers can work with five agents at once. Review is serial work, limited by the number of people who actually know the module. When production speed rises and review capacity stays flat, one of three things happens: PRs wait for days, reviews get shallow, or the team silently invents the rule "small PRs don't really need a look".
One property of AI-written code makes this worse: it looks tidy. Names are consistent, formatting is clean, comments are in the right places. The reflex a human reviewer builds over the years ("this code is sloppy, I should look closely") never fires. The bugs are still there, they just arrive well dressed.
That is why the purpose of a pre-review is narrow and should stay narrow. Every minute a human reviewer spends writing "this catch swallows the error" is a minute not spent on "why does this retry exist". We hand the first kind of comment to the machine.
What an AI reviewer catches reliably
Current models read a diff and the files around it and find local bugs well. By "local" we mean cases where the evidence for the bug sits inside one or two files. Take this Kotlin code:
class BookingRepository(
private val api: BookingApi,
private val cache: BookingCache,
private val session: SessionHolder,
) {
suspend fun refreshBookings(): List<Booking> =
try {
val fresh = api.fetchBookings()
cache.replaceAll(fresh)
fresh
} catch (e: Exception) {
Log.w(TAG, "refresh failed, token=${session.authToken}", e)
cache.readAll()
}
}
A well-configured pre-review leaves two comments on this PR, along these lines:
[important] BookingRepository.kt:11
`catch (e: Exception)` also catches CancellationException. When the screen
closes and the scope is cancelled, the coroutine swallows the cancellation
and carries on reading the cache. Catch CancellationException separately
and rethrow it.
[important] BookingRepository.kt:12
The auth token is written to the log. Rule: REVIEW.md, "Always check", item 2.
Both are correct, and both are the kind of thing a person misses on a tired afternoon. The first is something the Kotlin documentation warns about explicitly: catching CancellationException can break cancellation propagation, and if you catch it you have to rethrow it. The list of work in this category is long, and in all of it we find AI review more consistent than a human, because it does not get bored:
- Swallowed or mishandled errors: the empty
catch, a payment failure that vanishes throughtry?, the ignoredfailurebranch of aResult. - Obvious security smells: personal data or tokens in logs, a hard-coded key, a request going out over
http://, a deep link parameter used without validation. - Inconsistency: three names for one concept (
reservation,booking,order), an error type that departs from the project's pattern, a half-finished rename. - Missing tests: a new branch with nothing covering it, or a test that exists and asserts nothing.
- Summarising the diff: what a 40-file PR does, which files are mechanical changes and which change behaviour.
Do not underrate the last item. A good summary tells the human reviewer where to start. It can also be misused, which we get to in the section on the human side.
What it does not catch: one false positive, two misses
The false positive: when the context is outside the diff
// BookingListViewModel+Refresh.swift
extension BookingListViewModel {
func applyRefreshed(_ bookings: [Booking]) {
items = bookings.map(BookingRow.init)
isRefreshing = false
}
}
A reviewer that sees only the diff writes a confident comment here: "items is UI state and this method may be called from a background context, wrap it in MainActor.run." But BookingListViewModel is declared @MainActor in another file and the extension inherits that isolation, so the comment is wrong. It costs the author five minutes to refute, and the third time it happens they stop reading AI comments. Most false positives follow this pattern: the claim is plausible and the evidence lives in a file the diff does not show. The remedy has two parts: let the reviewer read surrounding files, and add a rule to the instruction file that a behaviour claim must cite the file and line of the declaration.
The miss: every line right, the whole thing wrong
actor SeatHoldStore {
private var holds: [FlightID: SeatHold] = [:]
private let api: any BookingAPI
init(api: any BookingAPI) { self.api = api }
func hold(for flight: FlightID) async throws -> SeatHold {
if let existing = holds[flight], !existing.isExpired {
return existing
}
let fresh = try await api.createHold(for: flight) // suspension point
holds[flight] = fresh
return fresh
}
}
The word actor gives a feeling that concurrency has been taken care of, and AI reviewers fall for that feeling often. Actors are reentrant: as SE-0306 puts it, actor-isolated state can change across an await. If the detail screen and the payment screen call hold(for:) at the same time, both find nothing in the dictionary, both hold a seat on the server, and one overwrites the other. Two seats are now blocked in the user's name and one of them waits to expire somewhere nobody is looking. The fix is to store the in-flight Task in the dictionary so the second caller joins the same work. Finding this bug requires knowing which screens call the code and with what timing. Models catch it some of the time, and "some of the time" does not describe a safety net.
Lifecycle bugs belong to the same family: state lost on process death, a request that completes after its screen is gone, a flow cut in half by backgrounding. The evidence is in the runtime ordering and is not visible in the diff.
The miss: this code should not exist
suspend fun confirmBooking(draft: BookingDraft): Booking =
retry(times = 3, initialDelay = 500.milliseconds) { // project helper
api.confirm(draft) // POST /bookings
}
An AI reviewer will most likely write "add jitter to the backoff" here, which is a reasonable local suggestion. The real question is different: should a payment request without an idempotency key be retried at all? If the first request timed out on the client and succeeded on the server, the user has bought two tickets. Saying so requires knowing the backend contract and what the product promises.
What the misses have in common is that the information is not in the repository. Intent (which problem was this PR trying to solve, and is it the right one), cross-module design (a twin of this helper already exists in another module, this dependency points the wrong way), product behaviour (what should the empty list screen show), and team memory of the "we decided to remove that feature flag three months ago" kind. An AI reviewer is inclined to improve the code that is there. It rarely says the code should not be there.
An AI reviewer checks whether the code works as written. A human checks whether this was the thing to write.
The instruction file: few rules, clear severity, permission to stay silent
An AI reviewer on default settings produces polite, long comments that touch on everything. The instruction file is what makes it useful. It needs four things: what counts as important, what is never reported, the project-specific checks, and an instruction to say nothing when there is nothing to say.
On the context side you need three sources. The project rules and architecture notes, meaning the rule file your agents already read; we cover that in architecture guardrails for AI. The diff itself. And the surroundings of the diff: the declaration of the changed type, its callers, the related tests. A model given only the diff guesses, as in the @MainActor example above. This is why an agent-based reviewer that can read the repository produces noticeably fewer false positives than a script that posts the diff to an API in a single call.
The following file is a realistic starting point:
# Review instructions
This repo contains the iOS (Swift 6) and Android (Kotlin) code of a travel
booking app. Architecture rules are in CLAUDE.md; read it first.
Review only the lines this PR changes. Read surrounding files for context,
do not comment on them.
## Severity levels
- [important]: findings that would cause wrong behaviour, data loss, data
leakage or a crash if merged. Wrong logic, swallowed errors, personal data
or tokens in logs, coroutines that swallow cancellation, UI state written
off the main thread.
- [minor]: naming inconsistency, missing test, unnecessary complexity.
- Never comment on style, formatting or import order at any level.
## Reporting
- Do not report an assumption about the server contract or product behaviour
as a finding; add it to the end of the summary as a "question for the
human reviewer".
- Show evidence for any behaviour claim: file and line. Do not infer from
names. For isolation, thread or scope claims, read the file where the type
is declared.
- If you are not sure, do not write it.
- At most 3 [minor] comments. If there are more, give the count in the summary.
- If there are no findings, write one line: "No findings in the mechanical
layer." No praise, no general advice, no line-by-line narration of the diff.
- If the PR was reviewed before, write only new [important] findings.
## Do not report
- Anything SwiftLint, detekt or ktlint already catches.
- Generated code: **/Generated/**, *.pb.swift, anything under build/.
- Package.resolved, gradle.lockfile, .xcstrings files.
## Always check
1. Errors in payment and booking requests reach the user and are not swallowed.
2. Log lines contain no email, phone number, token or card data.
3. Feature modules do not import each other; only Core and DesignSystem.
4. Every new public function has at least one test.
5. Kotlin: where a suspend function catches a general Exception,
CancellationException is rethrown.
The file name and location depend on the tool; the content does not. Anthropic's managed Code Review reads REVIEW.md at the repository root as review instructions and CLAUDE.md files as project context. GitHub Copilot code review uses .github/copilot-instructions.md and the path-specific *.instructions.md files under .github/instructions, scoped with applyTo front matter. If you write your own workflow, you choose the name and point to it in the prompt. Keep the file short: every line you add reduces the weight of the lines that matter.
Wiring it into CI: a pre-review that runs when the PR opens
There are two ways to set this up. The first is a vendor's managed integration: assigning Copilot code review as a reviewer or configuring it to run automatically, or enabling Anthropic's Code Review for the organisation. Setup is easy and you control little. The second is running a review agent in your own CI, where you define the prompt, the triggers and the permissions. We recommend starting with the second, because the workflow file shows you line by line what is sent and what the agent is allowed to do.
The example below uses anthropics/claude-code-action@v1 on GitHub Actions. Input names and permissions match the review example in the action's documentation as of October 2026:
name: AI pre-review
on:
pull_request:
types: [opened, ready_for_review, synchronize]
paths:
- "**/*.swift"
- "**/*.kt"
- "**/*.kts"
concurrency:
group: ai-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review:
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
The PR branch is checked out in the working directory. Read
REVIEW.md first and follow its rules to the letter. Get the
changes with `gh pr diff` and read surrounding files as needed.
For findings tied to a line, use
`mcp__github_inline_comment__create_inline_comment`
(with `confirmed: true`). Post the summary as a single comment
with `gh pr comment`. Do not approve the PR, do not request
changes, do not modify code.
claude_args: |
--max-turns 30
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
The vendor-independent parts of the pattern are these. Draft PRs are skipped, because the author has not yet said "look at this". concurrency cancels the older run when pushes arrive in quick succession; otherwise three reviews race and all three leave comments. timeout-minutes and --max-turns put a ceiling on cost. Permissions are tight: with contents: read the agent cannot change code, and with --allowedTools it can only read the diff and write comments. id-token: write is there for the action's GitHub App authentication. If running on every push feels expensive, drop synchronize and make re-review something people ask for.
Two things need care. First, do not make the AI review a merge requirement, and do not let the bot's review count towards required approvals. Anthropic's managed product does this by design: findings do not approve or block the PR and the check always completes with a neutral conclusion. GitHub likewise tells you to supplement a Copilot review with a human one. Second, forks: GitHub does not pass secrets other than GITHUB_TOKEN to pull_request runs triggered from a fork, so this workflow does not run on fork PRs. Do not work around that by switching to pull_request_target; the GitHub documentation warns explicitly that running untrusted code on that trigger can grant unintended access to write privileges or secrets.
The equivalents are set up the same way. OpenAI's openai/codex-action and Google's google-github-actions/run-gemini-cli apply the same pattern with their own CLIs, and on GitLab or Bitbucket the same steps become a pipeline job. Whichever you pick, the three questions are identical: which file does it read its instructions from, which tools is it allowed, and where does it write the result.
Noise control: few comments, new code, evidence required
A pre-review is killed far more often by the useless comments it writes than by the bugs it misses. After three worthless comments in a row a developer does not read the fourth, and the fourth is the one saying the token ends up in the log.
The controls we apply:
- A comment budget. A limited number of minor findings per PR, the rest as a count in the summary. We put no limit on important findings; if a PR has ten of them, the budget is not what needs fixing.
- New code only. The reviewer talks about the lines this PR changes. Billing the author of this PR for five years of debt in the file they touched is the quickest way to stop people opening small fix-up PRs.
- An evidence requirement. A confidence threshold is hard to express as a number; "back the claim with a file and line, and if you cannot, do not write it" does the same job and can be audited.
- No overlap with linters. Having the model repeat what SwiftLint, detekt or ktlint already catch is both expensive and inconsistent. Any rule that can be checked deterministically belongs to a linter, and the model keeps only the work that needs judgement.
- Convergence on re-review. After the first round, only new important findings. Otherwise a one-line fix is still debating naming in round seven.
- A feedback loop. Every kind of comment that turned out wrong or useless comes back as a line in the instruction file. If your tool supports reactions on comments, use them; if not, read twenty dismissed comments once a month. The instruction file changes through PRs like code does, and it has an owner.
The last one is the most neglected. An instruction file tuned in week one and then left alone means, six months later, a bot producing comments nobody reads.
The human side: anchoring, laziness and the signature
The technical setup takes an afternoon. The hard part is watching how the team's behaviour changes. We see three kinds of decay.
The first is the reviewer anchoring on the AI summary. If the summary says "this PR adds refresh to the booking list, low risk", the reviewer starts reading the diff to confirm that sentence. A summary describes the author's intent and guarantees nothing about what the code actually does. Our advice is that the reviewer reads the PR description and the diff first and looks at the AI comments afterwards. Some teams put the AI summary inside a collapsed section, which is a small bit of friction that works.
The second is the author who stops reading their own code. A PR opened with "the bot will look at it anyway", especially when the author did not write part of the code either, is a PR nobody has read from top to bottom. Our rule: whoever opens the PR arrives having run the AI review on their own machine and closed the findings. The run in CI is the second sieve.
The third is accountability. "The AI review passed" cannot be an approval. The author and the approving human are responsible for merged code; the bot has no signature and should not have one. Put that in writing: one sentence in the team's AI usage policy is enough.
We keep what the human reviewer must still cover in a short list. Google's engineering practices guide says the first thing to look at in a review is design, so the list starts there.
- Does this change solve the right problem, and does the code match the intent in the PR description?
- Should this code exist? Is there a twin in another module, or a simpler way?
- Do module boundaries and dependency direction follow the design?
- Concurrency and lifecycle: state that can change across an
await, cancellation, process death, backgrounding. - Product behaviour: error, empty and loading states, back navigation, offline.
- Do the tests check the right thing, or do they merely exist?
- What are the answers to the assumptions the AI left as "questions for the human reviewer"?
Code leaves the building: what goes, where it stays, who sees it
When a review agent runs, the model provider receives the prompt and instruction files, the diff, the contents of every file the agent reads, the output of the commands it runs, and the PR title and description. In the setup above the repository is checked out on the runner, and whichever file the agent opens becomes part of an API request. For an agent-based reviewer, the assumption that only the diff is sent is wrong.
Where it stays depends on your contract and differs between vendors; the text your legal team needs to read is the data usage document and the agreement. As an example, Anthropic's documentation as of October 2026 says that under commercial terms (Team, Enterprise, API) code and prompts are not used for model training unless the customer explicitly opts in, that standard retention is 30 days, and that zero data retention (ZDR) is enabled per organisation for qualified enterprise accounts. Consumer plans have different defaults, which is why everyone running reviews from a personal account is a bad idea. Who sees the results matters too: comments are visible to anyone who can see the PR, and workflow logs to anyone who can read the repository.
If sending code straight to a vendor's API is not acceptable, the options are:
- Route inference through your own cloud account. The same action runs on Amazon Bedrock, Google Cloud and Microsoft Foundry through the
use_bedrock,use_vertexanduse_foundryinputs, and other vendors offer similar routes. The data then falls under the terms of a cloud provider you already have a contract with. - A zero data retention agreement. Be aware that some managed features do not work with it. Anthropic's managed Code Review is not available to organisations with ZDR enabled; the action in your own CI is.
- An open-weight model on your own hardware. The code goes nowhere; in return review quality drops and you operate the infrastructure. Do not decide before measuring quality on a sample from your own PR history.
- Narrow the scope. Use the
pathsfilter to keep directories such as payments and authentication out of AI review and require two human approvals there.
There is one more risk most teams skip: the content of a PR is untrusted input. A comment line in the diff or the PR description can tell the model to "ignore the previous instructions". This is why the agent's permissions must be narrow (no write access, no free shell), the instruction file needs an owner through CODEOWNERS, and fork PRs must not reach secrets.
How to tell whether it works
"The bot wrote this many comments" cannot be a success measure; noise raises the comment count too. These are what we track, all of which can be extracted from your own PR history:
- The share of comments that led to action. Once a month, on a sample of twenty PRs, sort the AI comments into three groups: led to a code change, ignored, wrong. If the wrong and ignored share is growing, go back to the instruction file.
- The kind of comments humans write. On the same sample, label human comments as "mechanical" or "design, intent, product". If the pre-review is doing its job, the mechanical share falls. If it does not, either the bot is not catching things or people are repeating the bot.
- Time to first human review and the number of review rounds. Both come out of your Git host's API. If the round count is not falling, mechanical fixes are still being made in the human round.
- Escaped bugs. One question for every bug that reaches production: which layer should have caught this? Linter, test, AI review, human review. Those assigned to the AI layer become a check in the instruction file, and those assigned to the human layer go on the reviewer list.
To place these measurements in a wider frame, see measuring AI productivity.
Where to stop: for a team of two or three opening a handful of PRs a day, a CI-wired pre-review is usually too much. Having the author run the same review locally before opening the PR is enough. The CI integration pays for itself once the review queue really is waiting and the "whoever opens the PR looks first" rule has become hard to enforce by hand.
Checklist
- The AI review runs before the human review; the bot's output does not count towards required approvals and does not block the merge.
- The repository has a review instruction file: severity levels, a do-not-report list, project-specific checks, and the "one line if no findings" rule.
- The instruction file has an owner (
CODEOWNERS) and changes through PRs. - The reviewer can read the files around the diff, and behaviour claims require file and line evidence.
- Everything the linters catch is out of scope for the AI review.
- Minor findings have a comment budget; on re-review only new important findings are written.
- The workflow skips draft PRs, cancels stale runs, and has a time and turn limit.
- The agent's permissions are narrow: contents read-only, an explicit tool list, fork PRs cannot reach secrets.
- What goes to the provider, how long it is kept and whether it is used for training is written down; a decision has been made for sensitive directories.
- The author runs the review locally before opening the PR.
- The human reviewer list (intent, should it exist, boundaries, concurrency, product behaviour) sits in the PR template.
- Once a month, a twenty-PR sample is checked for the share of comments acted on and the kind of comments humans write.
Need support on this?
Let's review together whether these controls are set up correctly in your app. We're here with security audits and advisory.
Sources
- Claude Code Docs · Claude Code GitHub Actions
- Claude Code Docs · Code Review
- anthropics/claude-code-action · Solutions guide
- Claude Code Docs · Data usage
- GitHub Docs · About GitHub Copilot code review
- GitHub Docs · Adding repository custom instructions for GitHub Copilot
- GitHub Docs · Events that trigger workflows
- Google Engineering Practices · What to look for in a code review
- Kotlin · Cancellation and timeouts
- Swift Evolution · SE-0306 Actors
Comments and questions
Disagree with something, or stuck on this in your own project? Write to us and let's talk.