# Closing the Loop on AI-Generated Code with Automated Review

By Laith Al-Saadoon · Published 2026-02-12 · Updated 2026-09-11

Canonical: https://laithalsaadoon.com/notes/closing-the-loop

Topics: Code Review, Quality, AI-Generated Code

Independent AI review can catch design problems that linters miss. Build a review loop that complements tests, static analysis, and human judgment.

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.”

•   •   •

<a id="linters-are-table-stakes"></a>

## 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.

•   •   •

<a id="the-judgment-gap"></a>

## 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.

•   •   •

<a id="context-isolation-is-the-fix"></a>

## 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.

•   •   •

<a id="what-the-reviewer-evaluates"></a>

## 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.

•   •   •

<a id="the-severity-contract"></a>

## 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 id="a-real-example"></a>

## 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:

Warning tech debt

Race condition in the marker file: `touch` + `[ -f ]` isn’t atomic, so a crash between marker creation and commit retry leaves an orphaned file

Warning tech debt

Removed lefthook code-critic jobs but didn’t delete the script file they referenced. Dead code in the repo

Info tech debt

Hardcoded model name (`sonnet`) buried in a heredoc. Should be configurable

Info code smell

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.

•   •   •

<a id="extending-to-your-stack"></a>

## 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 |

•   •   •

<a id="the-playbook"></a>

## The playbook

The whole implementation is three files and five minutes:

1.  `.claude/hooks/review-before-commit.sh`: the PreToolUse hook (~80 lines of bash)
2.  `.claude/settings.json`: registers the hook (4 lines of JSON)
3.  `scripts/code-critic.sh`: standalone version for terminal use outside Claude Code sessions

It needs no external services, extra API keys, or plugins.

<a id="adding-this-to-your-workflow"></a>

## 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](https://www.linkedin.com/in/laithalsaadoon/).

Review starts after there is code to inspect. [Checking a spec for contradictions](https://laithalsaadoon.com/notes/symspec) moves one class of defect earlier, before implementation.

## Related reading

- https://laithalsaadoon.com/notes/symspec

- https://laithalsaadoon.com/notes/erpaval

