从"建议"到"强制":CLAUDE.md 为什么不够,Hooks 才是底线
前三篇讲了 Harness 是什么、Guides vs Sensors、七层解剖。这一篇专门讲第六层 Hooks------因为它是整个 Harness 里唯一能保证确定性执行的地方,也是大多数人最容易忽视的一层。
如果你只记住 Harness 系列的一个知识点,记住这个:CLAUDE.md 是建议,Hook 是强制。
一、一个真实的坑
第一次认真对待 Hooks,是因为踩了这个坑:
bash
在 CLAUDE.md 里写了:
"禁止删除项目目录外的任何文件。所有删除操作必须先确认。"
然后让 AI 重构一个模块。
AI 为了"清理冗余文件",执行了 rm -rf /tmp/build-cache/*
→ /tmp 不在项目目录里,但它觉得那是"构建缓存,删了没事"
→ 我没在旁边盯着,等发现的时候已经删完了
问它:"CLAUDE.md 里不是写了禁止删项目外的文件吗?"
它说:"抱歉,我在重构过程中注意力集中在代码结构上,
没有注意到这条规则。下次我会更小心。"
"下次会更小心"------这就是问题所在。
你不能把安全规则寄托在"模型下次会更小心"上。 它这次忘了,下次还会忘。上下文越长、任务越复杂,忘的概率越高。
二、CLAUDE.md 的本质:Context,不是 Configuration
Anthropic 官方文档里写得很清楚:
CLAUDE.md 是作为上下文(context)加载的,不是作为配置(configuration)强制执行的。
这意味着:
objectivec
CLAUDE.md 里的规则:
→ 模型读了
→ 通常会遵守
→ 但永远不保证遵守
→ 上下文窗口塞满时,routinely 忽略规则
这不是 bug,是 LLM 的本质。模型是概率性的,它"倾向于"遵守上下文里的规则,但没有任何机制保证它一定遵守。
objectivec
你写了 100 条规则在 CLAUDE.md 里:
第 1-10 条:模型大概率遵守
第 11-50 条:模型可能遵守,可能忽略
第 51-100 条:模型大概率忽略(上下文注意力衰减)
而且你不知道它忽略了哪条。
所以:任何合规、安全、硬性规则,不能只靠 CLAUDE.md。 你需要一个模型无法绕过的机制------Hook。
三、Hook 是什么:确定性执行的脚本
Hook 是在 Agent 生命周期的特定节点,由 Harness(不是模型) 确定性执行的脚本。
vbnet
常见 Hook 节点:
PreToolUse → 工具调用前执行(最常用、最强大)
PostToolUse → 工具调用后执行
UserPromptSubmit → 用户提交消息后执行
Stop → Agent 停止时执行
SubagentStart → 子 Agent 启动时
SubagentStop → 子 Agent 停止时
最关键的是 PreToolUse:
javascript
AI 想执行一个工具调用(比如 rm -rf /tmp/xxx)
↓
Harness 拦截这个调用,把事件 JSON 通过 stdin 传给 Hook 脚本
↓
Hook 脚本(Shell,确定性)读取 JSON,做检查
↓
没命中规则 → 不输出任何内容,正常退出 → 允许执行
命中规则 → 向 stdout 输出一段 JSON 决策
{"permissionDecision": "deny", "permissionDecisionReason": "..."}
→ Harness 阻止执行,并把原因返回给 AI
模型无法幻觉绕过 Hook。 因为 Hook 是 Harness 层面的代码,在模型的控制之外。模型可以"想"执行 rm,但 Harness 在执行前会跑你的脚本,脚本说不行就是不行。
四、Compact Test:判断你的 Harness 成不成熟
Harness 领域有一个判断标准,叫 Compact Test:
不成熟的 Harness:靠模型"记住"纪律。
成熟的 Harness:让纪律成为环境的一部分。
什么意思?
bash
不成熟的做法:
CLAUDE.md 写:"不要删除项目外的文件"
→ 靠模型记住并遵守
→ 模型忘了 = 出事
成熟的做法:
PreToolUse Hook 写:
if 命令包含 rm 且 路径不在项目目录内 → 拒绝
→ 不管模型记不记得,环境强制拦截
→ 模型永远删不了
再举几个例子:
| 规则 | 不成熟(靠模型记) | 成熟(靠环境强制) |
|---|---|---|
| 禁止删项目外文件 | CLAUDE.md 写禁令 | PreToolUse 检查路径 |
| 代码必须过 linter | CLAUDE.md 写"请跑 linter" | PostToolUse 自动跑 linter |
| 禁止提交到 main | CLAUDE.md 写"不要 push 到 main" | PreToolUse 检查 git 目标分支 |
| 敏感信息不能外传 | CLAUDE.md 写"注意保密" | PreToolUse 扫描输出内容 |
判断标准很简单:如果模型忘了这条规则,会不会出事?会 → 用 Hook 强制。不会 → 写 CLAUDE.md 就行。
五、实战:三个有用的 Hook
先用 .claude/settings.json 把脚本注册到对应事件上(这一步别漏,脚本不会自己生效):
json
// 项目根目录 .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Write|Edit|Delete",
"hooks": [
{ "type": "command", "command": ".claude/hooks/pre-tool-use/use-files.sh" },
{ "type": "command", "command": ".claude/hooks/pre-tool-use/bash-guard.sh" }
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": ".claude/hooks/post-tool-use/auto-lint.sh" }
]
}
]
}
}
Hook 1:路径白名单(禁止操作项目外文件)
bash
#!/bin/bash
# .claude/hooks/pre-tool-use/use-files.sh
# 只允许操作项目目录内的文件
# 事件 JSON 从 stdin 读入;拒绝时向 stdout 输出 JSON 决策
PROJECT_ROOT="/Users/file/Desktop/myproject"
INPUT=$(cat) # 读取事件 JSON(stdin)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# 检查文件操作类工具
if [[ "$TOOL_NAME" == "Delete" || "$TOOL_NAME" == "Write" || "$TOOL_NAME" == "Edit" ]]; then
if [[ -n "$FILE_PATH" && "$FILE_PATH" != "$PROJECT_ROOT"/* ]]; then
# 拒绝:输出 PreToolUse 决策 JSON
jq -n --arg p "$FILE_PATH" --arg root "$PROJECT_ROOT" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: ("拒绝:文件路径 " + $p + " 不在项目目录 " + $root + " 内")
}
}'
exit 0
fi
fi
exit 0
效果: 不管模型怎么想,项目外的文件它删不了、改不了。
Hook 2:命令黑名单(禁止危险 Shell 命令)
bash
#!/bin/bash
# .claude/hooks/pre-tool-use/bash-guard.sh
# 禁止危险的 Shell 命令
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
if [[ "$TOOL_NAME" == "Bash" ]]; then
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# 危险模式
if echo "$COMMAND" | grep -qE 'rm -rf /|sudo |chmod 777 /|dd if='; then
jq -n --arg c "$COMMAND" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: ("拒绝:检测到危险命令 " + $c)
}
}'
exit 0
fi
# rm 必须指定具体路径,不允许通配符删除
if echo "$COMMAND" | grep -qE 'rm .**'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "拒绝:不允许使用通配符删除,请指定具体文件"
}
}'
exit 0
fi
fi
exit 0
效果: 模型想执行 rm -rf / 或 sudo rm -rf /,直接被拦。
Hook 3:自动 linter(写完代码自动检查)
bash
#!/bin/bash
# .claude/hooks/post-tool-use/auto-lint.sh
INPUT=$(cat)
# 检查 jq
if ! command -v jq >/dev/null 2>&1; then
echo "jq not found, skip auto-lint" >&2
exit 0
fi
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')
if [[ "$TOOL_NAME" == "Write" || "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "MultiEdit" ]]; then
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
if [[ "$FILE_PATH" == *.kt ]]; then
# 检查 ktlint
if ! command -v ktlint >/dev/null 2>&1; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: "ktlint 未安装,跳过检查"
}
}'
exit 0
fi
# 跑 ktlint,不因非零退出码中断
RESULT=$(NO_COLOR=1 ktlint "$FILE_PATH" 2>&1 || true)
if [[ -n "$RESULT" ]]; then
jq -n --arg msg "$RESULT" '{
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: ("ktlint 检查结果:" + $msg)
}
}'
fi
fi
fi
exit 0
效果: 模型改完 Kotlin 文件,Harness 自动跑 ktlint,有问题直接反馈。这是 Computational Sensor------确定性、便宜、每次都跑。
六、Hook 的设计原则
objectivec
原则 1:Hook 要轻量
→ PreToolUse 在每次工具调用前跑,太慢会拖死整个 Agent
→ 简单检查(路径、命令黑名单)毫秒级完成
→ 复杂逻辑不要放 Hook 里,放 Agent 或独立服务里
原则 2:拒绝时要给明确原因
→ 不要只返回"拒绝"
→ 返回:"禁止删除项目外文件,你尝试删除的路径是 /tmp/xxx"
→ AI 知道为什么被拒,才能修正行为
原则 3:Hook 是最后一道防线,不是唯一防线
→ CLAUDE.md 仍然要写规则(Guides)
→ Hook 是强制兜底(Sensors + Guard)
→ 两者配合:Guides 告诉它"别这么做",Hook 确保"做不了"
原则 4:不要过度拦截
→ 每个命令都弹确认,Agent 根本跑不动
→ 只拦截真正危险的操作(删除、对外发送、权限变更)
→ 普通操作不要拦
七、一个常见误区:Hook 能解决一切问题
不能。Hook 能解决的是确定性规则 的强制执行,它解决不了判断性问题。
bash
Hook 能解决:
→ 不能删项目外的文件(路径检查,确定性)
→ 不能执行 rm -rf /(命令匹配,确定性)
→ 改完代码必须跑 linter(自动触发,确定性)
Hook 解决不了:
→ 这段代码架构合不合理(需要判断)
→ 这个函数命名好不好(需要判断)
→ 这个 PR 能不能合并(需要综合判断)
→ 判断性问题靠 Inferential Sensor(独立评审 Agent、LLM-as-judge)
Hook 管"能不能做",评审管"做得好不好"。 两者是互补的,不是替代的。
八、这一篇总结
markdown
1. CLAUDE.md 是 context 不是 configuration------模型可以忽略
2. 上下文越长,模型越容易忽略规则,这是 LLM 本质
3. Hook 是 Harness 层面的确定性脚本,模型无法幻觉绕过
4. PreToolUse 是最强大的 Hook:工具调用前拦截,输出 deny 决策 JSON 即阻止
5. Compact Test:不成熟靠模型记纪律,成熟让纪律成为环境的一部分
6. 安全/合规/硬规则必须用 Hook,不能只靠 CLAUDE.md
7. Hook 要轻量、拒绝给原因、不过度拦截
8. Hook 管"能不能做"(确定性),评审管"做得好不好"(判断性)
下篇讲 Harness 的棘轮原理(Ratchet Principle)------怎么让你的 Harness 越用越强,以及一个完整的实战例子。