ИИ-агенты умеют генерировать код, который компилируется.
Но это всё ещё не та планка, которая вам нужна. Планка — это код, который не импортирует то, что никогда не используется, не выкручивается из системы типов через any-приведения, не игнорирует возвращаемые ошибки и не зашивает в себя учётные данные, которые gosec должен поймать до ревью.
ИИ-модели обучаются на старых ответах со Stack Overflow и выдают паттерны, которые там усвоили, включая устаревшие API, отсутствующие аннотации типов и функции, которые формально корректны, но слишком велики, чтобы безопасно их отревьюить. Вам нужен линтер в цикле. Не как рекомендация, а как барьер.
В этом руководстве рассматривается конфигурация для трёх экосистем (Python с Ruff, TypeScript/JavaScript с плоской конфигурацией ESLint v10 и Go с golangci-lint) с правилами, настроенными именно под те паттерны сбоев, которые привносит ИИ. Затем рассказывается, как сделать барьер гораздо труднее обходимым, чтобы агент не мог просто пропустить локальные хуки через --no-verify без того, чтобы его поймал другой слой.
Настройка многоуровневая: линтинг на уровне IDE ловит проблемы прямо по ходу того, как агент пишет, pre-commit-хуки ловят всё, что доходит до попытки коммита, а CI ловит всё, что прошло локально. Каждый слой независим, и вы можете выбрать те языковые разделы, которые применимы к вашему стеку. Слой принуждения работает одинаково независимо от того, какой язык вы линтите.
Коротко о главном
- Ruff (Python), ESLint v10 (TS/JS) и golangci-lint (Go) — у каждого есть конкретные правила, которые ловят самые частые сбои ИИ
- Приведённые ниже конфигурации снабжены комментариями; каждое правило здесь не просто так
- Lefthook отвечает за pre-commit-барьер; хук afterFileEdit в Cursor запускает линтинг прямо по ходу работы
- Четыре слоя принуждения делают для ИИ-агентов гораздо более трудным пропуск барьера через --no-verify
- CI — последний рубеж: агенты не могут передать --no-verify в GitHub Actions
Как настроить Ruff для Python-кода от ИИ
Ruff — правильный линтер Python для кодовых баз, написанных с помощью ИИ. Он достаточно быстр, чтобы запускаться при каждом сохранении файла, ничего не блокируя, охватывает и стиль, и реальные логические ошибки, а ещё в том же бинарнике поставляет поведение форматтера (заменяя Black). Приведённые ниже правила нацелены на конкретные паттерны сбоев, которые ИИ-модели чаще всего привносят в Python.
Установка Ruff
pip install ruff
# or via uv (faster for new projects):
uv add --dev ruff
Вот и всё. Никакой экосистемы плагинов, никаких переговоров о peer-зависимостях.
Конфигурация pyproject.toml
[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
Правила F окупаются сразу же. ИИ-код генерирует инструкции импорта для пакетов, которые в итоге не использует, и F401 (неиспользуемый импорт) ловит каждую из них. Правила UP ловят вызовы устаревших API-паттернов, которые ИИ усвоил из ответов по Python до версии 3.10; одни только UP006 и UP007 помечают десятки ненужных паттернов проверки типов. Правила S (Bandit) ловят сбои безопасности: зашитые строки, похожие на учётные данные (S105/S106), инъекцию через shell с помощью subprocess(shell=True) (S603/S607), слабый выбор криптографии (S324).
ignore = [] оставлено пустым намеренно. Каждое исключение, которое вы добавляете в этот список, — это класс сбоев ИИ, который вы решили допустить.
Запуск Ruff
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 по умолчанию применяет безопасные исправления Ruff, например удаление неиспользуемых импортов или применение простого форматирования и исправлений линтинга. У Ruff есть и небезопасные исправления, но они требуют явного согласия и должны проверяться внимательнее. Всё, что Ruff не может безопасно исправить, проверяйте вручную.
Как настроить ESLint v10 для TypeScript- и JavaScript-кода от ИИ
ESLint v10 отказался от устаревшего формата конфигурации .eslintrc.*. Теперь всё — это плоская конфигурация в eslint.config.mjs. Если вы найдёте руководство, использующее .eslintrc.json или .eslintrc.js, оно нацелено на ESLint v8 или v9, где синтаксис другой. Используйте то, что приведено ниже.
Правила @typescript-eslint в этой конфигурации нацелены на конкретные режимы сбоев, которые раз за разом возникают в сгенерированном ИИ TypeScript: лазейка any, монолитные функции, которые тяжело ревьюить, и зашитые значения, которые должны быть константами.
Установка ESLint v10 с поддержкой TypeScript
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 актуален по состоянию на июнь 2026 года. Пакеты @typescript-eslint должны соответствовать вашей версии TypeScript; сверьтесь с их README на предмет матрицы совместимости.
Конфигурация eslint.config.mjs
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',
},
},
];
Правило max-lines-per-function: 50 — самое агрессивное в этой конфигурации. При первом запуске на кодовой базе, написанной с помощью ИИ, вы будете натыкаться на него постоянно. В этом и смысл. Вы и должны на него натыкаться. Функции, превышающие 50 строк, — это первое, что становится невозможно осмыслить, когда вы ревьюите вывод ИИ в больших объёмах.
Правило max-params: 2 принуждает к декомпозиции. ИИ-модели учатся на кодовых базах, где функции с пятью аргументами — норма; правило сопротивляется этому, требуя от агента использовать объект параметров, что и есть лучший дизайн и читается легче.
Запуск ESLint
npx eslint . # lint
npx eslint . --fix # auto-fix safe issues
npx eslint . --max-warnings 0 # CI mode — treats warnings as errors
Используйте --max-warnings 0 в шаге CI. Это поднимает предупреждения no-console из разряда «формально отмечено» в «реально блокирует».
Опционально: более строгие правила для файлов, сгенерированных ИИ
Если ваша команда использует соглашение об именовании файлов для пометки кода, сгенерированного ИИ (*.ai.ts, *-generated.ts или подобное), вы можете применить к этим файлам более жёсткие правила специально:
// 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 для Go-кода от ИИ
golangci-lint — стандартный мультилинтер-раннер для Go. Он поставляется с gosec, errcheck, staticcheck и более чем 40 другими линтерами, настраиваемыми из одного YAML-файла. Для сгенерированного ИИ Go-кода критически важны правила проверки возвращаемых ошибок и обнаружения паттернов безопасности: две категории сбоев, которые ИИ-модели в Go пропускают наиболее стабильно.
Установка golangci-lint
# 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
Конфигурация .golangci.yml
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 по своей сути явный: каждая функция, которая может завершиться неудачей, возвращает значение ошибки. ИИ-модели это понимают, но недооценивают; они опускают проверки ошибок в некритичных участках кода. errcheck превращает это упущение в ошибку линтинга.
gosec — линтер безопасности. Для ИИ-кода он ловит паттерны, которые ИИ подхватил из руководств по Go до 2020 года: небезопасную генерацию случайных чисел (G404), небезопасные хеш-функции (G401), проблемы с правами доступа к файлам (G306). Это те ошибки, которые вы не ловите на код-ревью, потому что синтаксически они выглядят нормально.
Запуск golangci-lint
golangci-lint run ./... # lint all packages
golangci-lint run --fix ./... # auto-fix where possible
Как встроить линтер в pre-commit-хуки
Pre-commit-хук запускает линтинг перед каждым git commit и блокирует коммит, если линтинг завершается неудачей. Это означает, что ИИ-агент не может закоммитить код, который не проходит ваши настроенные правила. Он обязан сначала исправить нарушения.
Lefthook — рекомендуемый вариант. Он кроссплатформенный, быстрый и имеет паттерн конфигурации, который работает именно с принуждением ИИ-агентов (рассматривается в следующем разделе).
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.
Сообщение fail_text читается ИИ-агентами, когда коммит завершается неудачей. Этот паттерн описан в разборе Liam Bigelow о принуждении линтинга через Lefthook для Claude Code. Одно это не остановит решительного агента, но даёт ему правильную следующую инструкцию («исправь нарушения линтинга»), вместо того чтобы оставить его додумывать обходной путь.
Альтернатива: pre-commit (только для Python-стека)
Если у вас стек только на Python и вы предпочитаете фреймворк pre-commit:
# .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: линтинг по ходу работы через хук afterFileEdit
Если вы используете Cursor, вы можете запускать линтинг сразу же, как только ИИ изменяет файл, ещё до того, как он попытается сделать коммит. Создайте .cursor/hooks.json в корне вашего проекта:
{
"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}"
}
]
}
}
Это срабатывает каждый раз, когда ИИ Cursor изменяет файл. Агент получает обратную связь линтинга по ходу работы, прежде чем сочтёт задачу выполненной, поэтому большинство нарушений исправляется до того, как они дойдут до pre-commit-барьера.
Как сделать барьер труднее обходимым для ИИ-агентов
Четыре слоя принуждения делают для ИИ-агента гораздо более трудным пропуск pre-commit-барьера через --no-verify. Каждый нацелен на свою поверхность атаки: политика в CLAUDE.md, deny-правило Claude Code, хук PreToolUse и резервный рубеж CI. Относитесь к ним как к многослойной системе, а не как к четырём независимым гарантиям.
ИИ-агенты иногда передают --no-verify в git commit, чтобы пропустить pre-commit-хуки, когда линтинг падает, а агент решил, что сбои «не связаны с его изменениями». Это решение не всегда неверно, но не стоит позволять агенту принимать его в одностороннем порядке. Весь смысл барьера линтинга в том, что политику задал человек; задача агента — удовлетворить её, а не обойти.
Вот каждый слой и поверхность атаки, которую он покрывает.
Слой 1: задокументируйте политику в 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 читает CLAUDE.md в начале сессии. Это не принуждение; агент по-прежнему может попытаться обойти барьер. Но это убирает путь «я не знал» и задаёт чёткую политику, которую агент должен активно решить нарушить, а на это он идёт реже, чем на додумывание обходного пути из тишины.
Слой 2: deny-правило Claude Code
Добавьте в .claude/settings.json:
{
"permissions": {
"deny": [
"Bash(git commit --no-verify*)"
]
}
}
Это блокирует явный вызов. Одно ограничение: deny-правило использует сопоставление по префиксу, поэтому оно ловит --no-verify только непосредственно после commit. Достаточно изобретательный агент мог бы выстроить вызов иначе. Не полагайтесь только на это.
Слой 3: хук PreToolUse
Установите пакет block-no-verify:
npm install --save-dev block-no-verify
Затем настройте его как хук PreToolUse в настройках Claude Code. Он срабатывает перед каждым вызовом инструмента и проверяет аргументы на наличие --no-verify в шести git-подкомандах, а не только в commit. Он завершается с ненулевым кодом, блокируя вызов до его выполнения.
Руководство по адресу pydevtools.com высказывается об этом прямо: «слой хука — единственный, который надёжно принуждает к соблюдению правила». Используйте его вместе с остальными слоями; не воспринимайте ни один локальный хук как полную гарантию.
Слой 4: резервный рубеж CI
CI выполняется на сервере, где у агента нет шелла, в который можно передать флаги:
# .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
ИИ-агенты не могут передать --no-verify в CI. GitHub Actions выполняется независимо от того, что агент делал локально. Если коммит с падающим линтингом каким-то образом проскочил, CI поймает его до слияния.
Это последний рубеж.
Бонус: ESLint MCP-сервер
Если вы используете Claude Code, есть упреждающий слой, который снижает, как часто вы вообще упираетесь в барьер.
ESLint MCP-сервер (@eslint/mcp) интегрирует ESLint прямо в цикл инструментов агента. Агент может опрашивать ESLint во время задачи, до того как попытается сделать коммит. Установите его глобально:
npm install -g @eslint/mcp@latest
Добавьте в .claude/settings.json:
{
"mcpServers": {
"eslint": {
"command": "npx",
"args": ["@eslint/mcp@latest"]
}
}
}
С этой настройкой агент может опрашивать ESLint во время задачи. Агент получает обратную связь линтинга по ходу работы, и некоторые нарушения можно исправить до того, как они дойдут до хука. Это не заменяет барьер, а снижает шум на нём.
Часто задаваемые вопросы
Нужно ли мне настраивать все три линтера, если я работаю только на одном языке?
Нет. Настройте линтер для своего основного языка и слой принуждения. Если ваш стек только на TypeScript, настройте ESLint и пропустите Ruff и golangci-lint. Раздел о предотвращении обхода агентом применим независимо от того, какой язык вы линтите.
Сломают ли эти конфигурации мою существующую кодовую базу при первом запуске?
Почти наверняка, и намеренно. Сначала запустите ruff check --fix . или npx eslint . --fix, чтобы автоматически исправить безопасные нарушения. То, что останется после автоисправления, — это список для ручного ревью: any-приведения no-explicit-any, функции, превышающие 50 строк, отсутствующая обработка ошибок. Прорабатывайте их постепенно. Не добавляйте правила в ignore, чтобы не разбираться с ними.
Является ли Biome заменой ESLint на данный момент?
Biome v2.5.0 (выпущен в июне 2026 года) конкурентоспособен в форматировании и базовых правилах линтинга. Он быстрее ESLint и не требует накладных расходов на конфигурацию, если установить его через bun x ultracite@latest init. Для команд, которым нужен единый инструмент и не нужна вся глубина правил @typescript-eslint, Biome — разумный выбор. Для специфичных для ИИ правил из этого руководства (max-params, no-magic-numbers, max-lines-per-function с порогами, нацеленными на ИИ) ESLint с @typescript-eslint всё ещё охватывает больше. Можно использовать оба: Biome для форматирования и базового линтинга, ESLint для специфичных для ИИ правил.
Что делать, если мой ИИ-агент снова и снова воспроизводит одни и те же нарушения линтинга?
Агент обходит правило, вместо того чтобы исправить лежащую в основе проблему. Для no-explicit-any это означает добавление приведения типа вместо определения настоящего типа. Для max-lines-per-function это означает выделение вспомогательной функции, которая не делает ничего полезного, но подгоняет количество строк под порог. Ни одно из этих решений не проходит код-ревью. Правило линтинга поймало симптом; первопричина — это промпт агента. Уточните промпт, указав ограничение по типу или ожидаемую декомпозицию; агент следует явным указаниям по структуре надёжнее, чем неявным правилам. Если у вас более сложная конфигурация агентов, ограничение работы выделенным субагентом, в чьи инструкции заложены ограничения, обычно держится лучше, чем один широкий промпт.
Заменяет ли ESLint MCP-сервер pre-commit-барьер?
Нет. Он снижает, как часто агент генерирует код, который не проходит барьер. Барьер по-прежнему срабатывает на каждом коммите. Проверка по ходу работы MCP-сервером и принуждение pre-commit-хуком дополняют друг друга, поэтому не убирайте ни то, ни другое.