Skip to content

AI-Powered Code Quality Gates

AI-powered code quality gates are layered verification: an agent that fixes lint and type errors as it writes, a headless review that scans every pull-request diff before a human looks, and continuous monitoring that fails the build when coverage or complexity regresses. Each layer catches the mechanical defects the previous one let through, across Cursor, Claude Code, and Codex.

A null slipped past review, shipped on Friday, and paged you at 2am. The diff looked fine — three reviewers approved it — but nobody noticed the unguarded response.data.user.id on a path that only fires for SSO logins. The Friday before that, a PR introduced a memory leak in the session handler: it passed every test, the reviewer approved it after a glance, and the linter had nothing to say. By Monday the service was eating 4GB of RAM and crashing every six hours.

Human review is the wrong tool for both. Style, type holes, N+1 queries, missing input validation, unhandled rejections, and slow resource leaks are what an agent wired into a quality gate catches every time, before a human ever opens the PR. This article builds that gate across Cursor, Claude Code, and Codex so reviewers spend their attention on architecture and intent instead of playing linter.

What this three-layer quality gate gives you

Section titled “What this three-layer quality gate gives you”
  • A shared standards file (.cursor/rules, CLAUDE.md, or AGENTS.md) that every agent checks its own output against
  • A working Claude Code hook that runs Prettier, ESLint, and tsc --noEmit on every file the agent edits — with the correct event-keyed schema and stdin file path
  • A headless AI PR-review step in GitHub Actions you can drop into any repo, on all three tools
  • An escalation ladder of review prompts: a routine stack-aware PR audit, a deep six-dimension review with severities, and a lens-by-lens split for when one pass produces mush
  • Continuous monitoring the agent cannot fake, plus prompts for triaging its output and for standing up mutation testing
  • Metrics that tell you whether the gate actually correlates with production stability
  • A failure-mode playbook for when the gate gets noisy, blocks CI on unrelated files, or hits diff limits

How the layers catch what the previous one missed

Section titled “How the layers catch what the previous one missed”

You want defects caught as early and as cheaply as possible, and no single layer is sufficient on its own. That means three, each catching what the one before it let through:

  1. Development-time — the agent fixes lint and type errors as it writes, inside the editor or hook loop. Cheapest possible feedback.
  2. Pre-merge — a headless agent reviews the diff in CI and posts findings on the PR before a human looks.
  3. Post-merge, continuous — a tool like SonarQube tracks coverage, complexity, and duplication trends so quality regressions show up as a graph, not a surprise at 2am.

The rest of this article builds each layer. Layers 1 and 2 are where the three tools differ, so they use <Tabs>.

Layer 1: the standards the agent checks its own work against

Section titled “Layer 1: the standards the agent checks its own work against”

All three tools read a project-level rules file and apply it to everything they generate. The file format and location differ; the content is nearly identical. Keep it in version control so the whole team — and every agent — works from the same standard. Write it as checkable conditions, not aspirations: a line limit and a complexity ceiling can be verified, “write clean code” cannot.

.cursor/rules/code-standards.mdc
---
description: Enterprise Code Quality Standards
alwaysApply: true
---
## Style
- 2-space indentation, max line length 100
- Every exported function has a JSDoc block
- No `any` without a `// eslint-disable-next-line` and a reason
- No magic numbers - use named constants
## Size and complexity
- No function longer than 50 lines; no file longer than 300
- Cyclomatic complexity under 10 per function
## Architecture
- Data access goes through the repository layer, never inline SQL in handlers
- Services receive dependencies via constructor injection
- All outbound HTTP calls go through the shared `httpClient` wrapper
- Error handling follows our Result<T, E> pattern (no bare try/catch)
## Performance
- Paginate any endpoint that returns a list
- No queries inside loops — batch with `IN (...)` or a join
- Memoize pure functions that run on every render
## Security
- Parameterized queries only
- Validate request bodies with the Zod schema in `schemas/`
- Never log tokens, passwords, or full request bodies

Automating layer 1: a Claude Code hook that actually loads

Section titled “Automating layer 1: a Claude Code hook that actually loads”

The most common mistake here is a hook config that silently never runs. Claude Code nests hook arrays under an event name (PostToolUse, PreToolUse) inside the top-level hooks object — a bare top-level hooks array will not load. The matcher is a regex run against tool_name, so it must be cased Write|Edit. And hooks do not receive the edited path in an environment variable; they read JSON on stdin and pull .tool_input.file_path.

Put the logic in a script so the config stays readable:

.claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "node scripts/quality-check.js" }
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format-and-lint.sh"
}
]
}
]
}
}
.claude/hooks/format-and-lint.sh
#!/usr/bin/env bash
set -euo pipefail
# The edited path arrives as JSON on stdin, not as an env var.
FILE_PATH=$(jq -r '.tool_input.file_path // empty')
[ -z "$FILE_PATH" ] && exit 0
npx prettier --write "$FILE_PATH"
npx eslint --fix "$FILE_PATH"
# Type-check only TS files; tsc does the type checking, not a linter.
case "$FILE_PATH" in
*.ts|*.tsx) npx tsc --noEmit ;;
esac

PreToolUse is the gate that can refuse a write before it happens — use it for policy checks that should stop an edit (a protected path, a forbidden import). PostToolUse is the fix-up pass. $CLAUDE_PROJECT_DIR is one of the few real hook variables (alongside $CLAUDE_ENV_FILE for SessionStart and $CLAUDE_CODE_REMOTE); wrap it in quotes so paths with spaces survive.

In Cursor, the equivalent is the auto-fix loop: when ESLint errors land in the Problems panel, the agent fixes them and re-runs until clean. Codex applies the same eslint --fix step inside its sandbox when you ask it to “make lint pass” as part of a task.

JavaScript / TypeScript

  • ESLint with your shared config
  • Prettier for formatting
  • typescript-eslint for TypeScript-aware lint rules
  • tsc --noEmit for type checking

Python

  • Ruff for fast linting (and formatting, replacing Black)
  • mypy for type checking
  • bandit for security lint

Java

  • Checkstyle for standards
  • SpotBugs for bug detection
  • PMD for code analysis

Go

  • golangci-lint aggregator
  • gofmt for formatting
  • go vet plus staticcheck

The single most useful in-editor habit is killing any the moment the checker points at it — an any is a hole the whole gate reads through. Cursor’s auto-fix loop does this when typed, but the prompt works in all three tools.

A clean result looks like this — the cast becomes a named, checkable contract:

interface UserResponse {
id: string;
status: 'active' | 'inactive';
metadata: Record<string, unknown>;
}
const data = response.data as UserResponse;

Layer 2: AI review of the diff before a human looks

Section titled “Layer 2: AI review of the diff before a human looks”

This is where the gate earns its keep. The setup is genuinely three-tool: each runs a headless agent against the PR diff and posts findings.

Cursor’s built-in PR review is BugBot. Enable it from the dashboard’s GitHub integration, then drop a .cursor/BUGBOT.md at the repo root to steer what it flags (see the review guidelines below). BugBot comments inline on the PR automatically once connected.

Both Cursor’s .cursor/BUGBOT.md and a prompt fed to Claude Code or Codex benefit from an explicit checklist. Keep it focused on what humans reliably miss:

# .cursor/BUGBOT.md (or paste into the review prompt)
## Security (block on any of these)
- Hardcoded credentials, tokens, or API keys
- Unparameterized SQL or string-concatenated queries
- Unvalidated request bodies reaching the database
- Missing auth check on a protected route
- User input rendered without escaping (XSS)
## Correctness
- Unhandled promise rejections / missing `await`
- Null/undefined dereferences on optional fields
- N+1 query patterns (a query inside a `.map`/loop)
## Quality
- New code without tests
- Functions over 50 lines or complexity over 10
- Logging that includes sensitive data

Drop this into any repo. It runs on pull requests and posts the agent’s findings. Note actions/checkout@v6@v3 is deprecated and forces JavaScript actions onto an unsupported Node runtime.

BugBot runs as a hosted GitHub integration, so there is no CI YAML to maintain — it reviews PRs automatically once enabled. Use the Claude Code or Codex tab if you want the review step to live in your own workflow file instead.

This non-interactive review cannot surface a new approval prompt. Use approval_policy=never only with a trusted, least-privilege CI identity; read-only remains the enforced sandbox boundary and any action requiring more access fails.

The routine review: one pass, short output

Section titled “The routine review: one pass, short output”

For everyday pull requests, keep the review narrow and stack-aware. The value is a short list of real defects, not a thorough essay.

The deep review: one report, with severities

Section titled “The deep review: one report, with severities”

A change to a payment path or a shared abstraction deserves more than the routine pass. This prompt adds the dimensions the routine one deliberately skips — architecture, maintainability, testability — and makes the agent classify each finding so you can triage it.

When one pass produces a shallow, blended answer — a common failure on a large or unfamiliar diff — run the dimensions as separate conversations instead. Each lens gets the model’s full attention, and you can throw away the ones that come back empty.

Run each lens as a separate Agent conversation for depth:

Lens 1 - Correctness: Review /src/services/payment.ts changes.
Assume every input is adversarial. Find every way this code could
produce incorrect results, crash, or behave unexpectedly.
Lens 2 - Performance: Same file. Assume 10,000 requests per second.
Find bottlenecks, memory leaks, and unnecessary allocations.
Lens 3 - Security: Same file. You are a penetration tester.
Find every way to exploit this code.

Layer 3: continuous monitoring an agent cannot fake

Section titled “Layer 3: continuous monitoring an agent cannot fake”

Real monitoring means real tooling, not a model recalling numbers. SonarQube (or SonarCloud) is the standard: it computes coverage, cyclomatic complexity, and duplication on every build and tracks the trend. Wire it into the same workflow:

# add to .github/workflows/ai-review.yml
- name: SonarQube scan
uses: SonarSource/sonarqube-scan-action@v6
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.qualitygate.wait=true

The qualitygate.wait=true flag blocks the PR if the project’s Sonar quality gate fails (for example, coverage on new code below 80% or a new blocker-severity issue). That is your enforcement point — concrete, measured, and not something an agent can talk its way around.

For the “what’s the AI’s read on this?” angle, feed Sonar’s findings to the agent rather than asking it to invent metrics:

A monthly health pass covers what a per-PR gate structurally cannot see: drift. Run it in a session where the agent can actually measure — a coverage report on disk, git log, a dead-code scan, a duplication scan — and say so in the prompt. Asked cold, a model will happily produce a plausible dashboard of numbers it never computed, which is worse than no dashboard.

Metrics that tell you whether the gate works

Section titled “Metrics that tell you whether the gate works”

Coverage percentage does not measure quality; it measures which lines ran. These are the numbers that move when the gate is actually working:

MetricWhat It RevealsTarget
Mutation scoreTests actually catch bugs, not just execute code> 75%
Mean time to detectHow quickly bugs are found after introduction< 1 sprint
Escaped defect rateBugs that reach production< 2% of changes
Review turnaroundHow long PRs wait for review< 4 hours
Rework ratePRs that need > 2 review rounds< 15%
Build reliabilityCI pipeline pass rate> 95%

Mutation score is the one worth standing up first, because it is the only metric on the list that cannot be gamed by writing more tests that assert nothing.

A common production regression is a query or endpoint that works fine in review and falls over under load. Bake load testing into the gate with k6 — the thresholds are real and make the test pass or fail on its own.

The generated test encodes the thresholds as gate conditions, so a regression turns the CI step red:

checkout.load.test.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 200 },
{ duration: '5m', target: 200 },
{ duration: '2m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.post(
`${__ENV.BASE_URL}/api/checkout`,
JSON.stringify({ cartId: 'c_1', paymentMethodId: 'pm_1', idempotencyKey: `${__VU}-${__ITER}` }),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'checkout' } },
);
check(res, { 'status 200': (r) => r.status === 200 });
}

A gate that only one repo runs is a personal habit. Distributing it means the standards themselves become a versioned artifact:

  1. Create a shared config package

    One package — in your monorepo or published to npm — holding the ESLint, TypeScript, and Prettier configs plus the AI rules files.

  2. Distribute through package management

    Each project extends the shared config. Local overrides must be documented and approved, not quietly added.

  3. Enforce in CI

    The pipeline checks that shared configs have not been overridden without approval.

  4. Point the AI rules at the shared standard

    .cursor/rules, CLAUDE.md, and AGENTS.md reference the shared standards document rather than each drifting its own copy.

  5. Review health monthly

    Run the codebase health pass on a schedule and compare teams against the shared benchmarks, so a regression is a trend line rather than an incident.

Once the prompts above prove useful, save them as reusable slash commands. A file at .claude/commands/security-audit.md becomes the /security-audit command inside an interactive Claude Code session (subdirectories add namespacing — .claude/commands/review/pr.md is /review:pr). Invoke it in the REPL:

> /security-audit

with the command file holding your OWASP-focused prompt. Cursor exposes the same idea through saved prompts; Codex through AGENTS.md workflows and custom prompts.