跪拜 Guibai
← Back to the summary

AI code is cheap, verification is expensive: a 6-layer checklist for reviewing AI-generated code

How to Review AI-Generated Code: An Actionable 6-Layer Checklist

Full summary: AI has made "writing code" cheaper, but it has made "proving code correct" more expensive — a patch may take AI 5 minutes to write, but a human can spend 45 minutes verifying it. This article provides an actionable 6-layer checklist, from "let the tools judge first" to "review intent, not syntax" "review tests, not implementation" "review boundaries and security", plus a runnable review script that turns "what to review" into executable rules.


Contents

  1. The Verdict: code is cheap, verification is expensive
  2. Why AI code requires a different review approach
  3. Layer 1: Let the tools judge first
  4. Layer 2: Review intent, not syntax
  5. Layer 3: Review tests and boundaries
  6. Layer 4: Review security and hallucinated dependencies
  7. Layers 5 & 6: Scope, rollback, and when to rewrite
  8. A runnable AI code reviewer
  9. Symptom → cause → solution table

1. The Verdict

Conclusion first: When reviewing AI-generated code, the focus is not on "is the code correct?" but on "why does it look correct?"

An AI-written patch can be done in 5 minutes, but verifying it often takes 45 minutes. The brutal 2026 math is: the marginal cost of writing code has been pushed near zero, and the cost of proving correctness has become a new tax. So reviewing AI code is essentially moving your attention from reading code to verifying code.

One sentence summary of the six-layer checklist: let the machine run the layers it can, and have a human focus only on the "looks correct but wrong" mistakes the machine misses.

Layer What to review Who does it
1 Compile, lint, types Machine
2 Intent and constraints Human
3 Tests and boundaries Human + machine
4 Security and hallucinated dependencies Human
5 Scope and rollback Process
6 Whether to rewrite it yourself Human

Analogy 🎭: AI is like an assistant that speaks extremely fast and occasionally makes things up. It can give you 10 pages of notes in 5 minutes, which looks efficient, but the real cost is the 45 minutes you spend checking them line by line. Reviewing AI code means learning to not read those 10 pages first, but instead asking "how did these come about?".

References:


2. Why AI code needs a different review approach

First, break a misconception: many people think AI code errors are "syntax errors" or "compile failures". Quite the opposite — CI catches those, and they don't need a human at all.

The real danger is hallucinated correctness: the code compiles, passes all tests, looks perfectly normal, but is wrong.

GitHub's official guidance lists this as the most serious red flag. Obvious hallucinations (calling a non-existent API, referencing an out-of-scope variable) get caught by CI. The dangerous ones are subtler: an off-by-one in pagination, a missing permission check on a branch never exercised by tests, a validation short-circuited by some edge condition — all hiding under the disguise of passing tests.

Why do AI errors look different from human errors? Three structural reasons:

① Pattern matching, not reasoning. AI generates code by predicting the next pattern from training data; it does not know it is predicting incorrectly. It produces code with confident certainty, even if the API it used is an invention.

② Same-source assumption. Let the same AI write both code and tests, and both share the same assumption about "what is correct". The tests are not derived from requirements; they are derived from the code. That is what the article calls tautological tests. They pass because they validate the implementation against itself, not against the requirement.

③ No external baseline. The industry default for AI code quality problems is to add another AI reviewer. But that is structurally circular: the generator and the reviewer start from the same artifact and share the same training distribution, so their failures are correlated. The review is validating code against code, not intent against code.

Analogy 🎭: This is like having the same person write both the exam questions and the answers. The answer is "right" only because it is derived from the questions, but the questions themselves may be wrong.

The consequence is quantifiable. CodeRabbit analyzed 470 real GitHub PRs and found AI-generated code produces about 1.7 times more issues than human code — with 75% more logic and correctness errors. These are measurements from production repositories, not toy benchmarks.

So the review approach has to change: let the machine run the gates it can, and let a human focus on the "looks correct" errors.

References:


3. Layer 1: Let the tools judge first

A human is not a good compiler. Don't spend ten minutes scanning for syntax by eye; a deterministic tool does it in seconds.

Layer 1 simply makes the machine run what it should run: compile, lint, type check, existing test suite. The purpose is not to "prove code correct", but to first cut out all obvious forms of "wrong", leaving the human's attention for the later layers.

For Python, Layer 1 can be:

python -m compileall .   # syntax and compilation
ruff check .             # static lint
mypy .                   # type checking
pytest -q                # existing test suite

For frontend / Node projects, Layer 1 is the usual CI lines:

- run: npm ci
- run: npx tsc --noEmit
- run: npx eslint .
- run: npm test -- --coverage

Key rule: generated code that fails Layer 1 should not be merged. This is not about "AI is bad"; it is that human-written and AI-written code both have to pass the same CI gate. Layer 1 is not about the reviewer; it is an automated gate that separates machine-judgeable errors from human-only errors.

⚠️ Blind spot: you will see errors when they are reported, but the absence of errors does not mean the code is right. What compilers and linters catch are obvious errors; what they miss are the "looks right but wrong" errors. That is the battlefield for the next layers.

References:


4. Layer 2: Review intent, not syntax

Once through Layer 1, the code compiles and passes tests. But running is not the same as solving the right problem.

The question in this layer is not "is this code well-written?" but "is this code solving the right problem?" It checks two things: does it match the project purpose, and does it follow architectural conventions.

GitHub's official three self-review questions, used directly:

Key prerequisite: you must give the AI enough context first. Before generation, feed the AI a starting point with README, docs, recent PRs, and tell it which sources are trustworthy, which are unusable, and provide good examples. This determines whether Layer 2 is "reviewing with full knowledge" or "looking at a mess" and guessing.

The reviewer also needs context. If you only take the PR itself to review, it is as thin as what the AI saw when generating. At minimum you need: repository structure and dependencies, architecture constraints and service boundaries, API contracts and backward-compatibility expectations, coding standards, historical incidents and postmortems, and the trade-offs accepted in previous review comments. That context decides whether you can judge "should this code look like this?"

Analogy 🎭: Reviewing syntax is like editing spelling mistakes; reviewing intent is like checking whether the essay went off-topic. Spelling belongs to the dictionary (compiler), while off-topic can only be judged by someone who knows the assignment.

The counterintuitive but important point: Layer 2 often requires you to first suspect that the tests might also be wrong. If the AI misinterpreted requirement A as B, its tests will verify B too — all green, but not what you need. So the core of the first two layers is: first decide "what is correct", then decide "is the code correct".

References:


5. Layer 3: Review tests, then boundaries

Read the tests first, then the implementation. This is the most critical rule when reviewing AI code.

Two reasons:

① Tests help you judge whether AI understood the requirement. If AI interpreted the requirement wrong, its tests will verify the wrong requirement. So the first thing is to look: are these tests actually checking the acceptance criteria, or are they just an echo derived from the implementation? The latter is a tautological test — it passes because it validates what the implementation does, not what it should do.

② Boundaries are where AI most often fails. AI defaults to the happy path. So check whether the test suite covers these edges:

Analogy 🎭: This is like reading an insurance policy. The important part is not whether the normal claim process works (that's always there), but whether earthquakes, floods, and suicide are covered. AI can make the happy path look fine, but boundaries are where it actually overturns.

Canary signal: if the AI-written tests all exist to prove the implementation, have no edge cases, and assertions are too loose (e.g., only assert "does not throw" rather than "returns a specific value"), then the tests are essentially worthless. They just stamp "looks tested" on "looks correct" code.

The data backs this up. Research on agent-generated tests for open-source projects found AI exceeds humans in the diversity of edge cases (variety 0.62 vs 0.32), but falls behind in stability — human tests have a flaky rate of 0.30, AI-generated tests 0.41. In other words, AI can think of more edges, but its tests are flakier, more brittle, and more likely to report false positives. That shows: being able to think of a boundary and reliably validating that boundary are two different things.

References:


6. Layer 4: Review security and hallucinated dependencies

This layer is not optional, because AI-generated code does not do adversarial thinking. It does not think like an attacker looking for ways to bypass a check. But attackers do.

The classes to scan are essentially the OWASP instinct list:

① Injection. AI especially likes to concatenate strings into SQL / queries, and stuff user input straight into commands:

② Missing authorization. Remember authentication ≠ authorization. AI easily writes "is the user logged in" but forgets "is this user allowed to do this action". Especially on branches never hit by tests.

③ Hardcoded secrets. AI often forgets to put keys, tokens, passwords into environment variables, writing them right into the source:

OPENAI_API_KEY = "sk-abc123def456"   # dangerous: hardcoded secret
DATABASE_URL = "postgres://user:password@localhost/db"  # dangerous: plaintext password

④ Over-permissive mass assignment. Binding the entire request body to an object, so "whatever the user sends gets set", letting unauthorized fields be overwritten.

⑤ Hallucinated dependencies — the most sneaky. AI may reference a library that does not exist, or is outdated. Worse: a non-existent package name can be registered by an attacker — you npm install a name AI invented, and you have installed someone else's package. So review any new dependency individually: does it exist in the lockfile, is it license-compliant, and is the source trustworthy.

⑥ Injection targeting the code-generation agent itself. Don't forget that much code is now written by an agent. The agent's context comes from the repository, the network, and connected tools. Attackers can place hidden instructions in issue bodies, PR descriptions, AGENTS.md, MEMORY.md — that is indirect prompt injection, an attack on the development workflow itself.

Analogy 🎭: AI is like a person who hides the key under the doormat because it's convenient; attackers are thieves who check under the mat. AI does not consider "who will come looking", so it cannot find this kind of trap on its own. Only a reviewer with an attacker's mindset can fill that gap.

References:


7. Layers 5 & 6: Scope, rollback, and when to rewrite

The first four layers check "is this code itself correct"; layers 5 and 6 check "should this code exist in this form".

Layer 5: Review scope and rollback

One of AI's biggest flaws is scope runaway: a 5-minute patch, and it also changed 10 files, deleted 3 unrelated functions, and added 2 new dependencies. So this layer looks at:

There is also a reading order for the diff: high-risk first, not top to bottom. Scan deletions (^-) first, then read additions, then look at the "unrelated areas" that AI casually changed.

Analogy 🎭: Like receiving a package. The first thing you check is whether anything is missing, not how nice the free gift is. AI PRs most often have missing things — a missing boundary case, a missing permission check line, a function that used to exist but is now gone.

Layer 6: When to rewrite instead of fixing

The biggest trap here is sunk cost. AI gives you a big chunk of code that doesn't run, and your instinct is "I'll just fix it". But in many cases, delete and regenerate is cheaper than patching.

Kunal Ganglani's checklist gives clear "delete and rewrite" criteria. Overwrite if any of these is hit:

Why delete? Because the cognitive cost of patching a large AI-generated diff with vague intent often exceeds the cost of re-generating with a tighter prompt. You are not fixing code; you are being a production-grade fallback for a vague convention, which is the worst deal.

⚠️ A vital principle: don't let the same tool both write code and write the tests that "approve" it. If the AI-generated code and the tests validating it originate from the same assumptions, that validation is circular. Tests should be written from an independent perspective.

References:


8. A runnable AI code reviewer

The previous sections discussed "how to review"; this section turns it into a script that can run directly. It uses regex + rules to automatically scan the most common Layer 4 (security and hallucinated dependency) traps: hardcoded secrets, suspicious APIs, over-permissive bindings, dangerous eval/exec, string-concatenated SQL.

So when you get a piece of AI code, run this script first, filter out what the machine can judge, and then use human effort for layers 2, 3, 5, and 6. It only relies on the standard library 🐍.

"""AI code pre-screener: static rule scan on Python source for common AI generation pitfalls.

Does not replace human review; it is only the machine pre-filter for "layer 1 + layer 4":
1. Hardcoded secrets / tokens / passwords
2. Suspicious or misused APIs (eval, exec, pickle, dangerous classes)
3. Over-broad input binding (mass assignment)
4. SQL string concatenation (injection risk)
5. Dangerous subprocess calls
"""

import re
import sys
from dataclasses import dataclass, field


# each rule: regex + severity + note
@dataclass
class Rule:
    """A static check rule. Attributes: pattern, severity, message"""
    pattern: re.Pattern
    severity: str
    message: str


@dataclass
class Finding:
    """One matched result. Attributes: line_no, severity, message, matched"""
    line_no: int
    severity: str
    message: str
    matched: str


# rule table of the most common AI security traps; pattern matches the "dangerous pattern" signature
RULES = [
    # 1) hardcoded secrets / tokens / passwords, e.g. OPENAI_API_KEY = "sk-..."
    Rule(re.compile(r'(api[_-]?key|secret|password|passwd|token|pwd)\s*=\s*["\'][^"\']{6,}["\']', re.I),
         'critical', 'possible hardcoded secret/credential; move it to an environment variable'),
    # 2) dangerous dynamic execution, AI often passes user input straight to eval/exec
    Rule(re.compile(r'\b(eval|exec)\s*\('),
         'critical', 'dangerous dynamic execution: user input entering eval/exec can lead to arbitrary code execution'),
    # 3) insecure deserialization, pickle can be crafted into arbitrary code execution
    Rule(re.compile(r'\bpickle\.loads?\b'),
         'critical', 'pickle deserialization is unsafe; can be crafted into RCE'),
    # 4) SQL string concatenation, the classic injection entry
    Rule(re.compile(r'\b(FROM|WHERE|INSERT|UPDATE|DELETE)\s+.*%|\+\s*[a-z_]+\s*\+\s*["\']'),
         'critical', 'possible SQL string concatenation; use parameterized queries instead'),
    # 5) unvalidated input into subprocess command, command injection risk
    Rule(re.compile(r'\b(os\.system|os\.popen|subprocess\.(run|call|Popen))\s*\('),
         'warning', 'subprocess call: make sure the command does not concatenate unvalidated input'),
    # 6) over-broad input binding, stuffing the whole request body into an object
    Rule(re.compile(r'\b(update|setattr)\s*\(.*\*\*,?\s*\w+'),
         'warning', 'potential mass assignment: binding a whole input block directly to an object'),
]


def scan(source: str, filename: str = "<code>") -> list[Finding]:
    """Run the rules line by line on source code."""
    findings = []
    for line_no, line in enumerate(source.splitlines(), start=1):
        for rule in RULES:
            m = rule.pattern.search(line)
            if m:
                findings.append(Finding(line_no, rule.severity,
                                        rule.message, m.group(0).strip()))
    return findings


def report(findings: list[Finding]) -> None:
    """Print findings sorted by severity."""
    if not findings:
        print("✅ no obvious AI generation traps found (still needs human review of intent and boundaries)")
        return
    order = {"critical": 0, "warning": 1}
    for f in sorted(findings, key=lambda x: order[x.severity]):
        tag = "🔴" if f.severity == "critical" else "🟠"
        print(f"{tag} L{f.line_no} [{f.severity.upper()}] {f.message}")
        print(f"    matched: {f.matched}")


# sample with deliberate AI-style traps
SAMPLE = '''
OPENAI_API_KEY = "sk-abc123def456"            # dangerous: hardcoded secret
sql = "SELECT * FROM users WHERE id = " + uid  # dangerous: SQL string concatenation
data = pickle.loads(payload)                   # dangerous: pickle deserialization
result = eval(user_expr)                       # dangerous: eval of user expression
os.system("ping " + host)                      # warning: subprocess concatenation
User(**request.json())                         # warning: mass assignment
'''


if __name__ == "__main__":
    report(scan(SAMPLE, "sample.py"))

Actual output:

🔴 L2 [CRITICAL] possible hardcoded secret/credential; move it to an environment variable
    matched: OPENAI_API_KEY = "sk-abc123def456"
🔴 L3 [CRITICAL] possible SQL string concatenation; use parameterized queries instead
    matched: WHERE id = " + uid
🔴 L4 [CRITICAL] pickle deserialization is unsafe; can be crafted into RCE
    matched: pickle.loads(payload)
🔴 L5 [CRITICAL] dangerous dynamic execution: user input entering eval/exec can lead to arbitrary code execution
    matched: eval(user_expr)
🟠 L6 [WARNING] subprocess call: make sure the command does not concatenate unvalidated input
    matched: os.system("ping " + host)
🟠 L7 [WARNING] potential mass assignment: binding a whole input block directly to an object
    matched: User(**request.json())

The value of this script is not rule completeness; it's that it turns something originally done by eye in Layer 4 into a reusable deterministic check. You can place it in CI and run it before review — that is the tangible implementation of Layer 1.

Analogy 🎭: This script is like a security screening gate. It stops the knives and guns first. It cannot catch a criminal disguised as a passenger (that is the intent and boundary layers), but it means you no longer spend time checking for knives. Use it as the first filter, and let humans handle the remaining high-value judgments.

References:


9. Symptom → cause → solution table

Compress the six layers into a table you can directly diagnose with. It is recommended to read the row that matches your recent experience, rather than reading top to bottom 📋. Each row is a real trap that can surface in production.

Symptom Cause Solution
CI is green, code runs, but within a week it causes rework after merging AI error is "hallucinated correctness": it compiles, passes tests, but the logic is wrong (CodeRabbit measured logic/correctness errors at 75% more than human code) Don't just ask "can it run?", ask "is it running the right thing?" Use Layer 2 to review intent against the requirement
Test coverage looks complete, but it breaks on edge cases right after launch AI wrote tautological tests that validate the implementation, not the requirement; boundaries are covered but assertions are loose Read tests first, then implementation; specifically hunt for assertions that only check "doesn't throw" without checking the returned value
A 5-minute patch changed 10 files and deleted 3 functions AI scope runaway: when generating, it did not treat "should I touch this?" as a cost Layer 5 atomicity: one PR does one thing; scan git diff for deletions (^-) first, then read additions
A new dependency only fails at deploy time with "package does not exist" AI referenced a non-existent library (hallucinated dependency); the non-existent package name could be pre-registered by an attacker Review every new dependency individually: confirm it is in the lockfile, license-compliant, and from a trustworthy source, then install
Authentication exists but an unauthorized request reaches a branch AI confused authentication (is the user logged in) with authorization (does this user have permission); especially on branches tests never cover Layer 4: check authorization on each branch; after each if user, ask "does this user have the right?"
Code quality swings wildly, like opening a blind box 90% of the variation comes from the prompt, not the model; missing "do not" constraints Give AI both "do" and "don't": no global variables, no swallowing exceptions, no sys.exit; generate iteratively in small steps
An AI change exceeds 300 lines and cannot be explained Sunk-cost trap: you think "I'll just fix it", but you are instead covering for a vague convention Use Layer 6 criteria to delete: diff >300 lines, >2 new dependencies, 10-minute read with unclear explanation → regenerate with a tighter prompt
Tests are all green but the code is unusable for you AI wrote the code and also wrote "approving" tests, making validation circular Don't let the same tool write both code and the tests that approve it; have independent perspective add boundary cases to tests

⚠️ Remember: do not treat "compiles and passes tests" as evidence that code is correct. Layer 1 is a machine gate that only removes the obviously wrong; the value is in the other five layers, because they decide whether this code should exist in this form.

References:


When to use it? When you have AI code that "looks right", run it through this table, find the rows most like your case, and inspect layer by layer. When not to use it? When you are working on your own long-maintained repo and making small changes to existing logic, don't mechanically apply all six layers; first rebuild context — without enough context, no checklist will reveal the real problems.

This column "AI Development Paradigm" focuses on making AI a true amplifier of development efficiency, not a source of rework. From "how to make AI write more steadily" to "how to write so AI does not mislead the architecture", each article includes a runnable code snippet and updates two or three times a month. To read in order, subscribe to the column.