# 用 Agent Hooks 和 nano-staged 替代 AGENTS.md 里的细碎规则

- 原文：https://evilmartians.com/chronicles/stop-writing-rules-in-agents-md-use-agent-hooks-and-nano-staged-instead
- 来源：Evil Martians，Andrey Sitnik / Travis Turner，2026-05-26
- 本教程目标：把文章里的观点改写成可落地流程：把“提醒 AI 不要犯错”的文档规则，迁移为会自动执行的 formatter、linter、agent hook 和 pre-commit hook。
- 适用对象：正在使用 Claude Code、Codex 或类似 AI coding agent 的团队。
- 边界说明：本文示例以 JavaScript/TypeScript 项目为主；非 JS 项目给出 Lefthook/Python 迁移模板。

## 1. 核心原则

不要把机器能检查的规则写进 `AGENTS.md`。

正确分工是：

- `AGENTS.md`：只写高层边界、项目入口、禁止事项、验证命令。
- Formatter：处理代码格式。
- Linter：处理代码质量和团队规则。
- Agent hook：在 AI 编程流程中自动运行快速检查。
- Pre-commit hook：在人类提交代码前自动运行相同检查。
- CI：运行完整测试、构建、类型检查和安全检查。

一句话判断标准：如果规则可以被脚本判定通过或失败，它就不该只存在于 `AGENTS.md`。

## 2. 最小落地架构

目标流程：

```text
AI 修改代码
  ↓
Claude Code Stop hook 触发
  ↓
nano-staged 只检查变更文件
  ↓
失败：阻止停止，让 AI 继续修
成功：结束本轮任务
  ↓
开发者 git commit
  ↓
pre-commit 再跑同一套 nano-staged
  ↓
CI 跑完整验证
```

这个架构的价值是：

- AI 不需要记住格式规则。
- 快速错误会在本轮会话内暴露。
- 人类和 AI 共用同一套检查。
- 规则迁移到工具后，不再消耗 prompt token。

## 3. 案例一：JavaScript/TypeScript 项目接入 nano-staged

### 3.1 安装依赖

在项目根目录执行：

```bash
pnpm add --save-dev nano-staged oxfmt oxlint stylelint husky
```

如果项目不用 CSS，可以先不装 `stylelint`。

### 3.2 添加 `.nano-staged.json`

在项目根目录创建：

```json
{
  "*": "oxfmt --no-error-on-unmatched-pattern",
  "**/*.{js,ts,jsx,tsx}": "oxlint",
  "**/*.css": "stylelint --fix"
}
```

含义：

- 所有文件先走 `oxfmt`，不存在匹配文件时不报错。
- JS/TS 文件走 `oxlint`。
- CSS 文件走 `stylelint --fix`。

### 3.3 手动验证

先制造一个小改动，然后运行：

```bash
./node_modules/.bin/nano-staged --unstaged --quiet --bail
```

预期结果：

- 没有问题：退出码为 `0`，输出很少或无输出。
- 有问题：退出码非 `0`，只显示第一个失败点，便于 AI 快速修复。

如果你想看退出码：

```bash
./node_modules/.bin/nano-staged --unstaged --quiet --bail
echo $?
```

### 3.4 常见失败处理

如果提示找不到命令：

```bash
pnpm install
```

如果 `stylelint` 没有配置文件，先移除 CSS 规则，或补充 `.stylelintrc.json`。

如果项目仍在使用 ESLint/Prettier，可以先把配置改成：

```json
{
  "**/*.{js,ts,jsx,tsx}": "eslint --fix",
  "**/*.{js,ts,jsx,tsx,json,md,css}": "prettier --write"
}
```

等团队确认后再迁移到 `oxlint` / `oxfmt`。

## 4. 案例二：给 Claude Code 增加 Stop Hook

### 4.1 添加 `.claude/settings.json`

在项目根目录创建或更新：

```json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./node_modules/.bin/nano-staged --unstaged --quiet --bail || exit 2"
          }
        ]
      }
    ]
  }
}
```

关键点：

- `Stop`：Claude Code 准备结束当前任务时触发。
- `--unstaged`：检查工作区里未提交的变更，而不只是 staged 文件。
- `--quiet`：减少输出，节省 token。
- `--bail`：第一个错误就停，缩短反馈链。
- `exit 2`：阻止 Claude Code 停止，让它继续修复。

### 4.2 验证 Hook 是否生效

让 Claude Code 做一个会触发格式化或 lint 的小改动。预期行为：

- 如果 `nano-staged` 失败，Claude Code 不会直接结束。
- 它会看到错误输出，并继续修改代码。
- 修复后再次触发 Stop hook，通过才结束。

### 4.3 防止无人值守死循环

如果你把 agent 用在无人值守的软件工厂场景，必须避免“永远修不好、永远重试”。

创建脚本 `scripts/agent-stop-check.sh`：

```bash
#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
ACTIVE=$(printf '%s' "$INPUT" | jq -r '.stop_hook_active // false')

if [ "$ACTIVE" = "true" ]; then
  ./node_modules/.bin/nano-staged --unstaged --quiet --bail || true
  exit 0
fi

./node_modules/.bin/nano-staged --unstaged --quiet --bail || exit 2
```

赋予执行权限：

```bash
chmod +x scripts/agent-stop-check.sh
```

然后把 `.claude/settings.json` 改成：

```json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/agent-stop-check.sh"
          }
        ]
      }
    ]
  }
}
```

验证 `jq` 是否存在：

```bash
jq --version
```

如果没有：

```bash
brew install jq
# 或 Debian/Ubuntu
sudo apt-get install jq
```

## 5. 案例三：让人类提交也跑同一套检查

Agent hook 只能约束 AI 会话；人类提交也要走同样规则。

初始化 Husky：

```bash
pnpm husky init
```

写入 pre-commit hook：

```bash
printf '%s\n' './node_modules/.bin/nano-staged' > .husky/pre-commit
chmod +x .husky/pre-commit
```

验证：

```bash
git add .
git commit -m "chore: test nano-staged hook"
```

预期结果：

- 如果检查通过，commit 正常完成。
- 如果检查失败，commit 被阻止，终端显示失败原因。

注意：不要在 pre-commit 里跑全量慢测试。pre-commit 只适合快检查；完整测试放到 CI 或任务收尾阶段。

## 6. 非 JS 项目迁移模板：Python + Lefthook

如果项目不是 JS/TS，文章建议使用 `lefthook`。下面是 Python 项目的等价思路。

### 6.1 安装 Lefthook

macOS：

```bash
brew install lefthook
```

或用项目工具安装，按团队现有规范选择。

### 6.2 添加 `lefthook.yml`

```yaml
pre-commit:
  parallel: true
  commands:
    ruff-format:
      glob: "*.py"
      run: uv run ruff format --check {staged_files}
    ruff-check:
      glob: "*.py"
      run: uv run ruff check {staged_files}
    ty-check:
      glob: "*.py"
      run: uv run ty check .
```

说明：

- `ruff format --check`：只检查格式，不自动改，避免提交时隐式改动。
- `ruff check`：检查 lint。
- `ty check .`：类型检查通常难以只看单文件，先全量跑；如果太慢，可移到 CI。

### 6.3 Claude Code Hook 调用 Lefthook

```json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "lefthook run pre-commit || exit 2"
          }
        ]
      }
    ]
  }
}
```

如果 `ty check .` 太慢，不要放在 Stop hook；改成只放 Ruff，类型检查留到阶段末。

## 7. 把 AGENTS.md 规则迁移成自动化规则

### 7.1 先分类

把现有 `AGENTS.md` 中的规则分成三类：

- 可机器检查：格式、lint、类型、文件命名、禁止某些 API。
- 半自动检查：测试必须通过、构建必须成功、覆盖率阈值。
- 只能人工判断：业务边界、安全审批、是否符合产品目标。

### 7.2 迁移策略

可机器检查规则：迁移到 formatter、linter、自定义脚本。

示例：

```text
不要在代码里使用 console.log
```

应迁移为 ESLint 规则：

```json
{
  "rules": {
    "no-console": "error"
  }
}
```

半自动检查规则：迁移到 hook 或 CI。

示例：

```text
提交前必须测试通过
```

可以落为：

```bash
pnpm test
```

但是否放进 Stop hook，要看耗时。超过 10–20 秒的检查通常不适合每轮都跑。

只能人工判断的规则：保留在 `AGENTS.md`。

示例：

```text
未经确认不得修改生产数据库迁移。
```

这种规则不能靠 formatter 解决，应该继续作为高层边界保留。

## 8. 推荐检查分层

### 每次 Agent 停止时运行

目标：快，输出少，能自动修。

建议内容：

```bash
nano-staged --unstaged --quiet --bail
```

适合检查：

- 格式化
- lint
- 简单静态规则
- 变更文件范围内的轻量检查

### 每次 commit 前运行

目标：阻止明显坏提交。

建议内容：

```bash
nano-staged
```

适合检查：

- staged 文件格式
- staged 文件 lint
- 部分类型检查

### 每个任务结束前运行

目标：证明这次任务可交付。

建议内容：

```bash
pnpm test
pnpm build
```

或 Python 项目：

```bash
uv run pytest -q
uv run ruff check .
uv run ty check .
```

### CI 中运行

目标：最终门禁。

建议内容：

- 全量测试
- 类型检查
- 构建
- 安全扫描
- 依赖审计

## 9. 验收清单

完成改造后，用这份清单验收：

- `AGENTS.md` 不再包含格式化细节。
- 项目有 formatter/linter 配置。
- 本地手动运行 `nano-staged --unstaged --quiet --bail` 能得到明确通过或失败结果。
- Claude Code Stop hook 能在失败时阻止结束。
- 无人值守场景有防无限循环 wrapper。
- Git pre-commit 使用同一套 staged checks。
- 慢测试没有放进每次 Stop hook。
- CI 仍保留完整验证。
- Hook 输出足够短，不会把大量日志塞进 LLM 上下文。

## 10. 常见问题

### Q1：是不是完全不要 AGENTS.md？

不是。`AGENTS.md` 仍然适合写：

- 项目结构
- 常用命令
- 安全边界
- 需要人工确认的动作
- 任务完成的验证标准

不适合写：

- 缩进规则
- import 排序
- 是否允许 console.log
- 文件命名正则
- 可自动检测的代码风格

### Q2：Hook 会不会太烦？

会，所以只放快检查。慢检查放到任务结束或 CI。

### Q3：为什么不用 lint-staged？

可以用。文章推荐 `nano-staged` 的理由是：零依赖、攻击面小、速度更快、对 LLM 输出做了优化。

### Q4：AI 修不好怎么办？

无人值守场景要用防循环 wrapper；有人值守场景可以让 hook 阻止一次，然后由人决定是否跳过、修规则或改代码。

### Q5：如何处理重复出现的 AI 错误？

不要继续往 `AGENTS.md` 加提示。优先写：

- ESLint 自定义规则
- Ruff rule 或项目脚本
- 静态检查脚本
- pre-commit/agent hook

## 11. 一小时落地计划

### 0–10 分钟：梳理规则

从 `AGENTS.md` 中挑出 3 条最容易自动化的规则，例如格式、lint、禁止某 API。

### 10–25 分钟：接入 nano-staged 或 lefthook

JS/TS 项目用：

```bash
pnpm add --save-dev nano-staged
```

非 JS 项目用 Lefthook。

### 25–40 分钟：接入 Agent Stop Hook

先用最简单命令：

```bash
nano-staged --unstaged --quiet --bail || exit 2
```

确认能阻止 agent 停止。

### 40–50 分钟：接入 Git pre-commit

让人类提交也走同一套检查。

### 50–60 分钟：补防循环和文档

无人值守场景加 wrapper；`AGENTS.md` 中只保留一句：

```text
代码风格和快速静态检查由项目 hook 自动执行；任务结束前仍需运行项目验证命令。
```

## 12. 最小可复制模板

如果你只想快速复制，使用下面三份文件。

`.nano-staged.json`：

```json
{
  "*": "oxfmt --no-error-on-unmatched-pattern",
  "**/*.{js,ts,jsx,tsx}": "oxlint",
  "**/*.css": "stylelint --fix"
}
```

`.claude/settings.json`：

```json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "./scripts/agent-stop-check.sh"
          }
        ]
      }
    ]
  }
}
```

`scripts/agent-stop-check.sh`：

```bash
#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
ACTIVE=$(printf '%s' "$INPUT" | jq -r '.stop_hook_active // false')

if [ "$ACTIVE" = "true" ]; then
  ./node_modules/.bin/nano-staged --unstaged --quiet --bail || true
  exit 0
fi

./node_modules/.bin/nano-staged --unstaged --quiet --bail || exit 2
```

验证命令：

```bash
chmod +x scripts/agent-stop-check.sh
./node_modules/.bin/nano-staged --unstaged --quiet --bail
```

## 13. 失败处理速查

- `nano-staged: command not found`：先运行 `pnpm install`，hook 中使用 `./node_modules/.bin/nano-staged`。
- `jq: command not found`：安装 `jq`，或把 wrapper 改成不解析 JSON 的简单版。
- Hook 无限触发：检查 `.stop_hook_active` 防循环逻辑。
- 输出太长：加 `--quiet`，并让 linter 只报第一个错误。
- Hook 太慢：只保留 formatter/linter，移除测试和构建。
- AI 总是修同类问题：写 linter 规则，不要继续堆提示词。

## 14. 最终结论

这篇文章真正要落地的不是某个工具，而是一条工程原则：

能被机器检查的规则，就应该变成自动化门禁；`AGENTS.md` 只保留无法自动判断的边界和流程。

这样做后，AI coding agent 会更像一个受工具约束的工程成员，而不是一个靠记忆和自觉工作的实习生。
