# 只写 AGENTS.md，挡不住 Agent 改客户的迁移文件

> Claude Code 只把指令文件当作上下文，不当作配置；真正能拦下写入的，是 PreToolUse 钩子。

原文: https://fdetimes.net/zh/guides/agents-md-pretooluse-hook-migrations/

在一个陌生的仓库里，编码 Agent 并不知道哪个目录正跑在生产环境。在它眼里，`db/migrations/` 和其他目录没什么两样。GitHub 分析了 2,500 多个包含 AGENTS.md 的仓库，给出的建议里有一条说得很直白：明确告诉 Agent 哪些东西绝对不能碰。

但告诉不等于拦住。Claude Code 的文档写明，这类指令文件被当作上下文，而不是强制执行的配置。要彻底拦下某个操作，需要一个 PreToolUse 钩子（hook）。

本指南写给那些入场第一周就要和 Agent 一起在客户代码库里干活的前线部署工程师（FDE）。走完六步，你会有两层防护：一份 AGENTS.md，让 Agent 理解规则；一个钩子，把禁区真正锁上。你还会清楚每一层的局限，免得向客户许下做不到的承诺。

## 要做什么，需要准备什么

全文围绕一个假想的例子：一家物流公司的 monorepo，里面有 `services/api`、`services/billing`，以及一个已在生产环境生效的 `db/migrations` 目录。文中的所有命令，比如 `make test`，都只是针对这个假想仓库的示例。实际工作中，请换成客户团队真正在用的命令。

你需要仓库的读权限，以及一个支持 AGENTS.md 的编码 Agent。Claude Code 可以读取 AGENTS.md 来代替 CLAUDE.md，Codex 也有自己加载这个文件的机制。最重要的是，约客户的一位工程师聊半小时，问清楚他们怎么构建、怎么测试，以及哪些目录谁都不许改。

## 第 1 步：像给新同事写 README 一样写给 Agent

agents.md 网站把这个文件描述为写给 Agent 的 README。它就是纯 Markdown，标题随你定，因为 Agent 只是读你写下的文字。没有 schema，也意味着没有任何语法能保证 Agent 一定照做。

文件开头先列出 Agent 需要运行的命令，每个参数都写全：

```markdown
# AGENTS.md

## 命令
- 构建：`make build`
- 测试（单个服务）：`make test SERVICE=api`
- 提交前做 lint：`make lint`
```

**检查：** 在自己的机器上把每条命令都跑一遍。你跑不通的命令，Agent 也跑不通，只是 Agent 可能会用你不想要的方式去“修”它。

## 第 2 步：三级边界，写明具体路径

在客户的仓库里，这是最重要的一部分。GitHub 提出了三级划分（始终要做、先问再做、绝不能做），并把边界列为每份文件都应具备的六个核心部分之一。GitHub 的示例文档 Agent 就带有一条路径级别的禁令：绝不能碰 `src/`。把同样的写法原样用到迁移目录上：

```markdown
## 边界
- 始终要做：提议提交前先运行 `make lint` 和 `make test`。
- 先问再做：新增依赖；修改 `services/billing/` 下的文件。
- 绝不能做：修改或删除 `db/migrations/` 下的文件；提交 `.env` 文件。
```

别省掉中间这一级：在客户那里，它往往最有用。计费代码不算禁区，但那里的每次改动都应该先得到客户方某个人的点头。

## 第 3 步：在迁移目录里单独放一份文件

据 agents.md 介绍，Agent 会自动读取目录树中离得最近的那份 AGENTS.md，且最近的文件优先。Codex 同样会从项目根目录（通常是 Git 根目录）一路走到当前工作目录，拼出一条指令链。所以放在 `db/migrations/` 里的文件，会在 Agent 进入该目录工作时加载。

```markdown
# db/migrations/AGENTS.md
本目录下的每个文件都已在生产环境执行过。
不要修改或删除现有文件。
如果需要改 schema，先停下来，询问团队成员。
```

拆分文件还有一个技术上的原因。Codex 在文件总大小达到 `project_doc_max_bytes` 上限（默认 32 KiB）后就不再加载更多文件。根目录的文件一旦臃肿，子目录的文件可能在没人察觉的情况下被跳过。

**检查：** 在 `db/migrations` 里开一个 Agent 会话，问它能看到哪些规则。如果没有出现这个目录的规则，问题就出在文件顺序或大小上。

## 第 4 步：用 PreToolUse 钩子锁住禁区

文件里写一句“绝不能做”并不是锁。Blake Crosley 在一篇讲解 Claude Code 钩子的文章里把这个区别讲得很清楚：指令文件给模型的是它大概率会遵循的指引，而钩子无论模型是否配合都会执行。

**要点:** 指令文件只是给 Agent 的建议；真正的禁区需要单独的拦截机制。

拦截机制很简单。钩子脚本向 stderr 写一条消息，然后以退出码 2 退出：这次工具调用会被拒绝，消息会回传给 Claude。下面是一个简化的示例脚本。它假定钩子从 stdin 收到工具调用的描述，只要调用中提到 `db/migrations/` 就拦下。

```bash
#!/usr/bin/env bash
# block-migrations.sh —— 示例，简化版
input=$(cat)

if echo "$input" | grep -q "db/migrations/"; then
  echo "已拦截：db/migrations/ 已在生产环境执行过。不要修改这里的文件，请停下来询问客户团队。" >&2
  exit 2
fi

exit 0
```

留意 stderr 里的这条消息。它会回传给 Agent，所以要写成带有下一步建议的解释，而不只是一句“拒绝”。这个简化版连读取该目录也会拦下；实际使用时，请查阅客户所用 Agent 的文档，弄清输入格式和 matcher 的声明方式，做到只拦写入。

exit 2 并不是唯一的办法。钩子也可以返回一个结构化决策 `permissionDecision: "deny"`。两者结果相同，都是拦下工具：一个靠退出码加 stderr，一个显式返回决策。

据 Crosley 介绍，即使在 `bypassPermissions` 模式下，返回 `deny` 的钩子依然会拦下工具。在客户那里，总可能有人把 Agent 的权限全部放开运行，这个细节很有分量。

**检查：** 让 Agent 往一个旧的迁移文件里加一行。这次编辑必须被拒绝，而且 Agent 应当复述你写在 stderr 里的原因。

## 第 5 步：钩子也有局限

钩子是防止失误的护栏，而不是阻止 Agent 有意绕过规则的机制。一篇在 dev.to 上讲如何拦截 `rm -rf` 的指南也指出，路径匹配是可以被绕过的。

看看上面的脚本：它只查找精确的字符串 `db/migrations/`。一条通过相对路径或环境变量进入该目录的 shell 命令，未必包含这个字符串，护栏很容易就放它过去。

所以和客户沟通时，不要把这两层中的任何一层称作“安全措施”。它们让失误更少，也更容易被发现。生产环境真正的保护，仍然是客户团队现有的访问控制和评审流程。

## 第 6 步：从小处起步，Agent 出错就补

第一天别写 300 行，何况 Codex 还有大小限制。给 Agent 一个真实的小工单，看它运行了哪些命令、改了哪些文件，然后在它出错的地方精准补充。

假设 Agent 没有只测一个服务，而是对整个仓库跑了 `make test`，花了二十分钟。你就加一行，说明如何用 `SERVICE` 测试单个服务。文件里的每一行，都应该能追溯到一次已经发生的失误，或客户点名的一项风险。

## 这两层在客户那里有什么用？

AGENTS.md 实际上就是一份关于代码库的客户调研纪要。问一句“哪些目录谁都不能碰？”，往往能挖出从未写下来的东西：一次曾引发事故的迁移，或者一个只有一个人敢改的模块。

把这些写下来，为最危险的区域加上钩子，再请客户的技术负责人把两者都过一遍。这是向对方表明你尊重他们系统的一个快捷方式。

如果你正在转向 FDE 岗位，可以在作品集里加一个示例仓库：一份三级边界的 AGENTS.md、迁移目录里的一份子文件、一个带测试的写入拦截钩子，以及一段展示每次 Agent 出错后文件如何改进的提交历史。

当职位描述提到要向客户团队推广 AI 编码工具时，你在面试中就有一个具体的例子可以展开，而不只是说自己“熟悉 Agent”。

## 你的草稿有没有漏掉什么？

拿你手头的仓库检查一遍。命令能否照写的样子跑通？边界里有没有“先问再做”这一级，还是只有“绝不能做”？“绝不能做”一级有没有写明具体路径？

接着，在某个子目录里打开 Agent，问它看到了哪些规则。最后，对每一条“绝不能做”问自己：如果 Agent 无视这一条，会不会出真正的事故？如果会，这一条就需要一个钩子。

你留给客户最好的 AGENTS.md，是你离开项目后对方团队还在持续修改的那一份；最好的钩子，是他们明白其存在理由的那一个。

**本周动手试试:**

- 选一个你正在做的仓库，写一个三级边界部分，在“绝不能做”一级至少写明一条具体路径，然后让 Agent 在这个区域附近做一个小任务。
- 在最敏感的目录里放一份简短的 AGENTS.md，在该目录里打开 Agent，问它能看到哪些规则。
- 复制第 4 步的钩子脚本，按你所用 Agent 的文档注册，然后让 Agent 修改一个迁移文件，看它是否被拦下、能否读到原因。

## 信源

- [AGENTS.md](https://agents.md)

- [How to write a great agents.md: Lessons from over 2,500 repositories](https://github.blog/ai-and-ml/github-copilot/how-to-write-a-great-agents-md-lessons-from-over-2500-repositories/)

- [How Claude remembers your project](https://code.claude.com/docs/en/memory)

- [Custom instructions with AGENTS.md | ChatGPT Learn](https://learn.chatgpt.com/docs/agent-configuration/agents-md)

- [Claude Code Hooks Explained: The Deterministic Layer Around Your Agent](https://blakecrosley.com/blog/claude-code-hooks-explained)

- [Block rm -rf with a Claude Code PreToolUse hook, and know its limits](https://dev.to/aicoding-guide/block-rm-rf-with-a-claude-code-pretooluse-hook-and-know-its-limits-2992)
