AI 에이전트는 컴파일되는 코드를 만들어낼 수 있습니다.
하지만 그것만으로는 원하는 기준에 미치지 못합니다. 기준은 한 번도 쓰지 않는 것을 import하지 않고, 타입 시스템을 any 캐스팅으로 빠져나가지 않으며, 에러 반환값을 무시하지 않고, gosec가 리뷰 전에 잡아내야 할 자격 증명을 하드코딩하지 않는 코드입니다.
AI 모델은 오래된 Stack Overflow 답변으로 학습하고 거기서 배운 패턴을 그대로 내놓습니다. 여기에는 더 이상 사용되지 않는 API, 누락된 타입 어노테이션, 기술적으로는 맞지만 안전하게 리뷰하기에는 너무 큰 함수가 포함됩니다. 루프 안에 린터가 필요합니다. 제안이 아니라 게이트로서 말입니다.
이 가이드는 세 가지 생태계의 설정을 다룹니다 (Ruff를 쓰는 Python, ESLint v10 flat config를 쓰는 TypeScript/JavaScript, golangci-lint를 쓰는 Go). 모두 AI가 만들어내는 실패 패턴에 맞게 튜닝된 규칙입니다. 그런 다음 게이트를 훨씬 더 우회하기 어렵게 만드는 방법을 다룹니다. 에이전트가 --no-verify로 로컬 훅을 그냥 건너뛰더라도 또 다른 계층이 이를 잡아내도록 말입니다.
이 설정은 계층적입니다. IDE 수준 린팅은 에이전트가 코드를 작성하는 동안 인라인으로 문제를 잡아내고, pre-commit 훅은 커밋 시도까지 도달한 것을 잡아내며, CI는 로컬을 통과한 것을 잡아냅니다. 각 계층은 독립적이며, 자신의 스택에 해당하는 언어 섹션만 골라 쓸 수 있습니다. 어떤 언어를 린팅하든 시행 계층은 동일하게 작동합니다.
요약
- Ruff (Python), ESLint v10 (TS/JS), golangci-lint (Go)는 각각 AI의 가장 흔한 실패를 잡아내는 구체적인 규칙을 가지고 있습니다
- 아래 설정에는 주석이 달려 있으며, 모든 규칙은 그 자리에 있을 이유가 있습니다
- Lefthook가 pre-commit 게이트를 처리하고, Cursor의 afterFileEdit 훅이 린트를 인라인으로 실행합니다
- 네 개의 시행 계층은 AI 에이전트가 --no-verify로 게이트를 건너뛰는 것을 훨씬 더 어렵게 만듭니다
- CI는 최종 안전장치입니다. 에이전트는 GitHub Actions에 --no-verify를 전달할 수 없습니다
Python AI 코드를 위한 Ruff 설정 방법
Ruff는 AI 보조 코드베이스에 적합한 Python 린터입니다. 파일을 저장할 때마다 아무것도 막지 않고 실행할 만큼 빠르고, 스타일과 실제 로직 오류를 모두 다루며, (Black을 대체하는) 포매터 동작을 같은 바이너리에 담고 있습니다. 아래 규칙은 AI 모델이 Python에서 가장 자주 만들어내는 특정 실패 패턴을 겨냥합니다.
Ruff 설치
pip install ruff
# or via uv (faster for new projects):
uv add --dev ruff
이게 전부입니다. 플러그인 생태계도, 피어 의존성 조율도 없습니다.
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 규칙은 즉시 본전을 뽑습니다. AI 코드는 결국 쓰지 않는 패키지의 import 구문을 생성하는데, F401 (사용하지 않는 import)이 그 모든 것을 잡아냅니다. UP 규칙은 AI가 3.10 이전 Python 답변에서 배운, 더 이상 사용되지 않는 API 패턴 호출을 잡아냅니다. UP006과 UP007만으로도 수십 개의 불필요한 타입 체크 패턴을 표시합니다. S (Bandit) 규칙은 보안 실패를 잡아냅니다. 자격 증명처럼 보이는 하드코딩된 문자열 (S105/S106), subprocess(shell=True)를 통한 셸 인젝션 (S603/S607), 취약한 암호화 선택 (S324) 등입니다.
ignore = []은 의도적입니다. 이 목록에 추가하는 예외 하나하나는 당신이 허용하기로 결정한 AI 실패의 한 부류입니다.
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는 사용하지 않는 import 제거나 간단한 포매팅 및 린트 수정처럼 Ruff의 안전한 수정을 기본적으로 적용합니다. Ruff에는 안전하지 않은 수정도 있지만, 그것은 명시적인 opt-in이 필요하며 더 신중하게 검토해야 합니다. Ruff가 안전하게 고칠 수 없는 것은 무엇이든 수동으로 검토하세요.
TypeScript와 JavaScript AI 코드를 위한 ESLint v10 설정 방법
ESLint v10은 레거시 .eslintrc.* 설정 형식을 폐기했습니다. 이제 모든 것은 eslint.config.mjs의 flat config입니다. .eslintrc.json이나 .eslintrc.js를 사용하는 튜토리얼을 발견했다면, 그것은 문법이 다른 ESLint v8 또는 v9를 대상으로 한 것입니다. 아래 내용을 사용하세요.
이 설정의 @typescript-eslint 규칙은 AI 생성 TypeScript에서 반복적으로 나타나는 특정 실패 양상을 겨냥합니다. any 탈출구, 리뷰하기 어려운 거대한 단일 함수, 상수여야 할 하드코딩된 값 등입니다.
TypeScript 지원과 함께 ESLint v10 설치
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년 6월 기준 최신 버전입니다. @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 규칙은 이 설정에서 가장 공격적인 항목입니다. AI 보조 코드베이스에서는 첫 실행 때 끊임없이 이 규칙에 걸릴 것입니다. 바로 그게 핵심입니다. 걸려야 합니다. 50줄을 초과하는 함수는 AI 출력을 대량으로 리뷰할 때 도저히 파악할 수 없게 되는 첫 번째 대상입니다.
max-params: 2 규칙은 분해를 강제합니다. AI 모델은 인자 다섯 개짜리 함수가 정상인 코드베이스에서 학습합니다. 이 규칙은 에이전트가 options 객체를 사용하도록 요구하여 그것을 되돌려 놓습니다. 이는 더 나은 설계이자 더 읽기 쉬운 방식입니다.
ESLint 실행
npx eslint . # lint
npx eslint . --fix # auto-fix safe issues
npx eslint . --max-warnings 0 # CI mode — treats warnings as errors
CI 단계에서 --max-warnings 0을 사용하세요. 이것은 no-console 경고를 "기술적으로 기록됨"에서 "실제로 차단함"으로 격상시킵니다.
선택 사항: AI 생성 파일을 위한 더 엄격한 규칙
팀이 AI 생성 코드를 표시하기 위해 파일 명명 규칙(*.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
},
}
Go AI 코드를 위한 golangci-lint 설정 방법
golangci-lint는 Go의 표준 멀티 린터 러너입니다. gosec, errcheck, staticcheck를 비롯한 40개 이상의 린터를 하나의 YAML 파일로 설정할 수 있게 함께 제공됩니다. AI 생성 Go 코드에서 핵심 규칙은 에러 반환 검사와 보안 패턴 탐지입니다. 이 두 가지가 AI 모델이 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의 에러 처리 패턴은 설계상 명시적입니다. 실패할 수 있는 모든 함수는 error 값을 반환합니다. AI 모델은 이를 이해하지만 비중을 낮게 둡니다. 중요하지 않은 코드 경로에서는 에러 검사를 생략합니다. errcheck는 그러한 생략을 린트 실패로 만듭니다.
gosec는 보안 린터입니다. AI 코드에서는 AI가 2020년 이전 Go 튜토리얼에서 익힌 패턴을 잡아냅니다. 안전하지 않은 난수 생성 (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 전에 린트를 실행하고, 린트가 실패하면 커밋을 막습니다. 이는 AI 에이전트가 당신이 설정한 규칙을 통과하지 못하는 코드를 커밋할 수 없다는 뜻입니다. 먼저 위반 사항을 고쳐야 합니다.
Lefthook가 권장 옵션입니다. 크로스 플랫폼이고 빠르며, AI 에이전트 시행과 특히 잘 맞는 설정 패턴을 가지고 있습니다 (다음 섹션에서 다룹니다).
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 메시지는 커밋이 실패할 때 AI 에이전트가 읽습니다. 이 패턴은 다음 글에 문서화되어 있습니다 Lefthook 린트 시행에 관한 Liam Bigelow의 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를 사용한다면, AI가 파일을 수정할 때 커밋을 시도하기도 전에 즉시 린트를 트리거할 수 있습니다. 프로젝트 루트에 .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의 AI가 파일을 수정할 때마다 발동됩니다. 에이전트는 작업을 완료로 처리하기 전에 인라인으로 린트 피드백을 받으므로, 대부분의 위반은 pre-commit 게이트에 도달하기 전에 수정됩니다.
AI 에이전트가 게이트를 우회하기 더 어렵게 만드는 방법
네 개의 시행 계층은 AI 에이전트가 --no-verify로 pre-commit 게이트를 건너뛰는 것을 훨씬 더 어렵게 만듭니다. 각 계층은 서로 다른 공격 표면을 겨냥합니다. CLAUDE.md의 정책, Claude Code deny 규칙, PreToolUse 훅, 그리고 CI 안전장치입니다. 이를 네 개의 독립된 보증이 아니라 하나의 계층적 설정으로 다루세요.
AI 에이전트는 린트가 실패하고 있고 그 실패가 "자기 변경과 무관하다"고 판단했을 때, pre-commit 훅을 건너뛰려고 git commit에 --no-verify를 전달하기도 합니다. 그 판단이 항상 틀린 것은 아니지만, 에이전트가 일방적으로 그 결정을 내리도록 두어서는 안 됩니다. 린트 게이트의 핵심은 사람이 정책을 정한다는 것입니다. 에이전트의 일은 그것을 충족하는 것이지, 우회하는 것이 아닙니다.
각 계층과 그것이 다루는 공격 표면은 다음과 같습니다.
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계층: Claude Code Deny 규칙
.claude/settings.json에 추가하세요.
{
"permissions": {
"deny": [
"Bash(git commit --no-verify*)"
]
}
}
이것은 명시적 호출을 차단합니다. 한 가지 한계가 있습니다. deny 규칙은 접두사 매칭을 사용하므로 commit 바로 뒤에 오는 --no-verify만 잡아냅니다. 충분히 창의적인 에이전트라면 호출 구조를 다르게 짤 수 있습니다. 이것 하나에만 의존하지 마세요.
3계층: PreToolUse 훅
block-no-verify 패키지를 설치하세요.
npm install --save-dev block-no-verify
그런 다음 Claude Code 설정에서 PreToolUse 훅으로 구성하세요. 이것은 모든 툴 호출 전에 발동하여 commit뿐 아니라 여섯 개의 git 하위 명령에 걸쳐 인자에서 --no-verify를 검사합니다. 호출이 실행되기 전에 0이 아닌 값으로 종료하여 호출을 차단합니다.
이 핸드북은 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
AI 에이전트는 CI에 --no-verify를 전달할 수 없습니다. 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를 실행해 안전한 위반을 자동 수정하세요. 자동 수정 후 남는 것이 수동 검토 목록입니다. no-explicit-any 캐스트, 50줄을 초과하는 함수, 누락된 에러 처리 등입니다. 그것들을 점진적으로 처리하세요. 그것들을 다루지 않으려고 ignore 규칙을 추가하지 마세요.
이 시점에서 Biome가 ESLint를 대체하나요?
Biome v2.5.0 (2026년 6월 출시)은 포매팅과 기본 린트 규칙에서 경쟁력이 있습니다. ESLint보다 빠르고, bun x ultracite@latest init으로 설치하면 설정 부담이 전혀 없습니다. 단일 도구를 원하고 @typescript-eslint의 완전한 규칙 깊이가 필요 없는 팀에게 Biome는 합리적인 선택입니다. 이 가이드의 AI 특화 규칙(max-params, no-magic-numbers, AI를 겨냥한 임계값을 가진 max-lines-per-function)에 대해서는 @typescript-eslint를 쓰는 ESLint가 여전히 더 폭넓게 다룹니다. 둘 다 실행할 수 있습니다. 포매팅과 기본 린트는 Biome로, AI 특화 규칙은 ESLint로 하면 됩니다.
AI 에이전트가 같은 린트 위반을 계속 재생성하면 어떻게 하나요?
에이전트가 근본적인 문제를 고치기보다 규칙을 우회하고 있는 것입니다. no-explicit-any의 경우, 실제 타입을 정의하는 대신 타입 단언을 추가한다는 뜻입니다. max-lines-per-function의 경우, 아무 쓸모 없지만 줄 수를 임계값 아래로 떨어뜨리는 헬퍼 함수를 추출한다는 뜻입니다. 어느 해법도 코드 리뷰를 통과하지 못합니다. 린트 규칙은 증상을 잡아냈을 뿐, 근본 원인은 에이전트의 프롬프트입니다. 타입 제약이나 기대하는 분해 방식을 명시하도록 프롬프트를 다듬으세요. 에이전트는 암묵적 규칙보다 명시적 구조 지침을 더 안정적으로 따릅니다. 더 복잡한 에이전트 설정을 운영한다면, 제약을 지침에 미리 박아둔 전용 서브에이전트로 작업 범위를 좁히는 편이 하나의 넓은 프롬프트보다 더 잘 유지되는 경향이 있습니다.
ESLint MCP 서버가 pre-commit 게이트를 대체하나요?
아닙니다. 에이전트가 게이트를 통과하지 못하는 코드를 생성하는 빈도를 줄일 뿐입니다. 게이트는 여전히 모든 커밋에서 실행됩니다. MCP 서버의 인라인 검사와 pre-commit 훅 시행은 상호 보완적이므로, 둘 중 어느 것도 제거하지 마세요.