← Lab

Technical guardrails for LLM-assisted code: the full catalog

11 min read ·

Our report More code, less confidence maps where quality breaks in AI-assisted teams: pull requests 2.5x larger, duplicated blocks up 81% in three years, error-masking constructs up 47%, review time up 441%. It ends on the habits of the teams that escape it — small batches, gates in CI, a Definition of Done that doesn’t bend.

This is the other half: the concrete catalog behind those habits. Each layer catches what the previous one missed. The failure modes they target are the ones in the 2025–26 data — oversized PRs, duplication, error-masking, “almost right” code, hallucinated dependencies, and review overload.

Ordering principle: the cheapest gate is the one closest to the moment code is written. Agent hooks, then pre-commit, then CI, then review, then production — in rising order of cost. Push every check as far left as it can run.


Layer 0 — Agent and editor context (before code exists)

Prevention starts before the LLM writes a line. Most “bad AI code” is the model guessing conventions it was never given.

MeasureToolingWhat it prevents
Repo context filesAGENTS.md / CLAUDE.md / .cursor/rules / .github/copilot-instructions.mdModel inventing conventions: naming, layer rules, banned patterns, “always use our @app/http-client, never raw axios”
Machine-readable conventionsdocs/adr/ (ADRs), CONTRIBUTING.md, example modules as templatesArchitecture drift; the agent copies the golden path instead of imagining one
Scaffolding by generator, not freeformNest CLI schematics, Nx generators, Hygen/Plop templatesStructural inconsistency: every module gets identical shape (controller/service/repo/dto/spec)
Pinned toolchainVolta / mise / .nvmrc / packageManager field + corepack, devcontainer”Works on the agent’s machine”: agent and CI run identical Node, pnpm, tsc versions
Internal libs surfaced to the agentMonorepo path aliases, published typedocs, MCP servers over internal docsDuplication: the model reuses @app/* packages instead of re-implementing them (the GitClear failure mode)
Editor-integrated feedbackESLint/tsc/Prettier on save in IDE and in the agent loopErrors discovered at CI instead of at write time

Layer 1 — Compiler and type system

The cheapest reviewer you have. Make it maximally strict; LLM code that “almost works” often dies right here.

MeasureConfigWhat it prevents
Full strict mode"strict": true plus noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noFallthroughCasesInSwitch, noPropertyAccessFromIndexSignatureWhole classes of “almost right”: undefined access, silent optional mismatches
No escape hatches@typescript-eslint/no-explicit-any, no-unsafe-* family, ban-ts-comment (require description, forbid @ts-ignore)The model silencing the compiler instead of fixing the type
Type coverage as a metrictype-coverage with a ratchet (never below current %)Gradual any erosion across hundreds of small AI commits
Typecheck as a standalone gatetsc --noEmit in pre-push and CI (not only via bundler)Bundlers (esbuild/SWC) that transpile without checking, shipping type errors

Layer 2 — Linting, complexity and hygiene

This is where you encode “bring sense to the code” as executable rules instead of review comments.

Core: typescript-eslint with strict-type-checked + stylistic-type-checked presets (or Biome if you trade rule depth for speed). --max-warnings 0 — a warning is a future ignored error.

ConcernRules / toolsWhat it prevents
Complexity budgetscomplexity (cyclomatic), sonarjs/cognitive-complexity, max-lines, max-lines-per-function, max-depth, max-params300-line generated functions nobody can review
Duplicationjscpd in CI with threshold (e.g. fail above 3% new duplication), SonarQube duplication on new codeThe +81% copy/paste trend, directly
Error maskingno-empty (no empty catch), @typescript-eslint/no-floating-promises, no-misused-promises, eslint-plugin-promise, prefer-promise-reject-errors, forbid catch {} and .catch(() => {}) via no-restricted-syntaxThe +47% rise in constructs that swallow failures — and unhandled rejections killing a Nest process
Dead codeknip (unused files, exports, dependencies), no-unused-vars, unused-importsOrphan helpers the agent generated “just in case”
Import disciplineeslint-plugin-import (no-cycle, no-extraneous-dependencies, order), no-restricted-importsUndeclared deps, tangled import graphs, reaching into other modules’ internals
Pattern bansno-restricted-syntax / no-restricted-properties: ban process.env outside the config module, raw console.* (force the logger), new Date() in domain logic (force clock provider), raw SQL outside repositoriesThe model bypassing your abstractions because it doesn’t know they’re mandatory
Zero-diff formattingPrettier or Biome, enforced in hook + CIFormatting noise inflating AI diffs and hiding real changes in review
Naming and API shape@typescript-eslint/naming-convention, explicit-function-return-type on exported APIsInconsistent public surfaces across generated modules
Custom org rulesLocal ESLint plugin (or GritQL/ast-grep rules)Anything you keep repeating in review: encode it once, never comment it again

Layer 3 — Architecture enforcement (NestJS scope rules)

Linting checks lines; this layer checks the shape. It’s the answer to “rules for ensuring scope of NestJS architecture” — make the layer diagram executable.

dependency-cruiser — the workhorse. Declare what may import what; CI fails on violation:

// .dependency-cruiser.cjs — examples
module.exports = {
  forbidden: [
    { name: 'no-circular', severity: 'error', from: {}, to: { circular: true } },
    { name: 'domain-is-pure',            // domain imports nothing from infra/http
      from: { path: '^src/.*/domain' },
      to:   { path: '^src/.*/(infrastructure|controllers|typeorm|prisma)' } },
    { name: 'controllers-use-services',  // controllers never touch repositories
      from: { path: '\\.controller\\.ts$' },
      to:   { path: '(\\.repository\\.ts$|/entities/)' } },
    { name: 'no-cross-module-internals', // other modules only via its public index
      from: { path: '^src/modules/([^/]+)/' },
      to:   { path: '^src/modules/(?!$1)[^/]+/(?!index\\.ts)' } },
  ],
};
MeasureToolingWhat it prevents
Dependency rules + orphan/circular detectiondependency-cruiser (graph output doubles as living architecture doc)Controllers→DB shortcuts, cross-domain reach-ins, cycles
Circular imports (fast check)madge --circular srcNest DI failures (forwardRef creep) and unloadable module graphs
Tagged module boundaries in monoreposNx @nx/enforce-module-boundaries with tags (type:feature, type:data-access, scope:billing) or eslint-plugin-boundariesscope:billing importing scope:orders internals; util libs importing features
Architecture as unit teststs-arch (“classes in domain/ depend only on domain/“)Regressions in rules too nuanced for path globs
Nest module encapsulationMinimal exports per module; barrel-file discipline; forbid @Global() except allow-listed”Everything is public so everything gets coupled”
DI disciplineBan new SomeService() in app code via no-restricted-syntax; constructor injection onlyThe model instantiating services by hand and bypassing lifecycle, scopes and testability

Layer 4 — Data and API contracts

The “almost right” code that hurts most fails at boundaries. Pin every boundary with a schema.

MeasureToolingWhat it prevents
Runtime validation of every inputNest ValidationPipe with whitelist, forbidNonWhitelisted and transform enabled globally + class-validator DTOs, or nestjs-zodUnvalidated fields flowing in because the model trusted the client
Typed, validated config@nestjs/config + Zod/Joi schema on startup — fail fast on missing envHallucinated env vars discovered in production
API contract lint + breaking-change gate@nestjs/swagger generated spec + Spectral lint + oasdiff/openapi-diff vs main in CISilent breaking changes to consumers inside a big generated diff
Consumer contract testsPact (if you have internal consumers)“It compiles here” breaking another team
Serialization controlClassSerializerInterceptor + explicit @Expose/@ExcludeEntity fields (password hashes, internal ids) leaking into responses
Migration gatesMigrations only (never synchronize: true); prisma migrate diff / TypeORM schema:log clean check; squawk lint for destructive Postgres DDL; require down-migrationsThe model “fixing” a bug by mutating schema, or dropping a column in a 400-line PR

Layer 5 — Local git hooks (pre-commit / pre-push)

Fast feedback before anything leaves the machine. Keep pre-commit under ~5 s or people (and agents) will bypass it.

MeasureToolingWhat it prevents
Staged-files lint + format + related testshusky + lint-staged (ESLint --fix, Prettier, optionally vitest related --run)Broken style/lint ever entering history
Full typecheck + affected tests on pushpre-push hook: tsc --noEmit + nx affected -t test / turbo run test --filter=...[origin/main]Pushing red builds; keeps pre-commit fast by deferring slow checks
Secret scanninggitleaks or trufflehog as pre-commit + CI backstopAPI keys the model copied from context into code
Commit message contractcommitlint + Conventional Commits (feat(billing): ...)Unreadable history; enables semantic-release and scope-based review routing
Commit size guardCustom hook or Danger local run warning/failing above ~300–400 changed LOCThe 2.5x oversized-diff habit, stopped at the source
Branch hygieneHook blocking direct commits to main/master”The agent pushed to main”
No bypass culture--no-verify allowed only with HOOK_BYPASS_REASON= env, logged in CIGuardrails that exist on paper only

Layer 6 — Agent-native guardrails (when the LLM finishes, before commit)

The newest layer: deterministic hooks around the agent itself, so verification happens before a human ever sees the diff.

MeasureToolingWhat it prevents
Post-edit hooksClaude Code hooks (PostToolUse → run eslint --fix + tsc --noEmit on touched files); equivalent hooks in Cursor and other agentsThe agent accumulating 40 broken files before anyone notices
”Done” gateStop-hook that runs lint + typecheck + affected tests and refuses completion until green”I’m finished” with failing tests — the agent must prove done
Scope confinementAgent permission config: deny-list .env*, migrations/, .github/workflows/, lockfiles, auth/ + payment paths; allow-list working dirsThe model “helpfully” editing CI, secrets, or the money path
Plan-before-applyRequire plan/diff approval for changes touching more than N files or protected pathsSprawling refactors nobody asked for
Sandboxed executionDevcontainer/VM: no prod credentials, egress-restricted network, disposable filesystemPrompt-injected or hallucinated commands reaching real systems
Independent verifier passSecond model/session prompted to refute the change (“find why this is wrong”) before PRAuthor-bias: the same context that wrote the bug approving it
Tests move with codeAgent instructions + Danger rule: src/** changes without *.spec.ts changes failGenerated code with zero or assertion-free tests
ProvenanceCommit trailer / PR label AI-assisted: yes + session log attachedReviewers calibrating scrutiny blind; no audit trail

Example (Claude Code, .claude/settings.json):

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Edit|Write",
      "hooks": [{ "type": "command",
        "command": "npx eslint --fix $CLAUDE_FILE_PATHS && npx tsc --noEmit" }]
    }],
    "Stop": [{
      "hooks": [{ "type": "command",
        "command": "pnpm lint && pnpm typecheck && pnpm test:affected" }]
    }]
  }
}

Layer 7 — CI/CD pipeline gates (the PR wall)

Everything above runs advisory-fast; CI is where it becomes law. All gates as required status checks — non-required checks are decoration.

Correctness and tests

MeasureToolingWhat it prevents
Build + typecheck + lint (zero warnings)tsc --noEmit, ESLint --max-warnings 0Baseline breakage
Coverage on new code (diff coverage)Vitest/Jest coverage + Codecov/Coveralls patch check (e.g. 80% or more on the diff) or Sonar “Clean as You Code”Coverage theater: global % stable while every new line ships untested
Mutation testing on critical modulesStryker (nightly or on core/, billing/, auth/ paths)AI-written tests that execute code but assert nothing
Real-infra integration testsTestcontainers (Postgres/Redis/Kafka) + supertest e2eCode that passes against mocks and dies against reality
Flaky-test quarantineRetry-with-report, quarantine tag, flake dashboardRed-is-normal culture that trains everyone to ignore CI
Quality gate on new codeSonarQube/SonarCloud: new duplication under 3%, new issues at zero, maintainability rating on diffDebt entering silently commit by commit
Duplication deltajscpd gate vs mainCopy/paste growth (the GitClear +81%)
PR size gateDanger.js / GitHub Action: fail or hard-label above ~400 changed lines (excluding lockfiles/generated)Unreviewable diffs; enforces the small-batch discipline DORA ties to elite performance
PR metadata contractDanger: linked ticket, description, risk note, test evidenceContext-free diffs that force reviewers to reverse-engineer intent
API breaking-change checkoasdiff vs main (from Layer 4)Accidental contract breaks

Security and supply chain

LLMs made this layer non-optional.

MeasureToolingWhat it prevents
SASTCodeQL and/or Semgrep (+ custom org rules)Injection, path traversal, authz mistakes in generated code
Dependency vettingSocket.dev / GitHub dependency review: block new deps younger than N days, typosquats, install scripts, no repoSlopsquatting — attackers pre-registering package names LLMs hallucinate
New-dependency approvalCI check: any new entry in package.json requires a labeled approvalThe model adding left-pad-utils-2 because it “seemed to exist”
Lockfile integritynpm ci/pnpm i --frozen-lockfile only; lockfile-lint (registry pinning, no git/http deps)Lockfile tampering and registry swaps hidden in big diffs
Vulnerability + license scanosv-scanner / npm audit signals via Renovate; license-checker gateKnown CVEs; GPL surprises in a commercial codebase
Controlled updatesRenovate with minimumReleaseAge (e.g. 7–14 days), grouped PRs, automerge patch-only when greenFresh-malicious-version attacks; agents bumping deps ad hoc
Secret scan (server-side)gitleaks in CI + GitHub push protectionKeys that slipped past local hooks
SBOM + provenanceSyft SBOM, npm provenance/SLSA attestations on your own publishesUnanswerable “are we affected?” moments

Repo policy

MeasureToolingWhat it prevents
Branch protectionRequired checks, at least one human review, dismiss stale approvals, no force-push, linear historyMerging around the gates
CODEOWNERS on critical pathsauth/, billing/, migrations/, .github/ → mandatory named reviewersHigh-blast-radius changes reviewed by whoever was fastest
No self-approval for bot PRsRule: agent-authored PRs cannot be approved by their invoker onlyRubber-stamping your own agent
Merge queueGitHub merge queue / MergifySemantically conflicting PRs that are individually green breaking main
AI first-pass reviewCodeRabbit / Greptile / Copilot code review / Claude as reviewer — advisory, never replacing the human gateReviewer fatigue on mechanical issues; frees humans for design and intent

Layer 8 — Post-merge and runtime (assume something got through)

MeasureToolingWhat it prevents
Feature flags + kill switchesOpenFeature + Unleash/Flagsmith; new AI-touched paths ship darkRollback requiring a redeploy at 3 a.m.
Progressive deliveryCanary/blue-green (Argo Rollouts), auto-rollback on error/latency burnFull-blast exposure of a defect to 100% of traffic
Observability contractOpenTelemetry + structured logs (pino) required on new endpoints; Sentry release tagging tied to PRs”It’s broken but we can’t see where”; regressions untraceable to the change that caused them
SLOs + error budget policyBurn-rate alerts; feature freeze when budget spentQuality debates decided by opinion instead of budget math
Delivery + churn telemetryDORA metrics (deploy freq, CFR, MTTR) + PR telemetry: size trend, review pickup time, 2-week churn (Apache DevLake, LinearB, Faros, DX)Not knowing whether any of the above is working
Scheduled debt workSonar new-debt reports feeding a standing 15–20% sprint budget; nightly knip/jscpd/audit reportsThe −74% legacy-maintenance collapse happening to you

If you only adopt ten things

The elite-team cut:

  1. TypeScript maximal strict + tsc --noEmit as a required check.
  2. typescript-eslint strict-type-checked with complexity budgets and error-masking bans, --max-warnings 0.
  3. Prettier/Biome + husky + lint-staged + commitlint.
  4. dependency-cruiser rules for your NestJS layers + no-cycle, as a required check.
  5. Global ValidationPipe (whitelist + forbid) and Zod-validated config.
  6. Agent hooks: post-edit lint/typecheck + a stop-gate running affected tests; deny-list for secrets, migrations, CI config.
  7. Diff coverage of 80% or more + Sonar quality gate on new code; Stryker on the money paths.
  8. PR size gate (~400 lines) + Danger metadata contract + CODEOWNERS + merge queue.
  9. Socket/dependency-review + frozen lockfile + Renovate with minimumReleaseAge.
  10. Feature flags + canary + OTel/Sentry tied to releases, with DORA + churn dashboards to verify it’s working.

Two honest caveats. Gates only work if they’re fast (parallelize CI, cache, affected-only runs) — slow gates get bypassed, and bypassed gates are worse than none. And no pipeline reviews intent: these guardrails buy your humans the time to do the one review only humans can do — “is this the right change?”

That is the same conclusion the data reaches in More code, less confidence: AI is an amplifier. This catalog is what “a strong engineering system” looks like when you write it down as configuration.

Further reading