Closing the Loop on AI-Generated Code with Automated Review
Updated
- Code Review
- Quality
- AI-Generated Code
Linters and type checkers catch rule-based errors, but they cannot settle every design question. I use a separate AI reviewer to examine the diff without the authoring session's context, then gate commits on the findings. It is an additional review layer, not a replacement for tests, security checks, or human judgment.
I shipped an entire DevSecOps pipeline in an afternoon last week. Bandit, semgrep, osv-scanner, gitleaks, license compliance, SBOM generation, and container scanning, all wired into GitLab CI with SARIF conversion and unified reporting. The code worked. The tests passed. I committed it.
Then I looked at what I’d actually written.
There was copy-pasted error handling in three modules, a function that grew to 40 lines because I kept saying “add this check too,” and a hardcoded CVSS threshold buried in a helper that should have been a config value. That kind of code passes every linter and scanner and still makes you wince six months later.
The AI wrote the code and the AI passed the tests. The AI also generated tech debt.
I approved all of it because I’d been watching it get written in real time, and my brain had already pattern-matched it as “correct.”
Linters are table stakes #
A mature Python pipeline already has layers of mechanical checks:
Pre-commit
ruff (lint + format), ty (type errors), gitleaks (secrets), commitizen (commit format)
Pre-push
pytest (regressions), semgrep OWASP (security patterns)
CI
bandit (Python SAST), osv-scanner (dependency vulns), pip-licenses (license compliance), guarddog (typosquatting), checkov / cdk-nag (IaC misconfigs), trivy (container scanning), syft (SBOM generation)
Every one of these tools is open source and runs in CI without vendor lock-in.
None of them will tell you that you just violated the Single Responsibility Principle.
The judgment gap #
Static analysis operates on rules, binary checks with deterministic answers. A 15-line function might be fine, or it might be doing two unrelated things that happen to fit in 15 lines. Duplicated logic across three modules might be intentional or accidental.
Experienced reviewers catch this through years of pattern-matching. When AI writes the code, though, the review step often gets compressed or skipped. The human watched it get generated, saw the reasoning, approved each step. By the time the diff is ready, the reviewer’s judgment is already anchored to the AI’s design decisions.
I tested this directly: I set up a Claude Code hook that blocked git commit and told Claude to review its own staged diff. Claude ran git diff --cached, scanned the output, and reported: “Code looks clean. No issues found.”
It had written that code five minutes earlier in the same conversation. The anchoring effect was identical to human self-review, only faster, because the AI’s memory of its own reasoning was perfect.
Context isolation is the fix #
Code review works because the reviewer lacks context. The missing context is what makes the review useful.
A human reviewer didn’t sit through the pairing session. They see only the diff and evaluate it on its own merits.
Claude Code’s Task tool spawns independent subagents. Each starts with a blank context, no conversation history, and no memory of prior decisions. Combined with a PreToolUse hook, the architecture works like this:
Developer says "commit this"
→ Claude tries git commit
→ PreToolUse hook intercepts → BLOCK
→ Creates session-keyed marker file
→ Returns instructions: "Spawn an independent reviewer"
→ Claude captures git diff --cached
→ Spawns Task subagent (fresh context, no history)
→ Subagent reviews diff against structured checklist
→ Returns findings with severity ratings + root cause analysis
→ Error findings? Fix first, then retry
→ Warnings/info only? Retry the commit
→ Second attempt finds marker → ALLOW
→ Lefthook runs (ruff, ty, gitleaks, commitizen)
→ Commit succeeds
The subagent receives only the raw diff and a review checklist. It has no access to the conversation that produced the code. It evaluates the diff the way any external reviewer would, without anchoring to the author’s intent.
What the reviewer evaluates #
The checklist targets categories static analysis misses:
- DRY violations: duplicated logic, copy-pasted blocks, structural duplication that linters can’t detect
- SOLID violations: multiple responsibilities (SRP), modification instead of extension (OCP), concrete dependencies where abstractions would decouple (DIP)
- Code smells: methods over 20 lines, nesting deeper than 3 levels, feature envy, Law of Demeter violations, data clumps
- Tech debt markers: TODO/FIXME/HACK in new code, hardcoded values, overly broad exception catches, implicit coupling
- Dead code: unreachable branches, unused imports/variables, commented-out blocks, uncalled functions
Each finding includes a 5 Whys root cause chain, five sentences tracing the surface symptom back to its architectural origin. That is the difference between “extract this method” (symptom treatment) and “this module lacks a formal data contract between its input parser and validator” (architectural fix). Surface findings recur, while root cause fixes compound.
The severity contract #
The system has to work at speed or it gets bypassed. There are three levels:
| Severity | Behavior | Scope |
|---|---|---|
| Error | Blocks the commit | Bugs, broken logic, severe architectural violations that compound over time |
| Warning | Reported, doesn’t block | DRY violations with 3+ repetitions, meaningful complexity, tech debt in new code |
| Info | Observational | Minor smells, refactoring suggestions, worth knowing but not worth stopping for |
The key calibration is that only errors block. If warnings blocked commits, every session would end in a review-fix-review loop that adds twenty minutes per commit. That’s the fastest way to get developers to disable the hook entirely.
A real example #
On the project where I built this system, I staged changes across three files (a new CI job, a security report script, a config update) and said “commit this.”
The hook blocked and spawned the reviewer subagent. Findings:
Race condition in the marker file: touch + [ -f ] isn’t atomic, so a crash between marker creation and commit retry leaves an orphaned file
Removed lefthook code-critic jobs but didn’t delete the script file they referenced. Dead code in the repo
Hardcoded model name (sonnet) buried in a heredoc. Should be configurable
Triple jq parse of the same stdin input. Wasteful and masks integration bugs if the JSON is malformed
There were no errors, so the commit went through on retry. The warnings were legitimate, though, the kind of things I’d catch in a human review two days later, when the context had faded. The subagent caught them in thirty seconds.
Extending to your stack #
The pattern works across stacks:
| Stack | Static Layer | Subagent Focus |
|---|---|---|
| TypeScript | ESLint + Prettier, tsc --noEmit, Knip | React anti-patterns, generic usage, module boundary violations |
| Rust | cargo clippy, cargo check, cargo audit | Ownership patterns, lifetime complexity, unsafe block justification |
| IaC | Checkov (TF/CFN/K8s), cdk-nag (CDK) | Least-privilege IAM, encryption-at-rest defaults, network isolation |
The playbook #
The whole implementation is three files and five minutes:
.claude/hooks/review-before-commit.sh: the PreToolUse hook (~80 lines of bash).claude/settings.json: registers the hook (4 lines of JSON)scripts/code-critic.sh: standalone version for terminal use outside Claude Code sessions
It needs no external services, extra API keys, or plugins.
Adding this to your workflow #
You likely have the mechanical layer already: linting, formatting, type checks, secret scanning, IaC misconfigs. The gap is judgment.
1. Add the judgment layer. An independent AI reviewer for design quality (DRY, SOLID, smells, debt, dead code). Context isolation prevents self-review bias.
2. Gate on severity. Errors block, warnings inform, and info educates.
3. Require root cause analysis. “5 Whys” traces symptoms to architectural origins, so fixes compound instead of patching.
4. Layer the defenses. Pre-commit for fast local checks. Pre-push for tests and deeper scans. CI for everything else.
The AI builds, an independent AI reviews, and the quality bar holds without slowing the build.
What’s your approach to automated code review in AI-assisted workflows? If you’ve built hooks, pipelines, or review gates for AI-generated code, I want to hear about it. Reach out on LinkedIn.
Review starts after there is code to inspect. Checking a spec for contradictions moves one class of defect earlier, before implementation.