The Importance of Human Oversight in AI-Generated Code: Verification Strategies for Developers

I shipped a feature last month where a large share of the code came out of an AI coding assistant. The PR was around 1,400 lines. It passed CI on the first run, deployed to staging, and worked. Two weeks later a background job started duplicating webhook events under load, and the root cause was a retry block the model had written that looked perfectly reasonable and was quietly wrong.
That is the entire problem with AI-generated code in one story. It compiles. It looks idiomatic. It passes the happy path. And it fails in a way that costs you a weekend.
I have delivered web projects for clients across several countries, and for the last couple of years most of that work has involved some level of AI assistance. This article is the AI-generated code verification process I actually run, not a theoretical framework. If you are a developer using Copilot, Cursor, Claude Code, or any agentic tool, or a founder wondering whether your team is shipping unreviewed AI output, this is for you.
Why AI-generated code needs a different review lens
Traditional code review assumes a human wrote the code and therefore has a mental model of intent. With AI-generated code, the author has no model. The model produced tokens that statistically fit your prompt. It has no memory of your architecture decisions from six months ago, no awareness that your payment provider rate-limits at a specific threshold, and no reason to care that your team banned any in TypeScript.
Three failure modes show up again and again in my work.
Confident hallucination. The model invents a method signature, an SDK option, or a config key that does not exist. It reads fluently because the surrounding code is correct.
Plausible-but-wrong logic. This is the dangerous one. Off-by-one errors, retries without idempotency keys, race conditions in async code, useEffect dependencies that look complete but are not. Nothing throws. Nothing logs. It just breaks later.
Silent scope creep. You ask for a date formatter and get a utility file with three exported helpers, a type guard, and a JSDoc block nobody asked for. Extra surface area means extra attack surface and extra maintenance.
Human oversight in AI code is not about distrust. It is about recognizing that the model optimizes for plausibility, and production systems need correctness.
The verification pyramid I use on every AI-assisted PR
Think of verification as layers, cheapest and fastest at the bottom, most expensive at the top. You run the bottom layers constantly and the top layers only when the code earns it.
| Layer | What it catches | Speed | Who runs it |
|---|---|---|---|
Type checking (tsc --noEmit) |
Hallucinated APIs, wrong shapes | Seconds | CI, every push |
| Linting + custom rules | Style drift, banned patterns | Seconds | CI, pre-commit |
| Static analysis (Semgrep, Sonar) | Injection, secrets, unsafe patterns | 1-3 min | CI on PR |
| Unit + integration tests | Logic errors, edge cases | Minutes | CI on PR |
| Adversarial review prompt | Reasoning gaps, missing cases | Minutes | Developer, manually |
| Human line-by-line review | Intent mismatch, architecture | 15-60 min | Senior dev |
| Staging soak + observability | Race conditions, load behavior | Hours | Staging environment |
The mistake I see most often is teams running only the top layer (a human skims the diff) or only the bottom layers (CI is green, ship it). You need both. CI catches the mechanical stuff so your reviewer can spend their attention on the stuff machines cannot see.
Step 1: Never let AI code skip the type system
If your project is TypeScript, strict: true is non-negotiable, and noImplicitAny saves you from half of the hallucination class. When a model invents a function that does not exist, TypeScript tells you in two seconds. When it invents one in a JavaScript file, you find out in production.
For Python or PHP, this means type hints and a real type checker (mypy, pyright, PHPStan at a decent level). It is boring. It is also the single highest-return verification step you can take. For a broader look at why type systems catch so much of this, see the TypeScript handbook on everyday types.
One rule I enforce: AI-generated code may not use any, @ts-ignore, or as unknown as X to silence an error. If the model reaches for those, it is telling you it does not know the correct type. That is a signal to stop and look, not to cast your way past it.
If you are working in a modern React stack, this discipline pairs well with the patterns in our guide to React Server Components vs Client Components in Next.js 15, where type boundaries matter even more.
Step 2: Write the tests before you accept the code
This is the part most developers get backwards. They generate the code, then ask the model to generate tests for it. Now you have two artifacts from the same source of misunderstanding. The tests pass because they encode the same wrong assumption.
Instead, write the test cases yourself first, from the requirements. Then let the AI write the implementation. If the implementation fails your tests, you learned something real.
// I write this by hand, from the spec
describe('applyDiscount', () => {
it('rejects negative discount percentages', () => {
expect(() => applyDiscount(100, -5)).toThrow('Invalid discount');
});
it('caps discount at 100 percent', () => {
expect(applyDiscount(100, 150)).toBe(0);
});
it('rounds to two decimal places', () => {
expect(applyDiscount(19.99, 33.333)).toBe(13.33);
});
});
Then I hand the model the function signature and the test file. In my experience the first generation often fails the rounding test. That is exactly the kind of bug that ships silently when you let the AI write both sides.
For critical paths (payments, auth, data mutations), I also want property-based tests. fast-check in JS or hypothesis in Python will find edge cases neither you nor the model thought about. You can read more in the fast-check documentation and the Hypothesis documentation.
Step 3: Run an adversarial review prompt, then a human review
Here is a trick that has saved me more than once. Before a human looks at the diff, I paste the AI-generated code back into a fresh model session with a hostile prompt:
Review this code as if you are trying to break it in production.
List every assumption it makes about input, ordering, concurrency,
and external services. For each, describe a scenario where the
assumption fails. Do not suggest style improvements.
The adversarial pass catches a surprising amount: missing null checks, unhandled promise rejections, assumptions about array ordering, retries without backoff. It is not a substitute for human review, but it makes the human review faster because the obvious stuff is already flagged.
Then a human reads it. Not skims. Reads. And the human review has one job the AI cannot do: checking whether the code matches the actual business requirement, not the prompt's approximation of it. If the code touches user-facing behavior, this is the same discipline we apply when securing AI agents in production, the model is a component, not an authority.
Step 4: Wire verification into CI/CD so it is not optional
Verification that depends on developer discipline will erode. Put it in the pipeline.
# .github/workflows/verify.yml (trimmed)
name: verify
on: [pull_request]
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx tsc --noEmit
- run: npx eslint . --max-warnings=0
- run: npx semgrep ci --config=p/typescript --config=p/owasp-top-ten
- run: npm test -- --coverage --coverageThreshold='{"global":{"lines":80}}'
Two things matter here. First, --max-warnings=0. If warnings are allowed, AI-generated code will accumulate them until nobody reads them. Second, the coverage threshold. AI code tends to be broad but shallow; a coverage gate forces the tests to actually exercise the new branches. (Semgrep's registry rulesets are documented at semgrep.dev/explore.)
For anything touching auth, payments, or user data, I add a manual approval gate. No exceptions, no matter how clean the diff looks.
Step 5: Treat AI code as untrusted input at runtime
This is the layer most teams skip entirely. Static analysis and tests verify code at build time. They cannot verify behavior under real load, real user input, or real third-party failures.
So I instrument. Every AI-generated endpoint gets structured logging, a trace ID, and at least one alert on error rate. For background jobs and webhook handlers, I add idempotency keys and a dead-letter queue, because that is exactly where the retry bug in my opening story lived.
If you are building AI automation or MCP servers, this matters even more. An MCP server exposes tools to a model, and a hallucinated tool call can mutate real data. I always validate tool arguments against a schema on the server side, never trust the model's payload, and log every invocation with the calling context. The model is a client. Treat it like one. For a deeper look at how these servers fit into a real stack, see MCP servers for business: how to integrate AI tools into your existing workflow.
AI code review best practices that actually hold up
A few rules I have settled on after enough incidents.
- Small diffs. If an AI PR is over 400 lines, split it. Review quality collapses past that.
- One concern per PR. Generated code loves to bundle refactors with features. Reject that.
- No AI-generated code in auth or payment paths without a second human reviewer. Always.
- Track your AI code. I tag commits or use a PR label. When something breaks, knowing whether a human or a model wrote it changes how you debug.
- Review the tests as carefully as the code. A test that asserts nothing is worse than no test.
- Ban generated migrations. Schema changes need a human who knows the data.
None of this slows a team down much once it is habitual. What slows a team down is a production incident caused by code nobody understood.
What this means if you are hiring
If you are a founder looking to hire a Next.js developer or an automation developer, ask one question in the interview: "Show me how you verify AI-generated code." The answer tells you more than any portfolio.
A developer who says "I just review it" is a liability in 2026. A developer who has a pipeline, a test-first habit, and a clear line about what AI is not allowed to touch is worth paying for. The best website developer is not the one who generates the most code fastest. It is the one whose code still works six months after the demo. If you want to know what to look for in that conversation, our guide on how to hire an automation developer in 2026 covers the skills and interview questions that separate the two.
I build this way on every project, whether it is a Next.js app, a WordPress integration, or an MCP server wired into a client's internal tools. If you want that process applied to your product, or you want a second opinion on an AI-generated codebase that has gotten away from your team, get in touch.
FAQ
Q: How do I verify AI-generated code without slowing down development?
A: Push the mechanical checks into CI so they run automatically (type checking, linting, static analysis, tests), then reserve human review for architecture, intent, and anything touching auth, payments, or data. Small diffs and test-first habits make the human layer fast. The slowdown people fear usually comes from reviewing 2,000-line PRs, not from the verification itself.
Q: Can I trust AI-generated tests?
A: Only if you wrote the test cases first. When the same model writes the implementation and the tests, both encode the same misunderstanding and everything passes. Write your assertions from the spec, then let the AI implement against them. For critical paths, add property-based tests to catch edge cases neither you nor the model considered.
Q: What is the biggest risk of AI-generated code in production?
A: Plausible-but-wrong logic. Hallucinated APIs fail loudly and get caught by the type checker. Subtle bugs in retries, async ordering, and edge-case handling compile cleanly, pass the happy path, and surface weeks later under real load. That is why runtime observability and idempotency matter as much as static review.
Q: Do I need formal verification tools for AI-generated code?
A: For most web and business applications, no. A solid pipeline of type checking, static analysis, tests, and human review covers the realistic risk. Formal methods earn their cost in safety-critical or high-compliance systems, not in a typical SaaS feature. Spend the effort on test coverage and observability first.
Frequently asked questions
How do I verify AI-generated code without slowing down development?
Push the mechanical checks into CI so they run automatically (type checking, linting, static analysis, tests), then reserve human review for architecture, intent, and anything touching auth, payments, or data. Small diffs and test-first habits make the human layer fast. The slowdown people fear usually comes from reviewing 2,000-line PRs, not from the verification itself.
Can I trust AI-generated tests?
Only if you wrote the test cases first. When the same model writes the implementation and the tests, both encode the same misunderstanding and everything passes. Write your assertions from the spec, then let the AI implement against them. For critical paths, add property-based tests to catch edge cases neither you nor the model considered.
What is the biggest risk of AI-generated code in production?
Plausible-but-wrong logic. Hallucinated APIs fail loudly and get caught by the type checker. Subtle bugs in retries, async ordering, and edge-case handling compile cleanly, pass the happy path, and surface weeks later under real load. That is why runtime observability and idempotency matter as much as static review.
Do I need formal verification tools for AI-generated code?
For most web and business applications, no. A solid pipeline of type checking, static analysis, tests, and human review covers the realistic risk. Formal methods earn their cost in safety-critical or high-compliance systems, not in a typical SaaS feature. Spend the effort on test coverage and observability first.