CI & Workflow Integration

Linting AI-Generated Code: Quality Gates for AI-Assisted Pull Requests

The gate that matters for AI-generated code is the one that runs before a human reads the diff. Machine-written code fails differently from human-written code: it is syntactically clean, stylistically consistent, and confidently wrong in places a skim will not catch. Automated checks are cheap and tireless in exactly the way review attention is not, so push everything a tool can decide — formatting, unused symbols, type errors, unhandled promises, hardcoded secrets, injection-shaped patterns — out of the review conversation entirely.

This is not a new pipeline so much as a change in what your existing one is for. When most code was typed by hand, linting mainly enforced consistency. When a large share of a diff arrives generated, the same tooling becomes the first reviewer.

What actually goes wrong in AI-generated code

Naming the failure modes matters, because they determine which rules earn their place.

Plausible-but-wrong APIs. Assistants generate calls that look idiomatic but do not exist in the version you have pinned, or that exist with different arguments. A type checker or a resolver-aware linter catches this immediately; a reviewer reading quickly often does not.

Silently dropped errors. Generated code frequently produces a try/catch that swallows the exception, a promise that is never awaited, or a returned error value nobody checks. These pass tests that only exercise the happy path.

Unused scaffolding. Imports, parameters, and intermediate variables left over from a shape the model started with and abandoned. Individually trivial; in aggregate they make later diffs noisy.

Copied-in credentials and example values. Prompted with "connect to the database," a model will happily produce a connection string with a literal password in it. This is the single highest-severity category and the easiest to automate.

Injection-shaped patterns. String-concatenated SQL, shell commands built from request data, unsanitised HTML. Models reproduce the patterns that are most common in training material, and unsafe patterns are common.

Duplication. The same helper regenerated three times in three files, because each request had no view of the others.

None of this is an argument against using assistants. It is an argument for making the checks non-negotiable, since the volume of code arriving for review has gone up and the time available to review it has not.

The four gates worth having

Run them in this order — cheapest and most deterministic first, so contributors get the fast failures fast.

  1. Format. A formatter such as Prettier, Ruff's formatter, or gofmt removes style from the discussion entirely. On generated code this matters more than usual, because assistants produce whatever style the surrounding sample suggested.
  2. Lint, including correctness rules. ESLint, Ruff, golangci-lint, or Clippy, configured with the rules that catch dropped errors and dead code rather than only stylistic ones.
  3. Type check. TypeScript's compiler, mypy or Pyright, or your language's equivalent. This is the highest-yield gate against hallucinated APIs, because a call to a function that does not exist with those arguments simply fails to compile.
  4. Secrets and security scanning. A secret scanner on every diff, plus a SAST tool for injection and taint-flow patterns.

If you only have budget for two, take type checking and secret scanning. They catch the two failure classes that are both most likely and most expensive.

Which lint rules pull their weight on generated diffs

Rule selection follows the failure modes. The general principle — turn on rules that catch bugs, be sparing with rules that merely express taste — is covered in which lint rules are worth the argument; what changes here is the weighting toward rules that detect abandoned or unchecked code.

For JavaScript and TypeScript, the type-aware rules are the ones that pay. A minimal flat config:

// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
  js.configs.recommended,
  tseslint.configs.recommendedTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
      },
    },
    rules: {
      "no-console": "warn",
      "@typescript-eslint/no-floating-promises": "error",
      "@typescript-eslint/no-misused-promises": "error",
      "@typescript-eslint/no-unused-vars": [
        "error",
        { argsIgnorePattern: "^_" },
      ],
    },
  },
);

The two promise rules are the point. Generated async code routinely calls something that returns a promise without awaiting it, which produces a function that returns before its work is done and swallows any rejection. That is invisible in review and intermittent in production.

For Python, an equivalent starting set in pyproject.toml:

[tool.ruff.lint]
select = ["E", "F", "B", "S", "ARG"]

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]

F covers undefined names and unused imports, B is the bugbear family of likely-bug patterns, S brings in security checks derived from Bandit, and ARG flags unused arguments — the leftover-scaffolding signal. The per-file ignore keeps assert legal in tests, where it is the point.

Whatever the language, resist the urge to enable everything at once because the code "is not yours." A wall of new violations trains people to skip the output, which is the failure mode described in the static analysis guide.

Wiring the gate so it is actually enforced

A check that runs only locally will be skipped on the day it matters. Put the authority in CI and use local hooks for speed.

A pre-commit configuration gives fast feedback and catches secrets before they ever leave the machine — which matters, because a credential pushed to a remote must be treated as compromised even after you rewrite history:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.9  # pin to the latest release
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4  # pin to the latest release
    hooks:
      - id: gitleaks

Then make CI the gate of record:

# .github/workflows/quality.yml
name: quality
on: [pull_request]

jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npx prettier --check .
      - run: npx eslint .
      - run: npx tsc --noEmit

The fetch-depth: 0 matters if you later add diff-scoped checks, which need the base commit to compare against. Branch protection turning these into required checks is what makes the whole thing real; the setup details are in the linting in CI guide.

Adopting this on a repo that is already messy

Turning on strict rules across an existing codebase produces thousands of violations and no behaviour change. Gate new code instead.

  • Baseline the existing violations. Most ecosystems have a mechanism: a lint baseline file, a suppression list, or # type: ignore comments generated in bulk. Record today's state as accepted debt.
  • Make the gate fail only on the diff. Run the checks against changed files, or compare the violation count to the baseline and fail when it rises.
  • Ratchet. Every time a file is touched, it must come out clean. Debt drains along the paths people actually work in, which are the paths that matter.
  • Tighten one rule family at a time. Ship the promise rules, let the team live with them for a sprint, then add the next set.

The same approach works for a SAST tool: run it in report-only mode, triage the existing findings once, then make new findings blocking.

What the gate cannot do

Static analysis validates shape, not intent. It will not tell you the generated function implements the wrong business rule, that the caching strategy is inappropriate, or that the code duplicates something the team already solved elsewhere. It also will not catch a subtly incorrect algorithm that type checks perfectly.

That is the real argument for automating the mechanical checks: it clears the review agenda so human attention lands on the questions only a human can answer. A reviewer who spends the first five minutes noting unused imports and inconsistent quoting has less left for the question of whether the code should exist at all.

FAQ

Do I need different lint rules for AI-generated code than for human code?

Not different rules — different priorities. Keep one config for the repo, since separate standards for machine-written code are unenforceable once the two are mixed in a single diff. What changes is the weighting: correctness rules that catch unused symbols, unhandled errors, and floating promises earn more than they used to, because those are the patterns generated code produces most.

Should CI reject a pull request that is mostly AI-generated?

Gate on the code, not its provenance, which you cannot reliably determine anyway. A policy of "AI-assisted PRs need a stricter check" invites people to under-report; a policy of "every PR passes the same gates" is enforceable and does not depend on anyone's honesty about their editor.

Can a linter detect that code was written by an AI?

No, and tools claiming to do so should be treated with caution. Linters analyse the code's structure, not its origin. Some teams require disclosure in the PR description for audit reasons, but that is a process control, not something static analysis can verify.

Is SAST worth adding if we already lint?

Yes, if you handle untrusted input. General-purpose linters flag some risky patterns, but tracking data from an input to a dangerous sink across function boundaries is taint analysis, which is a different capability. That distinction, and what each class of tool actually reports, is worth understanding before you buy anything.

How do I stop the checks from slowing down every pull request?

Cache dependencies, run the gates in parallel jobs, and scope the expensive ones to changed files or affected packages. Formatting and linting should finish in well under a minute on most repos; type checking and SAST are the slow ones, so they are the candidates for diff-scoping. A gate people wait five minutes for is a gate people learn to bypass.

Put the gates in before the volume arrives

The mechanics here are ordinary — a formatter, a linter with correctness rules on, a type checker, a secret scanner, all made blocking in CI. What has changed is the cost of skipping them, because the amount of plausible-looking code arriving for review keeps going up while review capacity stays flat. Start with one repository, baseline what is already there, and make new code pass. When it is time to choose the specific tools for your stack, compare code linters and static-analysis tools side by side on detection depth, speed, and configuration burden.

Comments are disabled for this article.