ESLint as an AI Guardrail
ESLint rules aren't just for humans — they're guardrails for AI agents. The right rules catch AI-generated code that works but violates the patterns your codebase needs.
There's a specific failure mode with AI-generated code: it works, but it's subtly wrong. Not buggy — just inconsistent. Files that are 600 lines when they should be 100. Functions with 12 possible paths when the limit should be 3. Import ordering that changes every file. Patterns from 2018 mixed with patterns from 2024.
ESLint catches this automatically. Not by being smart about your code — by being dumb and relentlessly consistent about rules you've defined.
The combination that works: strict ESLint + hooks that run the linter automatically = the AI's output gets checked every time it writes code.
The Rules That Matter Most
Complexity Limits
Cyclomatic complexity measures how many paths exist through a function. A function with no branching has complexity 1. Each if, else, &&, ||, ternary, and loop adds 1.
{
"complexity": ["error", { "max": 5 }]
}Why this works against AI: AI-generated code handles edge cases inline. The result is functions that are technically correct but impossible to read — and impossible to test thoroughly. A complexity limit of 5 forces the AI to break those into smaller, named pieces.
File and Function Size Limits
{
"max-lines": ["error", {
"max": 200,
"skipBlankLines": true,
"skipComments": true
}],
"max-lines-per-function": ["error", {
"max": 50,
"skipBlankLines": true
}]
}Why this works: smaller files are better for AI context (Cursor only reads the first 250 lines of any file during codebase indexing). A file size limit stops the AI from piling everything into one massive file. And smaller files are easier for the AI to understand and reason about on the next session.
This is self-reinforcing: small files → better AI context → better AI output → still small files.
No Nested Ternaries
{
"no-nested-ternary": "error"
}AI loves nested ternaries. They're compact and technically valid. Three levels deep, they become completely unreadable — and the AI will cheerfully write them.
No Unused Variables
{
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}]
}AI often generates variables it never uses. Dead code clutters context for future sessions.
Consistent Import Order
{
"import/order": ["error", {
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
"newlines-between": "always",
"alphabetize": { "order": "asc" }
}]
}Without this, AI-generated files have imports in whatever order the AI happened to write them. Consistent ordering makes diffs cleaner and makes it easier to check whether a given import is already present.
The Unicorn and Perfectionist Plugins
Two plugins particularly useful for AI-heavy codebases:
eslint-plugin-unicorn
Unicorn enforces modern JavaScript/TypeScript patterns. The value for AI workflows: it prevents the AI from mixing eras.
AI training data spans years. Without constraints, it'll generate code that mixes Array.prototype.forEach with Array.from() with for...of, patterns from 2016 and 2024 in the same file. Unicorn enforces a consistent baseline.
{
"unicorn/prefer-module": "error",
"unicorn/prefer-node-protocol": "error",
"unicorn/no-array-for-each": "error",
"unicorn/prefer-array-flat-map": "error",
"unicorn/filename-case": ["error", { "case": "kebabCase" }]
}The filename rule is particularly good: it stops the AI from creating files named UserCard.tsx, user-card.tsx, and userCard.tsx in the same project.
eslint-plugin-perfectionist
Perfectionist handles ordering: object keys, named exports, type properties. Boring — but it makes diffs far more readable.
{
"perfectionist/sort-imports": "error",
"perfectionist/sort-named-exports": "error",
"perfectionist/sort-object-types": "error"
}When the AI generates a type with 10 properties, they'll be alphabetically ordered. When it generates a barrel file with 20 exports, same. Makes every AI-generated file predictable.
Wiring ESLint Into the AI Loop
Rules only help if they run. Two ways to ensure they always do:
Option 1: Claude Code Hooks (PostToolUse)
In your Claude Code settings or .claude/settings.json:
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit",
"hooks": [{
"type": "command",
"command": "npx eslint --fix \"${file}\" 2>&1 | head -30"
}]
}]
}
}Every time Claude Code writes or edits a file, ESLint runs and auto-fixes what it can. Remaining errors appear in the terminal. Claude Code sees them and fixes them on the next pass.
This is the automated feedback loop. The AI doesn't need to remember to check linting — the hook just runs it.
Option 2: Cursor Rules
Add to your .cursorrules file:
After writing any code, run ESLint on the file and fix any errors
before considering the task complete. If auto-fix doesn't resolve all
issues, fix the remaining ones manually.This is instruction-based rather than automatic — softer, but it still nudges the AI toward checking its own output.
A Practical ESLint Config for AI-Assisted Projects
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended-type-checked",
"plugin:unicorn/recommended"
],
"plugins": ["perfectionist", "import"],
"rules": {
"complexity": ["error", { "max": 5 }],
"max-lines": ["error", { "max": 200, "skipBlankLines": true }],
"max-lines-per-function": ["error", { "max": 50, "skipBlankLines": true }],
"no-nested-ternary": "error",
"no-var": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"unicorn/filename-case": ["error", { "case": "kebabCase" }],
"unicorn/prevent-abbreviations": "off",
"perfectionist/sort-imports": "error",
"perfectionist/sort-named-exports": "error",
"import/no-cycle": "error",
"import/no-default-export": "error"
}
}Adjust max-lines and complexity limits to match your codebase. Start stricter than you think you need — you can always relax a rule, and starting strict catches more AI slop early.
The Compound Effect
Here's what the full cycle looks like in practice:
- AI generates code
- ESLint auto-fixes what it can (import ordering, minor style)
- ESLint flags what it can't fix (complexity too high, function too long)
- The AI sees the linting errors in the terminal output (via hooks) and fixes them
- The result fits your patterns — not because the AI is smart, but because the linter is dumb and fast
You've created a tighter feedback loop than code review. The AI gets corrections in seconds. Errors that would have appeared in review are caught before you ever look at the output.
The engineers getting the most leverage from AI coding tools aren't just writing better prompts — they're building systems that catch AI mistakes automatically. ESLint is the easiest place to start.
Keep reading
Enjoyed this? Get more like it.
Deep dives on system design, React, web development, and personal finance — straight to your inbox. Free, always.