Skip to main content
友田 陽大
Vibe coding: shipping AI-generated code safely
バイブコーディング
AI駆動開発
セキュリティ
TypeScript

Reviewing AI-generated code — shipping code you didn't write, on your own responsibility

Reviewing AI-generated code by 'reading top to bottom' doesn't work. Drawing on solo × generative-AI practice, this guide walks a review procedure that opens the diff and looks first at the four boundaries to suspect — input, authorization, secrets, dependencies. It shows what to mechanize with grep, how to separate out the authorization calls only a human can make, and how to automate with npx @aegiskit/cli scan.

Published
Reading time
13 min read
Author
友田 陽大
Share

When you review AI-generated code, don't read it top to bottom. What you look at first is the "boundaries" — input, authorization, secrets, dependencies — and logic correctness comes after. Because the causes of what AI-written code leaks in production concentrate, almost entirely, in these four boundaries.

Working solo × generative AI (Claude Code), I've built a B2B SaaS that won the Minister of Economy, Trade and Industry Award, a payment platform with zero double-charges in production, and an AI platform for a broadcaster. I can't give up the speed of generation. But — the moment I press the merge button, I become the author of that code. What stays in git blame isn't the AI's name; it's mine. So "the AI wrote it" is no excuse for a production incident. Shipping code you didn't write, on your own responsibility — the craft for that is review.

This article systematizes, from practice, a review procedure that opens the diff of AI-generated code and looks first at the "places to suspect first." The principles of "why AI mass-produces vulnerabilities" and the full landscape of assessment tools I leave to Vulnerability assessment of AI-generated code (the four-layer assessment), and the catalog of how to write safe code to Secure coding in practice (NIST SSDF / OWASP ASVS); here I concentrate on "the order you read in" and "the places to suspect."


1. Reviewing "code you didn't write" is a different skill from writing it

When you write code, you build up a design model in your head as you go. Premises like "this value is already validated here" and "this branch is handled here" get updated with every keystroke. That's why you notice bugs.

But when you have AI write the code, that design model isn't inside you — only the fluent finished product comes falling in. Here's the trap. AI's output has tidy variable names, careful comments, a clean structure. It's readable. And the human brain mistakes readability for "correctness" or "safety." Research backs this up: in Veracode's 2025 study, 45% of AI-generated code contained a known security flaw, and this security record stayed flat even as the models got smarter. Fluency and safety don't correlate.

So the reviewer's question isn't "does this look right?" What you should always ask is this —

When malformed input, concurrent access, or a legitimate request pointing at someone else's ID arrives, how does this code break?

You won't get an answer to this question by reading the code top to bottom. While you're following the beautiful happy-path logic, your concentration runs dry, and the review ends before you reach the boundaries that matter. You need to start the reading order from the boundaries.


2. Suspect before you read — the four boundaries

The holes AI builds into production show up in surprisingly the same places. When I open a diff, before the logic I look for the following four boundaries in this order. The order runs from the largest "blast radius when it breaks" down.

BoundaryThe mistake AI tends to makeWhat to look for first in the diff
① Input boundaryInserts external input straight into the DB / shell / HTML without validating itString-concatenated queries, dangerouslySetInnerHTML, handlers with no Zod
② Authorization boundaryAuthenticates but doesn't authorize (someone else's ID passes too)findUnique that doesn't scope to the owner, RLS with USING (true)
③ Secret boundaryHardcoded keys, leaking secrets to the clientDirect references to sk_live / AKIA / process.env, misuse of NEXT_PUBLIC_
④ Dependency boundaryAdds nonexistent / unnecessary packagesUnfamiliar dependencies newly added in the package.json diff

"Look at the four boundaries first" — that's the whole of this article. Below, for each boundary, I show what to grep for in the diff and what to judge good / bad.


3. Input boundary — ask "where did this value come from?" first

AI writes code on the optimistic assumption that "input arrives in the correct shape." So it doesn't validate at the boundary and flows external input straight into dangerous sinks (DB / shell / HTML / URL).

When you open the diff, first trace where external input enters and where it lands without passing validation.

# 入力境界: 検証を通らず危険なシンクへ着く経路を洗う
rg -n "dangerouslySetInnerHTML|child_process|new Function\(" --type ts
rg -n "`SELECT .*\$\{|`.*\+ *req\.|query\(.*\+" --type ts   # 文字列連結のSQL

In a route handler's diff, look first at whether there's schema validation at the entrance. If there isn't, then no matter how clean the logic below is, it means "unvalidated values are flowing through the whole thing."

 export async function POST(req: Request) {
-  const body = await req.json();               // 何でも通る
-  const user = await db.user.create({ data: body });
+  const body = ParsedInput.parse(await req.json()); // 境界で型と形を固定
+  const user = await db.user.create({ data: body });
   return Response.json(user);
 }

The point is to concentrate validation at the "boundary." Instead of scattering individual if checks deep inside the handler, run it through a Zod schema at the entrance, so that from there on only type-guaranteed values flow. How to kill each injection class (SQLi / XSS / SSRF / path traversal) I leave to the secure-coding-in-practice article. What review should look at is one thing — "is this value validated at the boundary?"


4. Authorization boundary — target "authenticates but doesn't authorize"

This is the most important and most frequent one. AI writes authentication (is the user logged in?). But it happily skips authorization (is this person the owner of this data?). The result: as long as you're logged in, you can access someone else's resource — IDOR (broken object-level authorization / BOLA) is born. This has been the No. 1, most frequent risk on the OWASP API Security Top 10 ever since 2019.

To borrow the language of the OSS Aegis: security has "horizontal controls" that apply uniformly across the app, and "vertical risk" that depends on "who owns what." Input validation and headers are horizontal — a library can take them off your hands. But authorization is vertical — it depends on your business's data model, so no library can fix it. That's why this is the boundary the reviewer must watch most sharply.

What you look for in the diff is the "looks up by id, yet doesn't scope to the owner" pattern.

# 認可境界: id で引いて所有者スコープが無い箇所を洗う
rg -n "findUnique\(|findFirst\(|\.eq\('id'" --type ts -A2 \
  | rg -v "user_?id|owner|auth\.uid|session"
 export async function GET(req: Request, { params }: { params: { id: string } }) {
-  // ❌ 認証はするが認可しない:ログイン済みなら「他人の文書」も返る
-  const doc = await db.document.findUnique({ where: { id: params.id } });
+  const { userId } = await requireSession(req);        // 認証
+  const doc = await db.document.findFirst({
+    where: { id: params.id, ownerId: userId },          // 認可=所有者で絞る(縦の統制)
+  });
   return Response.json(doc);
 }

On Supabase, always read the RLS policy diff in the SQL

Looking at TypeScript alone won't reveal the authorization boundary — because the authorization is written in SQL. Open the diff of supabase/migrations/**.sql and read the policy's USING clause.

-- ❌ 認証はするが認可しない:ログイン済みなら全員の行が見える
create policy "read" on documents for select
  using ( auth.role() = 'authenticated' );

-- ✅ 行を所有者に絞る(縦の統制)
create policy "read own" on documents for select
  using ( auth.uid() = owner_id );

Here are three review focal points, following Aegis's inspection lens.

  1. USING (true) is correct only when "this table truly may be public." For a public profile or a public article, true is the right answer. But if true is on invoices or chats, that's a red flag. It's the textbook case of "authenticates but doesn't authorize," so check all the patterns in the article that explains the difference between authentication and authorization (owner scope) with real code.
  2. service_role paths need an ownership check. As the official Supabase documentation states, the service_role key runs with PostgreSQL's BYPASSRLS and ignores RLS entirely. The defensive boundary moves to "where the key is kept" and "an explicit server-side ownership check," so for any route that uses service_role, always check whether ownerId validation exists in the code.
  3. Does the SECURITY DEFINER function pin its search_path? If it doesn't, it becomes a breeding ground for privilege escalation.

This isn't hypothetical. An RLS misconfiguration in an app built with an AI builder was registered as CVE-2025-48757 (CVSS 9.3 CRITICAL) — a case where anyone could read and write others' data without authentication. And for a sense of scale: a primary survey of 1,000 public Supabase apps found that about 9.2% of the apps that had RLS had a policy that "authenticates but doesn't scope rows to the owner" (a lower bound, since it's a public-repository sample; un-scoped by owner doesn't mean instantly vulnerable — it needs confirmation). The method and the full counts are in the Supabase RLS field study. You should review on the assumption that nearly one in ten has a hole in this boundary.


5. Secret boundary — key and env hygiene

AI, prioritizing getting things running, will happily hardcode keys. And in a framework like Next.js that has a "server/client boundary," it makes the mistake of leaking secrets into the client bundle.

# 秘密境界: 混入した鍵・トークンと、env の直参照を洗う
rg -n "sk_live|AKIA[0-9A-Z]{16}|-----BEGIN|ghp_[0-9A-Za-z]{36}" --type ts
rg -n "process\.env\.[A-Z_]+" --type ts        # 型なしの env 直参照

There are two things review looks at.

  • May a key exist in the code? The answer is always No. Move it to env or a secrets manager. If a hardcoded key is in the diff, block it unconditionally, and you also need to revoke it out of the git history (a key left in a commit lives on in history even after you delete it).
  • Is NEXT_PUBLIC_ misapplied anywhere? A value with NEXT_PUBLIC_ gets baked into the client bundle. If a secret key ends up here, it's published to the world. Route env through a single typed boundary module (ban direct process.env references), and you prevent this accident structurally.

6. Dependency boundary — always read the package.json diff

AI hallucinates and suggests nonexistent package names. When an attacker pre-registers that hallucinated name, the AI's hallucination becomes a supply-chain attack outright — this is "slopsquatting" (I covered the mechanism and defenses in detail in the four-layer assessment article, so I won't repeat them here).

The review procedure is simple: read the package.json diff with the same seriousness as the feature diff.

git diff package.json package-lock.json
npm view <新しく増えたパッケージ名>   # 実在するか・極端に新しくないか・DL数

If even one unfamiliar dependency has been added, a human confirms once that "it truly exists and is legitimate." Make the lockfile the single source of trust, and enforce an exact match with npm ci in CI. See an increase in dependencies as an increase in attack surface, not features — that's the reviewer's stance.


7. Offload review to the machine — grep and npx @aegiskit/cli scan

Typing all these greps by hand every time won't last. Suspicions the machine can handle should be offloaded to the machine.

The input boundary, the secret boundary, and horizontal controls (headers/CSP, rate limiting, CSRF, typed env) can be automated with static analysis. In reviewing AI-generated code, I slot in the free OSS Aegis with a single command.

npx @aegiskit/cli scan   # インストール・設定不要。いまのdiffを静的解析する

Honestly, let me spell out what it does and doesn't do. scan

  • automatically surfaces missing horizontal controls (unvalidated input, secrets that slipped in, taint analysis of injection paths).
  • only "detects and warns" for vertical risk (authorization/IDOR, Supabase RLS design). Whether a given table may be un-scoped by owner — i.e., an intentional public exposure — is a matter of the meaning of your business rules, and a tool can't judge it. That's why Aegis never claims to "fully protect" you — a tool that advertises that is, if anything, dangerous.

In other words, scan mechanizes my grep checklist and presents every place to suspect without gaps — it doesn't replace review. And if you pin it in CI, the machine keeps stopping the same hole from recurring. Turning CI into a quality gate (how to enforce the four layers — types, tests, static analysis, security) is covered in Quality-gate design for AI-driven development.


8. Judgments only a human can make — the "meaning" of authorization and business logic

After the machine raises its suspicions, what remains at the end is judgment about meaning. A tool reads the "shape" of a policy or code, but it can't read the "meaning" of your business rules.

  • Is this USING (true) an intentional public exposure, or an authorization gap? Only a human who knows the table's purpose can answer.
  • Is this state transition safe if it runs twice? The reason I achieved zero double-charges in production on the payment platform wasn't a scanner — it was designing and reviewing the state transitions and idempotency to the end with human eyes. "What happens if the same payment request arrives twice?" is a matter of business meaning, not the shape of the code, and no static analysis will make that judgment for you.
  • Can this quantity, price, or limit be abused? Holes in business logic are, likewise, the domain of the human who holds the spec.

So the premise for review to work is that the definition of "correct" — the acceptance criteria — is put into words. The workflow of nailing the spec first and turning acceptance criteria into tests I wrote in The production workflow for spec-driven development. If the spec is vague, the reviewer can't judge beyond "looks right." Implementation to the AI, spec decisions and verification to the human — this division of labor is what makes review possible.


9. The order to look when you open a diff (checklist)

Finally, here's the order I actually go through from opening a diff to giving the green light, laid out as-is. From the top down, starting with the boundary of largest blast radius.

① 入力境界   外部入力は境界で検証されているか(Zod等)。危険なシンクへの直挿入は無いか
② 認可境界   idで引いて所有者で絞っているか。RLSのUSING(true)は意図的公開か。
             service_role経路に所有権チェックはあるか
③ 秘密境界   鍵のハードコードは無いか。NEXT_PUBLIC_で秘密を漏らしていないか
④ 依存境界   package.jsonに見覚えのない依存が増えていないか。実在するか
⑤ 機械化     npx @aegiskit/cli scan を通し、疑いを機械にも洗わせる(CIに固定)
⑥ 意味の判断 認可の意図・状態遷移の冪等性・業務ロジックの悪用可否を人間が確認
⑦ ロジック   ①〜⑥が緑になって初めて、ハッピーパスの正しさを読む

Reverse the order — read the logic first and put the boundaries off — and a review of AI-generated code will almost certainly miss the boundaries. Clean logic can wait until last. First, suspect where it breaks.


Generative AI multiplies my shipping speed several times over. But speed only becomes safe once it's offset by verification. And the last layer of verification is always human review. Ship code you didn't write, on your own responsibility. For that, when you open a diff — suspect the boundaries, not the logic, first. Because it's my name that stays in git blame.

Start with a single npx @aegiskit/cli scan on the project in front of you. Let the machine sweep the boundaries it can sweep, and concentrate the human on judgments about meaning. That's the starting point for review in the AI era.

Frequently asked questions

How is reviewing AI-generated code different from ordinary code review?
The biggest difference is that you don't have a design model in your head. When a human writes code, intent accumulates as they write; but AI output arrives as a fluent finished product, so readability comes to look like correctness or safety. That's why, in practice, you shouldn't be pulled in by elegant logic — suspect the four boundaries (input, authorization, secrets, dependencies) first.
Why doesn't 'reading top to bottom' review work?
Because nearly all of what AI leaks in production concentrates at the boundaries. AI is good at happy-path logic and writes it cleanly, but it drops the behavior for malformed input, concurrent access, and a legitimate request that points at someone else's ID. Reading top to bottom spends your time on clean logic, and your concentration runs out before you reach the boundaries that matter. Look at the boundaries first.
If I hand review off to a tool, does human review become unnecessary?
No. Static analysis like npx @aegiskit/cli scan automates horizontal controls such as input validation and headers, but for vertical risk — authorization/IDOR, RLS design — it only detects and warns. Whether a given table may be un-scoped by owner (i.e., an intentional public exposure) is a matter of business-rule meaning, and only a human can judge it. The tool mechanizes the suspicion; the human holds the meaning.
Where in the diff do I find authorization problems?
First sweep for spots that look up by id but don't scope to the owner. If there's no user_id, owner, or auth.uid() around findUnique/findFirst or .eq('id'), it's suspect. On Supabase, RLS with USING(true) or a policy that stops at auth.role()='authenticated', plus missing ownership checks on service_role paths, are the standard holes.

References

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

That AI-generated code — safe to deploy as-is?

After “it works” comes “it doesn't leak” — scan your project free with one command

Aegis, an MIT-licensed OSS, automates the controls AI tends to skip (headers/CSP, rate limiting, input validation, secrets) and detects-and-warns on vertical risks in authorization and RLS design. The fix plan from `aegis fix --format json` pastes straight into Claude Code or Cursor.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading