teachyou.ai academy
← All posts
Claude Code

Claude Code for Code Review: Catching Bugs Before Merge

Ira Menon · Jun 24, 2026 · 15 min read

You already know the feeling. A pull request sits open for two days because the one senior engineer who "really understands the auth module" is on vacation. Meanwhile three other PRs stack up behind it, each one nervously rebasing onto a base branch that keeps moving. Code review is supposed to be the safety net that catches bugs before they hit production, but in practice it's usually the most rushed, most inconsistent step in the entire software lifecycle. Reviewers skim. Reviewers approve with "LGTM" after reading the diff for ninety seconds. Reviewers miss the null check that's missing on line 47 because they're mentally reviewing the function name, not the actual control flow.

Claude Code changes what's possible here, not by replacing your human reviewers, but by giving every pull request a first pass from something that reads every line, traces every call site, and never gets tired at 6pm on a Friday. This article walks through how to actually wire Claude Code into your review workflow — from a quick terminal pass on a feature branch to a full CI-gated review step — with real examples of the kinds of bugs it catches that humans routinely miss.

Why code review keeps failing in practice

Before getting into the mechanics, it's worth being honest about why code review is broken at most companies. It isn't that reviewers are lazy or incompetent. It's that code review requires holding an enormous amount of context in your head simultaneously: the diff itself, the surrounding code it touches, the call sites that depend on it, the test coverage (or lack of it), and the historical reasons certain patterns exist in the codebase. Humans are bad at holding that much context under time pressure.

A few patterns show up again and again:

  • Reviewers approve diffs based on the description in the PR, not the actual code, because the description sounds reasonable
  • Off-by-one errors, missing await keywords, and incorrect error handling are invisible on a quick skim
  • Security issues like SQL injection or missing input validation get missed because the reviewer is focused on architecture, not edge cases
  • Nobody checks whether a "small fix" quietly changed the behavior of a shared utility function used in twelve other places
  • Review comments devolve into style nitpicks (tabs vs spaces, variable naming) because that's what's easy to spot, while the actual logic bug sails through
  • Large PRs get approved faster than small ones, because reviewers subconsciously assume "the author clearly put in the effort, it's probably fine" — when in fact large diffs are exactly where bugs hide best
  • The reviewer assigned by the code owners file is often the person with the least available time that week, not the person with the most relevant context

There's also a structural problem: review quality degrades as the queue grows. A team that ships five PRs a day can give each one careful attention. A team that ships thirty PRs a day, across four time zones, with reviewers who are also trying to ship their own features, ends up with review as a formality rather than a safeguard. The bottleneck isn't a lack of talented engineers — it's that thorough review takes sustained attention, and sustained attention is the one resource that doesn't scale with headcount.

Claude Code is well suited to exactly the kind of work reviewers skip: mechanically tracing every branch, cross-referencing every function call against its definition, and checking a diff against the entire codebase rather than just the eight lines that changed. It does this because it can actually run commands, read files, execute the test suite, and reason about a codebase the way a very patient senior engineer would if they had unlimited time and never got bored.

Setting up Claude Code as a review tool

The simplest entry point is running Claude Code directly against a local branch before you even open a pull request. If you have the CLI installed, navigate to your repository and run it against your working changes:

cd my-project
git diff main...feature/checkout-refactor > /tmp/review.diff
claude "Review the diff in /tmp/review.diff for correctness bugs, \
race conditions, and missing error handling. Be specific about \
line numbers and explain why each issue is a problem."

This alone is already more useful than most first-pass human reviews, because Claude Code will actually open the files referenced in the diff, follow imports, and check whether a function signature change breaks any callers. But the real value comes from treating this as a repeatable step in your workflow rather than a one-off favor you ask for occasionally.

A more durable setup is a project-level slash command that your whole team can invoke. Drop a file like this into .claude/commands/review-pr.md:

Review the current git diff against the base branch for:
1. Logic bugs and incorrect conditionals
2. Unhandled promise rejections or missing awaits
3. Resource leaks (unclosed connections, listeners, timers)
4. Security issues (injection, auth bypass, secrets in code)
5. Breaking changes to function signatures used elsewhere

For each issue, cite the file and line number, explain the bug
in one sentence, and suggest the minimal fix. Do not comment on
naming or formatting unless it causes a real bug.

Now anyone on the team can type /review-pr before pushing a branch, and they get a consistent, opinionated first pass every single time — not dependent on whichever teammate happens to be free.

A real example: catching a race condition

Let's walk through something concrete, because "catches bugs" is a vague promise until you see it happen. Say a teammate submits this change to a payment processing service, moving from a synchronous balance check to an async one for a new "instant credit" feature:

async function processRefund(userId, amount) {
  const account = await getAccount(userId);

  if (account.balance < amount) {
    throw new Error("Insufficient balance for refund reversal");
  }

  account.balance -= amount;
  await saveAccount(account);
  await logTransaction(userId, "refund", amount);

  return { success: true, newBalance: account.balance };
}

At a glance this looks fine. It reads clean, the error handling is present, and the tests (if there are any) probably just check the happy path. But if you ask Claude Code to review this in the context of the surrounding service, here's the kind of finding you get back:

> This function has a read-modify-write race condition. Between getAccount (line 2) and saveAccount (line 8), there is no lock or optimistic concurrency check. If processRefund is called twice concurrently for the same userId — which is plausible if a client retries a timed-out request — both calls will read the same starting balance, both will pass the balance check, and the second write will overwrite the first, silently losing one of the two deductions. This is a double-spend bug. Recommend either a database-level row lock (SELECT ... FOR UPDATE), an atomic decrement at the database layer, or a version column with optimistic locking that rejects the second write.

That is a genuinely hard bug to catch in a normal review, because the code is syntactically correct, the logic is correct in isolation, and it only breaks under concurrency that isn't visible in the diff at all. A human reviewer would need to already be thinking about concurrent request handling to catch this. Claude Code catches it because it's not skimming — it's actually reasoning about what happens when this function runs twice at once, which is a question worth asking of any function that reads state, checks it, and writes it back.

Catching what tests don't cover

Another underrated use of Claude Code in review is cross-referencing the diff against the test suite, not just the code. It's common for a PR to include tests that pass, and for the reviewer to see green checks and move on — without noticing that the new tests don't actually exercise the interesting cases.

Here's a prompt pattern that works well for this:

claude "Look at the diff in this PR and the corresponding test file. \
Tell me which edge cases in the new code are NOT covered by the \
new or existing tests. Focus on error paths, boundary values, and \
any conditional branch that isn't hit by at least one test."

For a function that validates a discount code, this kind of prompt regularly surfaces gaps like:

  • The tests cover a valid code and an expired code, but never test a code that's valid but already used up its redemption limit
  • Nothing tests what happens when the discount would bring the total below zero
  • The tests all use codes that are already uppercase, so there's no coverage for whether the comparison is case-sensitive when it shouldn't be

None of these are things a reviewer would notice by reading the diff top to bottom. You notice them by asking "what input would break this," which is exactly the kind of adversarial thinking that's easy to skip when you're trying to get through your review queue before lunch.

It's also worth pointing Claude Code at test quality itself, not just test coverage. A test suite can have 100% line coverage and still be nearly worthless if every assertion just checks that a function didn't throw, without checking that it returned the right value. A useful follow-up prompt is to ask Claude Code to identify tests that would still pass even if the underlying logic were subtly wrong — tests that assert expect(result).toBeDefined() instead of checking the actual computed value, for instance. Weak assertions like this are extremely common in codebases that adopted a "must have tests" policy without a matching "tests must actually test something" policy, and they're almost invisible during a normal review because the test file has the right shape, just not the right substance.

Wiring Claude Code into CI as a review gate

Running reviews manually is useful, but the real leverage comes from making it automatic. Most teams wire Claude Code into their CI pipeline so every pull request gets an automated review comment before a human even looks at it. A minimal GitHub Actions setup looks like this:

name: claude-review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Run Claude Code review
        run: |
          git diff origin/${{ github.base_ref }}...HEAD > diff.txt
          claude -p "Review diff.txt for bugs, security issues, and \
          breaking changes. Output findings as a markdown list with \
          file:line references. If there are no issues, say so \
          explicitly." > review-output.md

      - name: Post review as PR comment
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const body = fs.readFileSync('review-output.md', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: body
            });

The important design choice here is scope. This pipeline doesn't block merges by itself — it posts a comment. That distinction matters. Automated review should augment your human reviewers with a fast, thorough first pass, not become a gate that blocks legitimate work over a false positive. Teams that try to make an automated review step a hard merge blocker on day one usually end up disabling it within a month because it's too noisy. Teams that treat it as "another very thorough reviewer commenting on the PR" tend to keep it running indefinitely, because it genuinely saves review time without adding friction.

Reviewing for security, not just logic

Security issues deserve their own pass, because they require a different mindset than "does this function do what it says." A logic bug review asks whether the code does what the author intended. A security review asks what happens when someone actively tries to misuse it. These are different enough that it helps to run them as separate prompts rather than hoping one generic "review this" pass catches both.

A security-focused review prompt looks like this:

claude "Review this diff specifically for security issues: \
injection (SQL, command, template), missing authorization checks, \
insecure deserialization, secrets or credentials in code, and \
any place user input reaches a filesystem path, shell command, \
or database query without sanitization. Ignore style issues \
entirely."

This catches things like a new admin endpoint that checks req.user.role === 'admin' but forgot to check that req.user exists in the first place, which would throw on an unauthenticated request rather than reject it cleanly — a bug that's easy to miss because "it still fails," just not in the way it should. It also catches the far more common issue of a new database query built with string concatenation instead of parameterized queries, something that slips through in a rushed review because the query "works" in every test that gets written for it.

One habit worth building: run the security-focused prompt on every PR that touches authentication, payments, file uploads, or anything accepting user input, even for changes that look trivial. The changes that look trivial are exactly the ones that get rubber-stamped by human reviewers, which is precisely why they're worth an extra automated pass.

Reviewing across the whole codebase, not just the diff

The single biggest advantage Claude Code has over a typical GitHub review interface is that it isn't limited to the diff view. A human reviewer looking at a pull request in a browser sees the changed lines, plus a few lines of context above and below. They don't automatically see every other place in the codebase that calls the function being changed. Claude Code can and should be asked to check that explicitly:

claude "The diff modifies the signature of calculateShippingCost() \
in src/pricing/shipping.js. Find every call site of this function \
across the entire codebase, and tell me whether any of them will \
break or behave differently with the new signature."

This is the kind of check that catches the quietly dangerous change: someone adds a new required parameter to a widely used utility function, updates the three call sites they know about, and misses a fourth call site in a background job that nobody remembered exists. A pure diff-based review will never catch that, because the background job file didn't change — the bug is in the fact that it *should* have changed and didn't. Claude Code catches this by actually searching the codebase for usages rather than trusting that the PR author found them all.

This same technique works for renamed database columns, changed API response shapes, and modified environment variable names — anything where the "bug" isn't in the diff itself but in what the diff forgot to update elsewhere.

Building review checklists that actually get followed

Every engineering team has a code review checklist somewhere — in a wiki page, a pinned Slack message, an onboarding doc — and virtually nobody follows it consistently, because remembering to manually check off eleven items for every PR is tedious and gets skipped under deadline pressure. Claude Code is a good fit for making these checklists actually enforced rather than aspirational.

Take whatever checklist your team already has and turn it into a project-level command:

Check this diff against our team's review checklist:
- Does every new API endpoint have input validation?
- Does every database query use parameterized queries?
- Are new environment variables documented in .env.example?
- Do new async functions handle rejection properly?
- Is there a test for at least one failure path, not just the
  happy path?
- Does this change require a migration, and if so, is it
  reversible?

Go through each item explicitly and mark it as PASS, FAIL, or
N/A with a one-line reason.

Running this on every PR turns a checklist that lived in institutional memory into something that's mechanically applied every single time, regardless of how tired the reviewer is or how many PRs are in the queue that day. It also makes onboarding new engineers easier, because the checklist enforcement isn't tribal knowledge anymore — it's a command anyone can run and understand.

What Claude Code review doesn't replace

It's worth being direct about the limits here, because overselling this leads to teams removing human review entirely and getting burned. Claude Code is excellent at mechanical correctness: tracing logic, finding unhandled cases, checking call sites, spotting missing validation. It is not a substitute for a human reviewer's judgment about whether a change is the *right* change — whether this is the correct architectural direction, whether this feature should exist at all, whether the approach fits the team's longer-term plans, or whether a "quick fix" is actually papering over a design problem that needs a bigger conversation.

The most effective teams use Claude Code review as a fast, thorough first pass that clears out the mechanical bugs — the missing await, the race condition, the unvalidated input, the broken call site — so that the human reviewer's limited attention goes toward the questions only a human can really answer: does this belong in the codebase, is this the right tradeoff, does this match how the team wants the system to evolve. Used this way, review gets faster and better at the same time, instead of trading one for the other.

If you want to get comfortable with this workflow from the ground up — setting up Claude Code in your own projects, writing effective project-level commands, and building automated review pipelines like the ones described here — our Claude Code Tutorial for Beginners course on teachyou.ai walks through all of it hands-on, starting from your first terminal session through wiring Claude Code into a real CI pipeline.