Ga naar hoofdinhoud
50% korting alle plannen, beperkte tijd. Vanaf $2.48/mo
14 min left
Developer-tools en DevOps

Een linter instellen voor AI-gegenereerde code

S By Sherwin 14 min read
ESLint output showing AI-specific lint violations in a TypeScript file

AI-agents kunnen code genereren die compileert.

Dat is nog steeds niet de lat die je wilt. De lat is code die geen dingen importeert die hij nooit gebruikt, zich niet met any-casts uit het typesysteem wurmt, foutretourwaarden niet negeert en geen credentials hardcodeert die gosec voor de review zou moeten opvangen.

AI-modellen trainen op oude Stack Overflow-antwoorden en leveren de patronen die ze daar geleerd hebben, inclusief verouderde API's, ontbrekende type-annotaties en functies die technisch correct zijn maar te groot om veilig te reviewen. Je hebt een linter in de loop nodig. Niet als suggestie, maar als gate.

Deze gids behandelt de configuratie voor drie ecosystemen (Python met Ruff, TypeScript/JavaScript met ESLint v10 flat config, en Go met golangci-lint) met regels die specifiek zijn afgestemd op de faalpatronen die AI introduceert. Daarna behandelt hij hoe je de gate veel moeilijker te omzeilen maakt, zodat de agent niet zomaar lokale hooks kan overslaan met --no-verify zonder dat een andere laag het opvangt.

De setup is gelaagd: linting op IDE-niveau vangt problemen inline op terwijl de agent schrijft, pre-commit hooks vangen alles op wat een commit-poging bereikt, en CI vangt alles op wat lokaal doorkomt. Elke laag is onafhankelijk, en je kunt de taalsecties kiezen die op jouw stack van toepassing zijn. De afdwingingslaag werkt hetzelfde, ongeacht welke taal je lint.

Samenvatting

  • Ruff (Python), ESLint v10 (TS/JS) en golangci-lint (Go) hebben elk specifieke regels die AI's meest voorkomende fouten opvangen
  • De configuraties hieronder zijn van toelichting voorzien; elke regel staat er met een reden
  • Lefthook regelt de pre-commit gate; Cursor's afterFileEdit hook draait lint inline
  • Vier afdwingingslagen maken het veel moeilijker voor AI-agents om de gate te omzeilen met --no-verify
  • CI is het laatste vangnet: agents kunnen --no-verify niet doorgeven aan GitHub Actions

Ruff configureren voor Python AI-code

Terminal output from Ruff flagging unused imports and a missing return type annotation in AI-generated Python code

Ruff is de juiste Python-linter voor AI-ondersteunde codebases. Hij is snel genoeg om bij elke bestandsopslag te draaien zonder iets te blokkeren, dekt zowel stijl- als echte logicafouten, en levert formatter-gedrag (ter vervanging van Black) in hetzelfde binary. De regels hieronder richten zich op de specifieke faalpatronen die AI-modellen het vaakst introduceren in Python.

Ruff installeren

pip install ruff

# or via uv (faster for new projects):
uv add --dev ruff

Dat is alles. Geen plugin-ecosysteem, geen onderhandeling over peer dependencies.

De pyproject.toml-configuratie

[tool.ruff]
line-length = 88
target-version = "py311"

[tool.ruff.lint]
select = [
  "E",   # pycodestyle — style consistency
  "F",   # pyflakes — catches unused imports (the most common AI artifact)
  "I",   # isort — import ordering (AI frequently reorders imports incorrectly)
  "N",   # pep8-naming — naming conventions
  "UP",  # pyupgrade — flags deprecated APIs (AI trains on old Stack Overflow answers)
  "S",   # flake8-bandit — security rules: subprocess.shell=True, eval(), hardcoded creds
  "ANN", # type annotation enforcement (AI frequently omits return type annotations)
]
ignore = []  # intentional: do not add sweeping ignores in AI-assisted codebases

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

De F-regels verdienen zichzelf meteen terug. AI-code genereert import-statements voor packages die hij uiteindelijk niet gebruikt, en F401 (ongebruikte import) vangt ze allemaal op. De UP-regels vangen aanroepen van verouderde API-patronen op die AI heeft geleerd van pre-3.10 Python-antwoorden; UP006 en UP007 alleen al markeren tientallen onnodige type-checking patronen. De S-regels (Bandit) vangen de beveiligingsfouten op: hardgecodeerde strings die op credentials lijken (S105/S106), shell injectie via subprocess(shell=True) (S603/S607), zwakke cryptografiekeuzes (S324).

De ignore = [] is bewust. Elke uitzondering die je aan deze lijst toevoegt, is een klasse AI-fouten die je hebt besloten toe te staan.

Ruff draaien

ruff check .           # lint only — see what's wrong
ruff check --fix .     # auto-fix safe violations (imports, formatting)
ruff format .          # format the codebase (replaces Black)

--fix past Ruff's veilige fixes standaard toe, zoals het verwijderen van ongebruikte imports of het toepassen van eenvoudige formatting- en lint-correcties. Ruff heeft ook unsafe fixes, maar die vereisen een expliciete opt-in en zouden zorgvuldiger gereviewd moeten worden. Review alles wat Ruff niet veilig kan fixen handmatig.

ESLint v10 configureren voor TypeScript- en JavaScript AI-code

ESLint v10 flat config reporting no-explicit-any and max-lines-per-function violations in an AI-generated TypeScript file

ESLint v10 heeft het verouderde .eslintrc.*-configuratieformaat laten vallen. Alles is nu flat config in eslint.config.mjs. Als je een tutorial vindt die .eslintrc.json of .eslintrc.js gebruikt, richt die zich op ESLint v8 of v9, waar de syntaxis anders is. Gebruik wat hieronder staat.

De @typescript-eslint-regels in deze configuratie richten zich op de specifieke faalpatronen die telkens weer opduiken in AI-gegenereerde TypeScript: de any-nooduitgang, monolithische functies die moeilijk te reviewen zijn, en hardgecodeerde waarden die constanten zouden moeten zijn.

ESLint v10 installeren met TypeScript-ondersteuning

npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

# or with pnpm:
pnpm add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

ESLint v10.5.0 is actueel per juni 2026. De @typescript-eslint-packages moeten overeenkomen met je TypeScript-versie; check hun README voor de compatibiliteitsmatrix.

De eslint.config.mjs-configuratie

import tseslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';

export default [
  {
    files: ['**/*.ts', '**/*.tsx'],
    languageOptions: {
      parser: tsParser,
      parserOptions: { project: './tsconfig.json' },
    },
    plugins: { '@typescript-eslint': tseslint },
    rules: {
      // AI defaults to `any` to bypass the type system — this blocks it
      '@typescript-eslint/no-explicit-any': 'error',

      // AI generates variables it declares but never uses
      '@typescript-eslint/no-unused-vars': 'error',

      // AI writes functions that work but are too long to review safely
      'max-lines-per-function': ['error', { max: 50 }],

      // AI overloads function signatures; this forces decomposition
      'max-params': ['error', 2],

      // AI hardcodes values that should be named constants
      'no-magic-numbers': ['error', { ignore: [0, 1, -1] }],

      // AI writes files that are too large to reason about in a single review
      'max-lines': ['error', { max: 250 }],

      // AI leaves console.log debugging in production code
      'no-console': 'warn',
    },
  },
];

De max-lines-per-function: 50-regel is het meest agressieve in deze configuratie. Je loopt er voortdurend tegenaan bij de eerste run in een AI-ondersteunde codebase. Dat is de bedoeling. Je hoort ertegenaan te lopen. Functies die langer zijn dan 50 regels zijn het eerste wat onmogelijk te doorgronden wordt wanneer je AI-output op grote schaal reviewt.

De max-params: 2-regel dwingt decompositie af. AI-modellen leren van codebases waar functies met vijf argumenten normaal zijn; de regel duwt terug door de agent te verplichten een options-object te gebruiken, wat een beter ontwerp is en makkelijker te lezen.

ESLint draaien

npx eslint .                       # lint
npx eslint . --fix                 # auto-fix safe issues
npx eslint . --max-warnings 0      # CI mode — treats warnings as errors

Gebruik --max-warnings 0 in je CI-stap. Het promoveert no-console-waarschuwingen van "technisch genoteerd" naar "daadwerkelijk blokkerend."

Optioneel: strengere regels voor AI-gegenereerde bestanden

Als je team een bestandsnaamconventie gebruikt om AI-gegenereerde code te markeren (*.ai.ts, *-generated.ts, of vergelijkbaar), kun je strakkere regels specifiek op die bestanden toepassen:

// Add to eslint.config.mjs after the main config object
{
  files: ['**/*.ai.ts', '**/*.ai.tsx', '**/*-generated.ts'],
  rules: {
    'max-lines': ['error', { max: 100 }],  // tighter file ceiling
    'complexity': ['error', 5],             // McCabe complexity limit
  },
}

golangci-lint configureren voor Go AI-code

golangci-lint is de standaard multi-linter runner voor Go. Hij wordt geleverd met gosec, errcheck, staticcheck en 40+ andere linters, configureerbaar vanuit een enkel YAML-bestand. Voor AI-gegenereerde Go-code zijn de kritieke regels het controleren van foutretourwaarden en het detecteren van beveiligingspatronen: de twee faalcategorieën die AI-modellen het meest consistent missen in Go.

golangci-lint installeren

# Official binary installer:
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b "$(go env GOPATH)/bin" v2.12.2

# or via Homebrew:
brew install golangci-lint

De .golangci.yml-configuratie

version: "2"

linters:
  enable:
    - gosec        # security: flags hardcoded creds (G101), file path injection (G304), weak crypto (G401)
    - unused       # flags unused vars and functions — a common AI artifact in Go
    - errcheck     # AI frequently ignores error returns — this blocks it
    - govet        # catches subtle correctness bugs AI introduces
    - staticcheck  # comprehensive static analysis
    - revive       # style: catches non-idiomatic Go patterns AI writes
    - misspell     # AI occasionally misspells in comments and string literals

  settings:
    gosec:
      severity: medium
      confidence: medium
    errcheck:
      check-type-assertions: true  # check `val, ok := x.(Type)` patterns
      check-blank: true            # catch `_ = someErr` error suppression

run:
  timeout: 3m
  issues-exit-code: 1

Go's foutafhandelingspatroon is van nature expliciet: elke functie die kan falen, retourneert een foutwaarde. AI-modellen begrijpen dit maar onderschatten het; ze laten foutcontroles weg in niet-kritieke codepaden. errcheck maakt van die omissie een lint-fout.

gosec is de beveiligingslinter. Voor AI-code vangt hij de patronen op die AI heeft opgepikt uit pre-2020 Go-tutorials: onveilige generatie van willekeurige getallen (G404), onveilige hashfuncties (G401), problemen met bestandsrechten (G306). Dit zijn de fouten die je niet opmerkt bij een code review omdat ze er syntactisch normaal uitzien.

golangci-lint draaien

golangci-lint run ./...            # lint all packages
golangci-lint run --fix ./...      # auto-fix where possible

Hoe je de linter koppelt aan pre-commit hooks

Een pre-commit hook draait lint voor elke git commit en blokkeert de commit als lint faalt. Dit betekent dat de AI-agent geen code kan committen die jouw geconfigureerde regels niet haalt. Hij moet eerst de overtredingen fixen.

Lefthook is de aanbevolen optie. Het is cross-platform, snel, en heeft een configuratiepatroon dat specifiek werkt met afdwinging voor AI-agents (behandeld in de volgende sectie).

Lefthook

npm install --save-dev lefthook
npx lefthook install

lefthook.yml:

pre-commit:
  parallel: true
  commands:
    lint-python:
      glob: "*.py"
      run: ruff check {staged_files} --fix
    lint-js-ts:
      glob: "*.{js,ts,tsx}"
      run: npx eslint {staged_files} --fix
    lint-go:
      glob: "*.go"
      run: golangci-lint run {staged_files}
  fail_text: |
    Lint failed. For AI Agents: fix all lint violations before committing.
    Do not use --no-verify to bypass this gate.

Het fail_text-bericht wordt gelezen door AI-agents wanneer een commit faalt. Het patroon is gedocumenteerd in Liam Bigelow's uiteenzetting over Lefthook lint-afdwinging voor Claude Code. Dit alleen houdt een vastberaden agent niet tegen, maar het geeft hem de juiste volgende instructie ("fix lint-overtredingen") in plaats van het aan hem over te laten een omweg af te leiden.

Alternatief: pre-commit (setups met alleen Python)

Als je op een stack met alleen Python zit en de voorkeur geeft aan het pre-commit framework:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.17
    hooks:
      - id: ruff-check
        args: [--fix]
      - id: ruff-format

Cursor: inline linting via de afterFileEdit hook

Als je Cursor gebruikt, kun je lint direct activeren wanneer de AI een bestand aanpast, voordat hij zelfs maar een commit probeert. Maak .cursor/hooks.json aan in de root van je project:

{
  "hooks": {
    "afterFileEdit": [
      {
        "match": "*.py",
        "run": "ruff check {file} --fix"
      },
      {
        "match": "*.{ts,tsx,js}",
        "run": "npx eslint {file} --fix"
      },
      {
        "match": "*.go",
        "run": "golangci-lint run {file}"
      }
    ]
  }
}

Dit wordt elke keer geactiveerd wanneer Cursor's AI een bestand aanpast. De agent krijgt lint-feedback inline, voordat hij de taak als voltooid beschouwt, zodat de meeste overtredingen worden gefixt voordat ze de pre-commit gate bereiken.

Hoe je de gate moeilijker te omzeilen maakt door AI-agents

Diagram of four enforcement layers (CLAUDE.md policy, Claude Code deny rule, PreToolUse hook, and CI backstop) blocking an AI agent from skipping the lint gate with --no-verify

Vier afdwingingslagen maken het veel moeilijker voor een AI-agent om de pre-commit gate te omzeilen met --no-verify. Elke laag richt zich op een ander aanvalsoppervlak: beleid in CLAUDE.md, een Claude Code deny-regel, een PreToolUse hook, en een CI-vangnet. Behandel ze als een gelaagde setup, niet als vier onafhankelijke garanties.

AI-agents geven soms --no-verify door aan git commit om pre-commit hooks over te slaan wanneer lint faalt en de agent heeft besloten dat de fouten "niets met zijn wijzigingen te maken hebben." De beslissing is niet altijd verkeerd, maar je zou de agent die niet eenzijdig moeten laten nemen. Het hele punt van de lint-gate is dat een mens het beleid heeft vastgesteld; de taak van de agent is om eraan te voldoen, niet om eromheen te werken.

Hier is elke laag en het aanvalsoppervlak dat hij dekt.

Laag 1: documenteer het beleid in CLAUDE.md

## Linting Policy

NEVER use `git commit --no-verify`. All commits must pass pre-commit hooks.
Pre-commit hooks run lint. Fix lint violations before committing. Do not treat
lint failures as unrelated to your changes — they may not be, and you don't get
to decide that.

Claude Code leest CLAUDE.md bij het begin van een sessie. Dit is geen afdwinging; de agent kan de omzeiling nog steeds proberen. Maar het haalt de "ik wist het niet"-route weg en stelt een duidelijk beleid vast dat de agent actief moet kiezen te overtreden, wat hij minder snel zal doen dan een omweg afleiden uit stilte.

Laag 2: Claude Code deny-regel

Voeg toe aan .claude/settings.json:

{
  "permissions": {
    "deny": [
      "Bash(git commit --no-verify*)"
    ]
  }
}

Dit blokkeert de expliciete aanroep. Eén beperking: de deny-regel gebruikt prefix-matching, dus hij vangt --no-verify alleen op direct na commit. Een voldoende creatieve agent zou de aanroep anders kunnen structureren. Vertrouw hier niet alleen op.

Laag 3: PreToolUse hook

Installeer het block-no-verify-package:

npm install --save-dev block-no-verify

Configureer het vervolgens als een PreToolUse hook in de instellingen van Claude Code. Dit wordt geactiveerd voor elke tool-aanroep en onderzoekt de argumenten op --no-verify over zes git-subcommando's, niet alleen commit. Het sluit af met een niet-nulwaarde om de aanroep te blokkeren voordat hij wordt uitgevoerd.

Het handboek op pydevtools.com is er duidelijk over: "de hook-laag is de enige die de regel betrouwbaar afdwingt." Gebruik dit met de andere lagen; behandel geen enkele lokale hook als de volledige garantie.

Laag 4: CI-vangnet

CI draait op de server, waar de agent geen shell heeft om flags in door te geven:

# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Lint Python
        run: |
          pip install ruff
          ruff check . --output-format github

      - name: Lint JS/TS
        run: npx eslint . --max-warnings 0

      - name: Lint Go
        uses: golangci/golangci-lint-action@v9
        with:
          version: v2.12.2

AI-agents kunnen --no-verify niet doorgeven aan CI. GitHub Actions draait onafhankelijk van wat de agent lokaal ook deed. Als een commit op de een of andere manier doorkwam met falende lint, vangt CI het op voordat het wordt gemerged.

Dit is het laatste vangnet.

Bonus: ESLint MCP-server

Als je Claude Code gebruikt, is er een proactieve laag die vermindert hoe vaak je überhaupt tegen de gate aanloopt.

De ESLint MCP-server (@eslint/mcp) integreert ESLint rechtstreeks in de tool-loop van de agent. De agent kan ESLint tijdens de taak bevragen, voordat hij een commit probeert. Installeer het globaal:

npm install -g @eslint/mcp@latest

Voeg toe aan .claude/settings.json:

{
  "mcpServers": {
    "eslint": {
      "command": "npx",
      "args": ["@eslint/mcp@latest"]
    }
  }
}

Met dit geconfigureerd kan de agent ESLint tijdens de taak bevragen. De agent krijgt lint-feedback inline, en sommige overtredingen kunnen worden gecorrigeerd voordat ze de hook bereiken. Dit vervangt de gate niet, het vermindert ruis op de gate.

Veelgestelde vragen

Moet ik alle drie de linters configureren als ik maar in één taal werk?

Nee. Stel de linter in voor je primaire taal en de afdwingingslaag. Als je stack alleen TypeScript is, configureer dan ESLint en sla Ruff en golangci-lint over. De sectie over het voorkomen van agent-omzeiling is van toepassing ongeacht welke taal je lint.

Breken deze configuraties mijn bestaande codebase bij de eerste run?

Vrijwel zeker, en met opzet. Draai ruff check --fix . of npx eslint . --fix om eerst de veilige overtredingen automatisch te corrigeren. Wat overblijft na de auto-fix is de handmatige reviewlijst: no-explicit-any casts, functies die langer zijn dan 50 regels, ontbrekende foutafhandeling. Werk die geleidelijk weg. Voeg geen ignore-regels toe om er niet mee te hoeven omgaan.

Is Biome op dit punt een vervanging voor ESLint?

Biome v2.5.0 (uitgebracht in juni 2026) is concurrerend op formatting en basis lint-regels. Het is sneller dan ESLint en heeft nul configuratieoverhead als je het installeert via bun x ultracite@latest init. Voor teams die één tool willen en niet de volledige diepte van de @typescript-eslint-regels nodig hebben, is Biome een redelijke keuze. Voor de AI-specifieke regels in deze gids (max-params, no-magic-numbers, max-lines-per-function met op AI gerichte drempels) heeft ESLint met @typescript-eslint nog steeds meer dekking. Je kunt beide draaien: Biome voor formatting en basis lint, ESLint voor de AI-specifieke regels.

Wat als mijn AI-agent telkens dezelfde lint-overtredingen blijft regenereren?

De agent werkt om de regel heen in plaats van het onderliggende probleem te fixen. Voor no-explicit-any betekent dit een type-assertie toevoegen in plaats van het daadwerkelijke type definiëren. Voor max-lines-per-function betekent het een helperfunctie extraheren die niets nuttigs doet maar de regelteller onder de drempel krijgt. Geen van beide oplossingen komt door de code review. De lint-regel ving het symptoom op; de grondoorzaak is de prompt van de agent. Verfijn de prompt om de typebeperking of de verwachte decompositie te specificeren; de agent volgt expliciete structuuraanwijzingen betrouwbaarder dan impliciete regels. Als je een complexere agent-setup draait, houdt het werk beperken tot een toegewijde subagent met de beperkingen ingebakken in zijn instructies doorgaans beter stand dan een enkele brede prompt.

Vervangt de ESLint MCP-server de pre-commit gate?

Nee. Het vermindert hoe vaak de agent code genereert die de gate niet haalt. De gate draait nog steeds bij elke commit. Inline controle door de MCP-server en afdwinging door de pre-commit hook zijn complementair, dus verwijder geen van beide.

Share

Meer van de blog

Blijf lezen.

Klaar om uit te rollen? Vanaf $2,48/mnd.

Onafhankelijke cloud, sinds 2008. AMD EPYC, NVMe, 40 Gbps. 14 dagen niet-goed-geld-terug.