用 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. 最小落地架构
目标流程:
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 安装依赖
在项目根目录执行:
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"
}含义:
- 所有文件先走
oxfmt,不存在匹配文件时不报错。 - JS/TS 文件走
oxlint。 - CSS 文件走
stylelint --fix。
3.3 手动验证
先制造一个小改动,然后运行:
./node_modules/.bin/nano-staged --unstaged --quiet --bail预期结果:
- 没有问题:退出码为
0,输出很少或无输出。 - 有问题:退出码非
0,只显示第一个失败点,便于 AI 快速修复。
如果你想看退出码:
./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"
}
]
}
]
}
}关键点:
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:
#!/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 jq5. 案例三:让人类提交也跑同一套检查
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"预期结果:
- 如果检查通过,commit 正常完成。
- 如果检查失败,commit 被阻止,终端显示失败原因。
注意:不要在 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 .说明:
ruff format --check:只检查格式,不自动改,避免提交时隐式改动。ruff check:检查 lint。ty check .:类型检查通常难以只看单文件,先全量跑;如果太慢,可移到 CI。
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 中的规则分成三类:
- 可机器检查:格式、lint、类型、文件命名、禁止某些 API。
- 半自动检查:测试必须通过、构建必须成功、覆盖率阈值。
- 只能人工判断:业务边界、安全审批、是否符合产品目标。
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适合检查:
- 格式化
- lint
- 简单静态规则
- 变更文件范围内的轻量检查
每次 commit 前运行
目标:阻止明显坏提交。
建议内容:
nano-staged适合检查:
- staged 文件格式
- staged 文件 lint
- 部分类型检查
每个任务结束前运行
目标:证明这次任务可交付。
建议内容:
pnpm test
pnpm build或 Python 项目:
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 项目用:
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 --bail13. 失败处理速查
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 会更像一个受工具约束的工程成员,而不是一个靠记忆和自觉工作的实习生。