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

1. 核心原则

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

正确分工是:

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

2. 最小落地架构

目标流程:

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

这个架构的价值是:

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

3.1 安装依赖

在项目根目录执行:

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

如果项目不用 CSS,可以先不装 stylelint

3.2 添加 .nano-staged.json

在项目根目录创建:

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

含义:

3.3 手动验证

先制造一个小改动,然后运行:

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

预期结果:

如果你想看退出码:

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

3.4 常见失败处理

如果提示找不到命令:

pnpm install

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

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

{
  "**/*.{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

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

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

关键点:

4.2 验证 Hook 是否生效

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

4.3 防止无人值守死循环

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

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

#!/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

赋予执行权限:

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

然后把 .claude/settings.json 改成:

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

验证 jq 是否存在:

jq --version

如果没有:

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

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

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

初始化 Husky:

pnpm husky init

写入 pre-commit hook:

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

验证:

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

预期结果:

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

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

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

6.1 安装 Lefthook

macOS:

brew install lefthook

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

6.2 添加 lefthook.yml

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 .

说明:

6.3 Claude Code Hook 调用 Lefthook

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

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

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

7.1 先分类

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

7.2 迁移策略

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

示例:

不要在代码里使用 console.log

应迁移为 ESLint 规则:

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

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

示例:

提交前必须测试通过

可以落为:

pnpm test

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

只能人工判断的规则:保留在 AGENTS.md

示例:

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

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

8. 推荐检查分层

每次 Agent 停止时运行

目标:快,输出少,能自动修。

建议内容:

nano-staged --unstaged --quiet --bail

适合检查:

每次 commit 前运行

目标:阻止明显坏提交。

建议内容:

nano-staged

适合检查:

每个任务结束前运行

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

建议内容:

pnpm test
pnpm build

或 Python 项目:

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

CI 中运行

目标:最终门禁。

建议内容:

9. 验收清单

完成改造后,用这份清单验收:

10. 常见问题

Q1:是不是完全不要 AGENTS.md?

不是。AGENTS.md 仍然适合写:

不适合写:

Q2:Hook 会不会太烦?

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

Q3:为什么不用 lint-staged?

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

Q4:AI 修不好怎么办?

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

Q5:如何处理重复出现的 AI 错误?

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

11. 一小时落地计划

0–10 分钟:梳理规则

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

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

JS/TS 项目用:

pnpm add --save-dev nano-staged

非 JS 项目用 Lefthook。

25–40 分钟:接入 Agent Stop Hook

先用最简单命令:

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

确认能阻止 agent 停止。

40–50 分钟:接入 Git pre-commit

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

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

无人值守场景加 wrapper;AGENTS.md 中只保留一句:

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

12. 最小可复制模板

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

.nano-staged.json

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

.claude/settings.json

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

scripts/agent-stop-check.sh

#!/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

验证命令:

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

13. 失败处理速查

14. 最终结论

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

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

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