To review a large pull request, read the description and write down what a correct change must do, group the files by area, then read them in dependency order, one change at a time, checking for what's missing as well as what's there. Comment as you go, stop when you're tired, and finish with a summary of what you checked and what you skipped.
You open the pull request and GitHub says 40 files changed. The easy thing is to scroll, leave a couple of comments about naming, and approve. Most of us have done it. The trouble is that big changes are where the expensive bugs hide, and scrolling finds very few of them.
Here's a method for reviewing a large pull request properly. It takes longer than skimming. It takes less time than reviewing the same change a second time because the first pass missed something.
Does this apply to pull requests an AI wrote?
Yes, and it matters more there. An AI-written pull request tends to be larger, and its author may not know every line, so you can't rely on asking them. The steps below don't depend on who wrote the code. How to review a pull request that AI wrote adds what to do differently, and Reviewing code an AI wrote lists the mistakes to look for.
First, ask whether it should be one pull request
Before you read any code, look at what the pull request says it does. If the description lists unrelated things, such as "adds CSV export, fixes the timezone bug, renames the settings module", the most useful review might be one comment asking to split it. A rename across 30 files hides a 10-line logic change very well.
Splitting isn't always possible. A migration and the code that depends on it often have to land together. Still, ask early, before you've spent an hour on it.
Read the description and the issue
Read what the author says the change does, then read the issue or ticket behind it. You want answers to two questions:
- What problem is this solving, in one sentence?
- What would a correct solution have to do?
Write the answer to the second question down as a short list before you open the diff. For a CSV export feature it might be:
- People can export a report as a CSV file.
- Large reports don't time out.
- Only people who can see a report can export it.
This list is what you'll check the code against. A diff shows you what the code does. It can't show you what the code should have done and didn't, so you have to bring that with you.
If the description is empty, ask the author for a few sentences before you start. That's a fair request on a 40-file change.
Get the shape before you read lines
Spend five minutes on the file list without opening anything. Sort the files into rough groups:
- Data: migrations, schemas, models.
- Logic: services, jobs, helpers.
- Edges: API handlers, routes, CLI commands.
- Interface: components, templates, styles.
- Tests.
- Noise: lockfiles, generated code, snapshots, pure renames and formatting.
The file tree on GitHub's Files changed tab works for this. So does git diff --stat in a terminal:
$ git diff --stat main...csv-export
db/migrations/0042_export_jobs.sql | 24 +
src/exports/job.ts | 118 +
src/exports/csv.ts | 86 +
src/routes/reports/[id]/export.ts | 41 +
src/components/ReportHeader.tsx | 19 +-
package-lock.json | 212 +-
...
Now you know where the weight is. Mark the noise so you can skip it later without guilt. A lockfile deserves a glance to see which packages moved and why, and nothing more.
Read in dependency order
GitHub lists files alphabetically. That order has nothing to do with how the change works, which is why reading a big pull request from top to bottom feels like reading a book with the chapters shuffled.
Read from the bottom of the stack up instead:
- Data first. The migration or schema tells you what the change stores and what it assumes about the rows that already exist.
- Then the logic that uses that data.
- Then the edges that call the logic.
- Then the interface.
- Tests alongside the code they test, or at the end.
By the time you reach a component, you know what the data looks like and what the service returns. You can review the component for its own job, instead of reverse-engineering the whole feature from its props.
Some people prefer the opposite direction: start at the user-facing entry point and follow the calls down. That works too. What matters is picking an order that follows the code's dependencies and sticking with it.
Read one change at a time
Inside a file, treat each changed section as its own small question. What does this change do, and why is it here? If you can't answer the why, that's worth a comment even when the code looks fine.
Check out the branch and read in your editor if you can. You get go-to-definition, find-references and the whole file around each change. Plenty of bugs sit in lines the pull request didn't touch: a caller that now passes the wrong thing, or a second code path that needed the same fix and didn't get it.
gh pr checkout 1234
# or, without the GitHub CLI:
git fetch origin pull/1234/head:pr-1234 && git switch pr-1234
Don't try to hold a large file in your head at once. Read one change, note what you think, move to the next.
This is the part we built Hyperpowers for VS Code for. Your Claude Code reads the pull request first, then the extension takes you through it one change at a time, in the order the code runs, opening each change in the editor beside a short note on what changed and why. You can do all of this by hand, and the method stays the same. How to review large GitHub pull requests in VS Code shows how the extension handles each step.
Look for what's missing
The hardest bugs to catch in review are the ones with no lines. Go back to the list you wrote from the description and check each item against the code. Then ask the questions a diff never prompts:
- What happens when it fails? The network call times out, the file is empty, the user isn't signed in.
- Does every new endpoint check permissions?
- Does the migration handle existing rows, including the odd ones?
- Is there a test for the exact case this change exists to fix?
- If a function's signature changed, what else calls it? Search for it.
- Do docs, config or feature flags need to change too?
In the CSV example, the third item on the list ("only people who can see a report can export it") is the kind of thing that's easy to forget and leaves no trace in a diff. Here's a new route that looks fine line by line:
// src/routes/reports/[id]/export.ts
export async function GET(request: Request, { params }: { params: { id: string } }) {
const report = await db.reports.find(params.id)
const csv = await buildCsv(report)
return new Response(csv, { headers: { 'content-type': 'text/csv' } })
}
Every line is correct. The bug is the permission check that isn't there, and anyone who can guess a report ID can download it.
Comment while you read
Write comments as you go, while the context is fresh, and keep them pending until you're done. On GitHub, "Start a review" does this. Your comments stay private until you submit, so you can delete the ones a later file answers.
Comments on a big review are more useful when they:
- Say how much they matter. A prefix like "blocking:", "question:" or "nit:" tells the author what to fix first.
- Say why. "This runs one query per row" helps more than "this is slow".
- Ask when you're unsure. "What happens here if
reportis null?" gets an answer. "This is wrong" gets an argument. - Suggest the fix when it's small. GitHub's suggestion blocks let the author apply it with one click.
Keep the nits few. On a 40-file change, twenty naming comments bury the one about the missing permission check.
Split the review across sittings
Attention runs out. If a review is going to take more than an hour or so, stop at a natural boundary, like the end of the data layer, and come back later. Write down where you stopped. Reading the interface code fresh beats skimming it at the end of a long session.
If the author pushes new commits in between, look at what changed since your last pass instead of starting over. GitHub can show you only the changes since your last review from the commit picker on the Files changed tab. Hyperpowers also keeps your place for each pull request, marks the files the new commits changed, and explains only those files again when you press Update.
Write a short summary
When you submit, put two or three sentences at the top of the review:
- What you think the change does, in your own words. If you've got it wrong, the author will say so, and that's useful too.
- Which comments block merging, if any.
- What you didn't review. "I skipped the generated API client and the snapshot updates" is honest, and it tells the next reviewer where to look.
Then pick approve, comment or request changes to match what you wrote. An approval that carries five blocking comments leaves the author guessing.
A checklist for next time
- Ask whether it should be split.
- Read the description and the issue, and write down what correct looks like.
- Group the file list and mark the noise.
- Read in dependency order, starting with data.
- Read each change on its own, in your editor if you can.
- Check for what's missing: failures, permissions, existing data, tests, other callers.
- Comment as you go, and say how much each comment matters.
- Stop when you're tired, and come back.
- Write a summary that says what you checked and what you skipped.
None of this makes a 40-file pull request small. It gives you an order to read it in and a list to check it against, and those two things are most of what a skim is missing.
To try the method on an example pull request, open Hyperpowers in your browser, or install the extension for your own pull requests.
Common questions
How long should a large code review take?
As long as it takes to answer what the change does and whether it should merge. If it runs past an hour or so, split it across sittings at a natural boundary, such as the end of the data layer.
Should I ask the author to split the pull request?
Ask early when the description lists unrelated changes. Some changes, like a migration and the code that depends on it, have to land together.
What order should I read the files in?
Data first, then the logic that uses it, then the edges that call the logic, then the interface, with tests next to the code they test.