Chuyển đến nội dung chính
Giảm 50% tất cả các gói, có thời hạn. Khởi điểm từ $2.48/mo
14 min left
Công cụ lập trình và DevOps

Cách thiết lập linter cho code do AI tạo ra

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

AI agent có thể tạo ra code biên dịch được.

Đó vẫn chưa phải là tiêu chuẩn bạn muốn. Tiêu chuẩn là code không import những thứ không bao giờ dùng đến, không dùng any-cast để lách khỏi hệ thống kiểu, không bỏ qua các giá trị trả về lỗi, và không hardcode thông tin xác thực mà gosec lẽ ra phải bắt được trước khi review.

Các mô hình AI được huấn luyện trên những câu trả lời cũ trên Stack Overflow và đưa ra chính những mẫu code đã học được ở đó, bao gồm cả API đã lỗi thời, thiếu chú thích kiểu, và những hàm về mặt kỹ thuật là đúng nhưng quá lớn để review an toàn. Bạn cần một linter nằm trong vòng lặp. Không phải như một gợi ý, mà như một cổng chặn.

Hướng dẫn này bao gồm cấu hình cho ba hệ sinh thái (Python với Ruff, TypeScript/JavaScript với cấu hình flat của ESLint v10, và Go với golangci-lint) với các quy tắc được tinh chỉnh riêng cho những mẫu lỗi mà AI gây ra. Sau đó, nó trình bày cách làm cho cổng chặn khó vượt qua hơn nhiều, để agent không thể chỉ đơn giản bỏ qua các hook cục bộ bằng --no-verify mà không bị một lớp khác bắt lại.

Thiết lập được phân tầng: linting ở cấp độ IDE bắt lỗi ngay trong dòng khi agent viết, các hook pre-commit bắt mọi thứ chạm đến lần thử commit, và CI bắt mọi thứ lọt qua được ở cục bộ. Mỗi lớp là độc lập, và bạn có thể chọn những phần ngôn ngữ phù hợp với stack của mình. Lớp thực thi hoạt động giống nhau bất kể bạn lint ngôn ngữ nào.

TL;DR

  • Ruff (Python), ESLint v10 (TS/JS) và golangci-lint (Go) đều có những quy tắc cụ thể bắt được các lỗi phổ biến nhất của AI
  • Các cấu hình bên dưới đều có chú thích; mỗi quy tắc đều có lý do tồn tại
  • Lefthook xử lý cổng pre-commit; hook afterFileEdit của Cursor chạy lint ngay trong dòng
  • Bốn lớp thực thi khiến AI agent khó bỏ qua cổng chặn bằng --no-verify hơn nhiều
  • CI là chốt chặn cuối cùng: agent không thể truyền --no-verify cho GitHub Actions

Cách cấu hình Ruff cho code AI viết bằng Python

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

Ruff là linter Python phù hợp cho các codebase có AI hỗ trợ. Nó đủ nhanh để chạy mỗi khi lưu file mà không chặn bất cứ thứ gì, nó bao quát cả phong cách lẫn các lỗi logic thực sự, và nó tích hợp luôn cả tính năng định dạng (thay thế Black) trong cùng một file nhị phân. Các quy tắc bên dưới nhắm vào những mẫu lỗi cụ thể mà các mô hình AI hay gây ra nhất trong Python.

Cài đặt Ruff

pip install ruff

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

Vậy là xong. Không có hệ sinh thái plugin, không phải đàm phán peer dependency.

Cấu hình 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

Các quy tắc F mang lại giá trị ngay lập tức. Code AI tạo ra các câu lệnh import cho những package mà cuối cùng nó không dùng đến, và F401 (import không sử dụng) bắt được từng cái một. Các quy tắc UP bắt những lời gọi đến các mẫu API lỗi thời mà AI đã học từ các câu trả lời Python trước phiên bản 3.10; chỉ riêng UP006 và UP007 đã đánh dấu hàng chục mẫu kiểm tra kiểu không cần thiết. Các quy tắc S (Bandit) bắt các lỗi bảo mật: những chuỗi hardcode trông giống thông tin xác thực (S105/S106), shell injection qua subprocess(shell=True) (S603/S607), các lựa chọn mã hóa yếu (S324).

Việc đặt ignore = [] là có chủ đích. Mỗi ngoại lệ bạn thêm vào danh sách này là một loại lỗi AI mà bạn đã quyết định cho phép.

Chạy 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 áp dụng các bản sửa an toàn của Ruff theo mặc định, chẳng hạn như loại bỏ các import không sử dụng hoặc áp dụng các chỉnh sửa định dạng và lint đơn giản. Ruff cũng có các bản sửa không an toàn, nhưng những bản đó đòi hỏi phải bật rõ ràng và nên được xem xét cẩn thận hơn. Hãy tự tay xem xét bất cứ thứ gì Ruff không thể sửa an toàn.

Cách cấu hình ESLint v10 cho code AI viết bằng TypeScript và JavaScript

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

ESLint v10 đã loại bỏ định dạng cấu hình cũ .eslintrc.*. Bây giờ mọi thứ đều là cấu hình flat trong eslint.config.mjs. Nếu bạn tìm thấy một hướng dẫn dùng .eslintrc.json hoặc .eslintrc.js, nó đang nhắm vào ESLint v8 hoặc v9, nơi cú pháp khác đi. Hãy dùng những gì bên dưới.

Các quy tắc @typescript-eslint trong cấu hình này nhắm vào những kiểu lỗi cụ thể lặp đi lặp lại trong TypeScript do AI tạo ra: cửa thoát hiểm any, các hàm khổng lồ khó review, và các giá trị hardcode lẽ ra phải là hằng số.

Cài đặt ESLint v10 với hỗ trợ 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 là phiên bản hiện hành tính đến tháng 6 năm 2026. Các package @typescript-eslint nên khớp với phiên bản TypeScript của bạn; hãy kiểm tra README của chúng để biết ma trận tương thích.

Cấu hình 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',
    },
  },
];

Quy tắc max-lines-per-function: 50 là điều quyết liệt nhất trong cấu hình này. Bạn sẽ liên tục vướng phải nó trong lần chạy đầu tiên trên một codebase có AI hỗ trợ. Đó chính là mục đích. Bạn nên vướng phải nó. Những hàm vượt quá 50 dòng là thứ đầu tiên trở nên bất khả thi để hiểu khi bạn đang review code AI với khối lượng lớn.

Quy tắc max-params: 2 buộc phải phân rã. Các mô hình AI học từ những codebase mà các hàm năm tham số là chuyện bình thường; quy tắc này đẩy lùi điều đó bằng cách yêu cầu agent dùng một options object, vốn là thiết kế tốt hơn và dễ đọc hơn.

Chạy ESLint

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

Dùng --max-warnings 0 trong bước CI của bạn. Nó nâng các cảnh báo no-console từ mức "có ghi nhận về mặt kỹ thuật" lên mức "thực sự chặn lại."

Tùy chọn: Các quy tắc nghiêm ngặt hơn cho file do AI tạo ra

Nếu nhóm của bạn dùng một quy ước đặt tên file để đánh dấu code do AI tạo ra (*.ai.ts, *-generated.ts, hoặc tương tự), bạn có thể áp dụng các quy tắc chặt chẽ hơn riêng cho những file đó:

// 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
  },
}

Cách cấu hình golangci-lint cho code AI viết bằng Go

golangci-lint là trình chạy đa linter tiêu chuẩn cho Go. Nó đi kèm với gosec, errcheck, staticcheck, và hơn 40 linter khác có thể cấu hình từ một file YAML duy nhất. Đối với code Go do AI tạo ra, các quy tắc quan trọng là kiểm tra giá trị trả về lỗi và phát hiện mẫu bảo mật: hai loại lỗi mà các mô hình AI bỏ sót thường xuyên nhất trong Go.

Cài đặt 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

Cấu hình .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

Mẫu xử lý lỗi của Go là tường minh theo thiết kế: mọi hàm có thể thất bại đều trả về một giá trị lỗi. Các mô hình AI hiểu điều này nhưng đánh giá thấp nó; chúng sẽ bỏ qua việc kiểm tra lỗi ở những đường dẫn code không quan trọng. errcheck biến sự bỏ sót đó thành một lỗi lint.

gosec là linter bảo mật. Đối với code AI, nó bắt những mẫu mà AI tiếp thu từ các hướng dẫn Go trước năm 2020: tạo số ngẫu nhiên không an toàn (G404), các hàm băm không an toàn (G401), các vấn đề về quyền file (G306). Đây là những sai lầm bạn không bắt được khi review code vì chúng trông bình thường về mặt cú pháp.

Chạy golangci-lint

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

Cách kết nối linter vào các hook pre-commit

Một hook pre-commit chạy lint trước mỗi lần git commit và chặn commit nếu lint thất bại. Điều này có nghĩa là AI agent không thể commit code vi phạm các quy tắc bạn đã cấu hình. Nó phải sửa các vi phạm trước.

Lefthook là lựa chọn được khuyến nghị. Nó đa nền tảng, nhanh, và có một mẫu cấu hình hoạt động riêng cho việc thực thi với AI agent (sẽ trình bày ở phần tiếp theo).

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.

Thông điệp fail_text được AI agent đọc khi một lần commit thất bại. Mẫu này được ghi lại trong bài viết của Liam Bigelow về thực thi lint bằng Lefthook cho Claude Code. Riêng điều này sẽ không chặn được một agent kiên quyết, nhưng nó cho agent chỉ dẫn tiếp theo đúng đắn ("sửa các vi phạm lint") thay vì để nó tự suy ra một cách lách luật.

Phương án thay thế: pre-commit (thiết lập chỉ dành cho Python)

Nếu bạn dùng stack chỉ có Python và thích framework pre-commit hơn:

# .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: Linting trong dòng qua hook afterFileEdit

Nếu bạn dùng Cursor, bạn có thể kích hoạt lint ngay khi AI chỉnh sửa một file, trước cả khi nó thử commit. Tạo file .cursor/hooks.json trong thư mục gốc của dự án:

{
  "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}"
      }
    ]
  }
}

Hook này kích hoạt mỗi lần AI của Cursor chỉnh sửa một file. Agent nhận được phản hồi lint ngay trong dòng, trước khi nó coi nhiệm vụ là đã hoàn thành, nên hầu hết các vi phạm được sửa trước khi chúng chạm đến cổng pre-commit.

Cách làm cho cổng chặn khó bị AI agent vượt qua hơn

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

Bốn lớp thực thi khiến một AI agent khó bỏ qua cổng pre-commit bằng --no-verify hơn nhiều. Mỗi lớp nhắm vào một bề mặt tấn công khác nhau: chính sách trong CLAUDE.md, một quy tắc từ chối của Claude Code, một hook PreToolUse, và một chốt chặn CI. Hãy coi chúng là một thiết lập phân lớp, không phải bốn đảm bảo độc lập.

AI agent đôi khi truyền --no-verify cho git commit để bỏ qua các hook pre-commit khi lint thất bại và agent đã quyết định rằng các lỗi đó "không liên quan đến thay đổi của nó." Quyết định này không phải lúc nào cũng sai, nhưng bạn không nên để agent tự ý đưa ra. Toàn bộ ý nghĩa của cổng lint là con người đặt ra chính sách; việc của agent là đáp ứng nó, chứ không phải đi đường vòng.

Dưới đây là từng lớp và bề mặt tấn công mà nó bao quát.

Lớp 1: Ghi chính sách vào 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 đọc CLAUDE.md khi bắt đầu phiên làm việc. Đây không phải là thực thi; agent vẫn có thể thử vượt qua. Nhưng nó loại bỏ con đường "tôi không biết" và đặt ra một chính sách rõ ràng mà agent phải chủ động lựa chọn vi phạm, điều mà nó ít có khả năng làm hơn so với việc suy ra cách lách luật từ sự im lặng.

Lớp 2: Quy tắc từ chối của Claude Code

Thêm vào .claude/settings.json:

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

Điều này chặn lời gọi rõ ràng. Một hạn chế: quy tắc từ chối dùng so khớp tiền tố, nên nó chỉ bắt --no-verify đứng ngay sau commit. Một agent đủ sáng tạo có thể cấu trúc lời gọi theo cách khác. Đừng chỉ dựa vào riêng điều này.

Lớp 3: Hook PreToolUse

Cài đặt package block-no-verify:

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

Sau đó cấu hình nó như một hook PreToolUse trong cài đặt của Claude Code. Hook này kích hoạt trước mỗi lời gọi công cụ và kiểm tra các tham số xem có --no-verify trên sáu lệnh con của git hay không, không chỉ riêng commit. Nó thoát với mã khác 0 để chặn lời gọi trước khi nó thực thi.

Cẩm nang tại pydevtools.com nói thẳng về điều này: "lớp hook là lớp duy nhất thực thi quy tắc một cách đáng tin cậy." Hãy dùng nó cùng với các lớp khác; đừng coi bất kỳ hook cục bộ nào là toàn bộ sự đảm bảo.

Lớp 4: Chốt chặn CI

CI chạy trên máy chủ, nơi agent không có shell để truyền các cờ vào:

# .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 agent không thể truyền --no-verify cho CI. GitHub Actions chạy độc lập với bất cứ điều gì agent đã làm ở cục bộ. Nếu một commit bằng cách nào đó lọt qua với lint thất bại, CI sẽ bắt được nó trước khi nó được merge.

Đây là chốt chặn cuối cùng.

Phần thưởng: Máy chủ ESLint MCP

Nếu bạn dùng Claude Code, có một lớp chủ động giúp giảm tần suất bạn vướng phải cổng chặn ngay từ đầu.

Máy chủ ESLint MCP (@eslint/mcp) tích hợp ESLint trực tiếp vào vòng lặp công cụ của agent. Agent có thể truy vấn ESLint trong khi làm nhiệm vụ, trước khi nó thử commit. Cài đặt nó ở phạm vi toàn cục:

npm install -g @eslint/mcp@latest

Thêm vào .claude/settings.json:

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

Khi cấu hình điều này, agent có thể truy vấn ESLint trong khi làm nhiệm vụ. Agent nhận được phản hồi lint ngay trong dòng, và một số vi phạm có thể được sửa trước khi chúng chạm đến hook. Điều này không thay thế cổng chặn, nó giảm nhiễu cho cổng chặn.

Câu hỏi thường gặp

Tôi có cần cấu hình cả ba linter nếu tôi chỉ làm việc với một ngôn ngữ không?

Không. Hãy thiết lập linter cho ngôn ngữ chính của bạn và lớp thực thi. Nếu stack của bạn chỉ có TypeScript, hãy cấu hình ESLint và bỏ qua Ruff và golangci-lint. Phần ngăn chặn agent đi đường vòng áp dụng bất kể bạn lint ngôn ngữ nào.

Các cấu hình này có làm hỏng codebase hiện có của tôi trong lần chạy đầu tiên không?

Gần như chắc chắn, và đó là có chủ đích. Hãy chạy ruff check --fix . hoặc npx eslint . --fix để tự động sửa các vi phạm an toàn trước. Những gì còn lại sau khi tự động sửa chính là danh sách cần review thủ công: các cast no-explicit-any, các hàm vượt quá 50 dòng, thiếu xử lý lỗi. Hãy xử lý dần từng cái. Đừng thêm các quy tắc ignore để né tránh việc giải quyết chúng.

Biome có phải là phương án thay thế cho ESLint ở thời điểm này không?

Biome v2.5.0 (phát hành tháng 6 năm 2026) có sức cạnh tranh về định dạng và các quy tắc lint cơ bản. Nó nhanh hơn ESLint và không tốn công cấu hình nếu bạn cài đặt qua bun x ultracite@latest init. Đối với các nhóm muốn một công cụ duy nhất và không cần đến độ sâu đầy đủ của bộ quy tắc @typescript-eslint, Biome là một lựa chọn hợp lý. Đối với các quy tắc dành riêng cho AI trong hướng dẫn này (max-params, no-magic-numbers, max-lines-per-function với các ngưỡng nhắm vào AI), ESLint với @typescript-eslint vẫn có độ bao phủ rộng hơn. Bạn có thể chạy cả hai: Biome để định dạng và lint cơ bản, ESLint cho các quy tắc dành riêng cho AI.

Nếu AI agent của tôi cứ liên tục tạo lại cùng những vi phạm lint thì sao?

Agent đang đi vòng quanh quy tắc thay vì sửa vấn đề gốc rễ. Đối với no-explicit-any, điều này có nghĩa là thêm một type assertion thay vì định nghĩa kiểu thực sự. Đối với max-lines-per-function, nó có nghĩa là tách ra một hàm trợ giúp chẳng làm gì hữu ích nhưng đưa số dòng xuống dưới ngưỡng. Cả hai giải pháp đều không qua được review code. Quy tắc lint đã bắt được triệu chứng; nguyên nhân gốc rễ nằm ở prompt của agent. Hãy tinh chỉnh prompt để chỉ định ràng buộc về kiểu hoặc cách phân rã mong muốn; agent sẽ tuân theo hướng dẫn cấu trúc rõ ràng đáng tin cậy hơn so với các quy tắc ngầm định. Nếu bạn đang chạy một thiết lập agent phức tạp hơn, việc khoanh vùng công việc cho một subagent chuyên biệt với các ràng buộc được tích hợp sẵn trong chỉ dẫn của nó thường giữ vững hơn so với một prompt rộng đơn lẻ.

Máy chủ ESLint MCP có thay thế cổng pre-commit không?

Không. Nó giảm tần suất agent tạo ra code vi phạm cổng chặn. Cổng chặn vẫn chạy trên mỗi lần commit. Việc kiểm tra trong dòng của máy chủ MCP và việc thực thi của hook pre-commit là bổ trợ cho nhau, nên đừng loại bỏ cái nào.

Share

Thêm bài viết từ blog

Tiếp tục đọc.

Sẵn sàng triển khai? Từ $2.48/tháng.

Cloud độc lập, từ 2008. AMD EPYC, NVMe, 40 Gbps. Hoàn tiền trong 14 ngày.