How to Inspect AI-Generated Diffs Before Accepting

A 200-line Claude Code or Cursor diff isn't safe just because it compiles. Read it deletions-first, in the right order.

Claude Code or Cursor hands you a 200-line diff, it compiles, tests are green, and you hit Cmd+Enter to accept it all. Two hours later production has a weird bug: the AI deleted a “seemingly unused” boundary check; or replaced if (!user) with if (user === null) while callers pass undefined; or renamed an identifier and silently touched an unrelated context with the same name. These are all “diff compiles, but behavior is not unchanged” failures.

TL;DR — the fastest reliable review: read the diff in risk-descending order, never top-to-bottom. (1) Scan deletions first (git diff | grep '^-'), git grep each deleted symbol across the whole repo to prove it’s actually dead. (2) Cross-check every rename for scope bleed. (3) Eyeball logic-but-not-syntax changes (flipped conditionals, swapped Promise combinators). (4) Read additions last. Then run the change-scoped tests, the full suite, typecheck, and lint. The rest of this page is each step in detail, plus the exact tool settings in Cursor 2.5 and Claude Code that make this faster.

Before any of this: stop accepting blind. In Claude Code, press Shift+Tab to cycle out of acceptEdits (auto-accept) back to default mode, or into Plan Mode so Claude proposes a plan you approve before a single file is written (as of June 2026, Shift+Tab cycles default → acceptEdits → plan; the current mode shows in the status line — see the official permission modes docs). In Cursor (Agent mode, formerly Composer, in 2.5), the per-change diff appears inline; Cmd/Ctrl+Enter accepts and Cmd/Ctrl+Backspace rejects. Review each hunk before either.

Common causes

Ordered by risk, highest first.

1. Deleted “seemingly dead” code that wasn’t dead

AI confidently deletes // legacy, // TODO: remove, and functions with no direct callers. But dynamic dispatch, CLI entry points, dynamic require, reflection, and exports referenced only in comments are invisible to static analysis.

How to spot it: For any deletion block in the diff, git grep <deleted function or string> across the whole repo (including docs/, scripts/, tests/, CI configs). Any hit means keep it.

2. Rename touched the wrong context

The agent renames user to currentUser in one component and accidentally rewrites a totally separate user.role somewhere else — another component, a type definition, a prop name.

How to spot it: Count the files where - user / + currentUser appears in the diff. If it’s outside the scope you named in the prompt, there’s bleed-over risk.

3. Logic changed without syntax changing

if (!user)if (user === null) flips behavior when user is undefined. array.find(x => x.id === id)array.filter(x => x.id === id)[0] differs when ids repeat. Promise.allPromise.allSettled turns “any failure throws” into “silent success.”

How to spot it: Read conditional, loop, and Promise combinator lines in the diff. These type-check fine but the semantics need line-by-line confirmation.

4. Added a new branch but didn’t update all callers

The agent added a parameter or return value but only updated some call sites. Strict TypeScript catches it; loose configs or plain JS won’t.

How to spot it: Function signature changed → grep -rn "functionName(" --include='*.ts' --include='*.tsx' and compare caller count to what the diff modified.

5. Import paths / export styles changed

export defaultexport const works in the two imports the agent updated, but misses lazy loads, dynamic imports, and Storybook config references.

How to spot it: grep -rn "from.*FileName" . covers all imports — diff them against what the AI updated.

6. Tests / mocks deleted to “make tests pass”

The most insidious: you said “make the test pass,” the agent deleted the test case or weakened the assertion.

How to spot it: Any test(, it(, expect(, or describe( line in the deletion column is a red flag.

Shortest path to fix

A fixed “risk-descending” reading order.

Step 1: Make the AI summarize its own diff

Before accepting, ask in chat:

Before I accept, summarize this change in the following format:

1. Files modified (full paths)
2. For each file:
   - Functions / behaviors deleted
   - Identifiers renamed
   - Behavior changes (not just syntax)
   - New dependencies / imports added
3. For every line you deleted, confirm whether it's referenced
   anywhere else in the repo
4. What is the single highest-risk change here, and why

If the summary mentions anything you didn’t expect, reject and ask the agent to refine or split the diff.

Step 2: Read deletions first, with git tooling

# Only show deleted lines
git diff --staged | grep '^-' | grep -v '^---'

# Which files have the most deletions
git diff --staged --stat

# Read each file, deletions side first
git diff --staged path/to/file.ts | less

In GitHub or Cursor’s diff viewer, switch to “Split” view and scan the red (deletion) column top to bottom. In Cursor 2.5 you can also let the editor pre-flag risky edits: after the agent finishes, open the Source Control tab, or click Review then Find Issues, and Cursor runs a dedicated AI pass that analyzes the proposed edits line by line and surfaces likely problems. Treat that as a first filter, not a substitute for reading deletions yourself — it will not know that a “dead” function is wired up through reflection.

Step 3: Cross-grep every rename

For each - oldName / + newName pair in the diff:

# Where does the old name still appear
git grep -n "oldName"

# Compare against files the diff actually touched
git diff --staged --name-only

Mismatch = bleed-over risk. For ambiguous names (user, data, config), force the agent to pick a more specific identifier to avoid collisions.

Step 4: Apply “can I explain this in 5 minutes” to additions

Read new code last. For each function, ask:

  • What are the input boundaries? How does it handle null / undefined / empty arrays?
  • Any async race? Possible race conditions?
  • How does it propagate errors? Are any swallowed?

For anything you can’t explain quickly, ask the agent to add 5 lines of comments; for anything the agent can’t explain, demand a rewrite.

Step 5: Run two rounds of regression

Don’t just run the tests directly related to the change:

# Change-related tests
npm test -- path/to/changed.test.ts

# Full suite
npm test

# Then typecheck and lint to surface side effects you missed
npm run typecheck
npm run lint

If possible, run two git worktree checkouts (HEAD and the change) through the same end-to-end scenario and diff the outputs.

Risk typeToolPass criterion
Deleted useful codegit grep across repo0 remaining references
Rename bleed-overgit diff --name-only vs grepModified files = grep hit files
Semantic changeUnit tests + boundary casesBoundary case tests exist and pass
Untouched callerstypecheck + lintNo errors under strict mode

How to confirm it’s safe to accept

You are done reviewing only when all four hold:

  1. Every deleted symbol returns zero hits from git grep -n <symbol> across the full repo (including docs/, scripts/, tests/, CI YAML).
  2. The set of files containing each rename equals the set you intended to touch — no extra files.
  3. npm run typecheck && npm run lint exits clean (under strict mode), and both the change-scoped tests and the full suite are green.
  4. You can explain, in one sentence each, every behavior change and every new branch the diff introduces. If you can’t, it is not reviewed yet.

Only then accept (Cmd/Ctrl+Enter in Cursor, or approve the hunks in Claude Code). Commit immediately so the next AI change starts from a clean checkpoint.

Prevention

  • Any AI diff over 200 lines gets a real review, never blind accept-all; if it’s over 500 lines, ask the agent to split into multiple commits.
  • Use Plan Mode for anything non-trivial. In Claude Code, Shift+Tab into plan mode so you approve a written plan before any file is touched; in Cursor, ask the agent to lay out its plan first and only then write code. The cheapest place to catch a bad change is the plan, not the diff.
  • In CLAUDE.md / .cursor/rules (the new home for project rules, replacing the legacy .cursorrules file) hard-code: “Before deleting any code, search the whole repo for references and report them.”
  • git commit a checkpoint before any AI change so the diff is reviewable and rollback is one command.
  • Strict TypeScript and strict ESLint are the safety net for AI coding; turning them off removes the one check that catches untouched callers and type mismatches.
  • Add a pre-commit hook that blocks test deletion: git diff --cached | grep -E '^-\s*(test|it|expect)\(' && exit 1
  • For pull requests, turn on an automated reviewer (Cursor’s Bugbot reviews PRs on push and can propose fixes). It catches a different class of issue than your local diff read, so run both.
  • Put critical modules (auth, payment, permission) in dedicated directories and use CODEOWNERS to require human review before merge.

FAQ

Why did a diff that compiles and passes tests still break production? Because “compiles” only proves the types line up and “tests pass” only proves the cases you wrote still pass. Neither covers a deleted boundary check reached via reflection, a flipped if (!user) vs if (user === null) on an undefined input, or a test the agent quietly weakened. Read deletions and logic-but-not-syntax changes by hand; those are exactly what green checks miss.

Is it safe to leave Claude Code in auto-accept (acceptEdits) mode? Only when you will review afterward via git diff and the change is low-stakes. acceptEdits skips the per-edit prompt, so the agent can refactor more than you asked before you see it. For auth, payments, migrations, or anything over ~200 lines, drop back to default mode or Plan Mode with Shift+Tab.

The agent says it removed “dead code.” How do I trust that? Don’t trust, verify. For each deleted name, run git grep -n <name> across the entire repo, including docs/, scripts/, tests/, and CI config. Static analysis and the agent both miss dynamic dispatch, require() by string, reflection, and references that live only in config or comments. Any hit means keep it.

How big a diff is too big to review at once? Past roughly 200 lines, reading top-to-bottom stops being reliable. Review in risk order instead (deletions, renames, logic, additions). Past ~500 lines, ask the agent to split the work into separate commits — a reviewable diff is one you can fully explain, not one you skim.

What if I already accepted a bad diff? If you committed a checkpoint first, git revert or git reset --hard to it. If you didn’t commit, you may still recover the pre-change file from the editor’s local history or git reflog. This is the whole reason to commit before each AI change.

Tags: #AI coding #Debug #Troubleshooting