# CLAUDE.md 里写明不许动的文件，Claude 还是照改，这种硬规矩该写在哪

**URL:** <https://www.shiqidu.com/t/topic/1822>\
**Category:** 开发调优\
**Tags:** 问与答, Claude, AICoding\
**Created:** [2026 年10 月 28 日 04:22 UTC](https://www.shiqidu.com/t/topic/1822 "2026-10-28T04:22:44Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![shiqidu\_editor](https://www.shiqidu.com/user_avatar/www.shiqidu.com/shiqidu_editor/32/528_2.png) [@shiqidu\_editor](https://www.shiqidu.com/u/shiqidu_editor)\
发布日期： [2026 年10 月 28 日 04:22 UTC](https://www.shiqidu.com/t/topic/1822/1 "2026-10-28T04:22:44Z")

</div>

CLAUDE.md 里写一句「migrations/ 下已经上线的迁移文件不许改」，加粗、写 NEVER、挪到文件第一行，Claude 照样有一定概率去改。规则文件放哪、各家工具认哪个，站里[这篇](https://www.shiqidu.com/t/topic/1788)聊过；这里想聊的是写了它不听怎么办。

官方 [memory 文档](https://code.claude.com/docs/en/memory)是这么说的：CLAUDE.md 是 context, not enforced configuration，它是作为一条用户消息塞在系统提示后面的，Claude 会读、会尽量照做，但不保证严格遵守。文档还建议单个文件控制在 200 行以内，写长了遵守程度会下降。同一段话里也给了出路：真要不管 Claude 怎么想都拦住某个动作，用 PreToolUse hook。

这样分工就比较清楚了：CLAUDE.md 放「希望它这么做」的东西，写得具体、能验证，只跟某些目录有关的挪到 `.claude/rules/` 里按路径加载，别让一个文件越写越长；「绝对不能发生」的事交给 hooks 和权限规则，那一层是客户端执行的，跟模型听不听话没关系。

## 拦住改文件

[hooks 指南](https://code.claude.com/docs/en/hooks-guide)开头就说，hooks 是在生命周期固定节点跑的 shell 命令，给的是 deterministic control，不靠模型自己决定要不要做。配置写在 `.claude/settings.json`（项目级，可以提交进仓库），个人全局的放 `~/.claude/settings.json`，只想自己生效又不提交的放 `.claude/settings.local.json`。

拦截脚本可以这么写。hook 从 stdin 收到一段 JSON，里面的 `tool_input.file_path` 就是要改的文件：

```bash
#!/bin/bash
# .claude/hooks/guard-paths.sh
f=$(jq -r '.tool_input.file_path // empty')
case "$f" in
  */migrations/*|*.env|*/.env.*)
    echo "不允许改 $f：上线过的迁移和 env 文件请找人手动处理" >&2
    exit 2 ;;
esac
exit 0

```

注册的时候 matcher 写 `Edit|Write`，顺手把交卷前跑测试的 `Stop` 也挂上（`Stop` 不支持 matcher，写了也会被忽略）：

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard-paths.sh" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests.sh" }
        ]
      }
    ]
  }
}

```

脚本记得 `chmod +x`，配好后在 Claude Code 里敲 `/hooks` 看有没有列出来。改 settings 一般不用重启会话，文件会被自动重新加载；菜单里迟迟不出现，先检查 JSON 里有没有多余的逗号或注释。

退出码是最容易踩的地方。[hooks 参考](https://code.claude.com/docs/en/hooks)里写着：exit 2 才是阻断，`PreToolUse` 上会直接挡掉这次工具调用，stderr 当作原因回给 Claude，它一般会换个做法。exit 1 虽然是 Unix 习惯里的失败码，在这里算 non-blocking error，动作照常执行。脚本路径写错、hook 根本没启动，也只会在记录里出个 hook error 提示，不会拦。

## 测试和格式化

`Stop` 在 Claude 答完准备收手时触发，exit 2 会让它停不下来、接着干。`run-tests.sh` 里先读 `stop_hook_active`，为 true 说明这一轮本来就是被 hook 拽回来的，直接 exit 0，否则跑测试，没通过就把失败信息写到 stderr 再 exit 2。这么写最多逼它返工一轮。不加这个判断，测试一直红就会一直循环，官方给了个兜底：连续拦 8 次、中间没有工具调用，就强制结束这一轮。

改完文件想自动跑 Prettier、lint，就用 `PostToolUse` 配 `Edit|Write`。要注意 `PostToolUse` 的 exit 2 拦不住任何东西，工具已经跑完了，只是把 stderr 给 Claude 看，适合告诉它 lint 报了什么。

上下文压缩后约定容易丢，可以挂 `SessionStart`，matcher 写 `compact`，往 stdout 打印的纯文本会被加进上下文，把几条关键约定 echo 进去就行。

## 拦了也有漏的

- `Edit|Write` 只管那两个工具。Claude 用 Bash 跑 `sed -i` 或者一段脚本照样能改文件，官方建议另挂 `Stop` 扫一遍工作区，或者把 `Bash|PowerShell` 也匹配上，用 `git status --porcelain` 看改了什么。
- hook 上的 `if` 字段是尽力而为，官方明说硬性 allow/deny 用[权限规则](https://code.claude.com/docs/en/permissions)，比如 `permissions.deny` 里写 `Edit(/migrations/**)`。deny 先于 ask 和 allow，allow 没法在 deny 里开例外。
- 权限规则也管不住 Python、Node 脚本自己开文件，要系统级隔离得开沙箱。

反过来，一个 exit 2 的 PreToolUse hook 优先级比 allow 规则高，所以「Bash 全放行、个别命令用 hook 拒掉」这种搭配也成立。

大家仓库里现在都拦了哪些东西？Bash 改文件这条口子是怎么堵的？
