写给小白的 Claude Code 进阶指南 - Hooks

cover

上周我在 CLAUDE.md 里写了一条规则:”每次修改代码后跑一下 TypeScript 类型检查”。

Claude 看到了。它确实会做。大部分时候。

但有一次对话拉得很长,上下文被压缩了一轮,它就悄悄跳过了这步。我没注意到,代码带着类型错误被提交了。第二天上线,炸了。

CLAUDE.md 里的规则是”建议”。Claude 会尽量遵守,但它终究是一个概率模型——它不保证每次都执行。

Hooks 不一样。Hooks 是确定性的。它每次都执行。没有例外。

一位开发者在自己的实践文章中写得很精确:”概率性指令与确定性执行的区别——这正是 Hooks 被严重低估的原因。”

Hooks 到底是什么?

一句话:Hooks 是绑定在 Claude Code 生命周期事件上的自动化脚本。

你可以把它理解为 Git Hooks 的 AI 版本。Git Hooks 在你 commit、push 的时候自动触发。Claude Code Hooks 在 Claude 读文件、写文件、跑命令、停止工作的时候自动触发。

每个 Hook 有三个要素:

  1. 事件(Event):什么时候触发?比如”Claude 准备使用某个工具之前”
  2. 匹配器(Matcher):针对哪些工具?比如”只针对 Bash 命令”
  3. 处理器(Handler):触发后做什么?比如”跑一个 Shell 脚本检查危险命令”

配置写在 JSON 文件里,长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-dangerous.sh",
"timeout": 30
}
]
}
]
}
}

看不懂没关系。后面一步步拆解。

关键概念:22 个生命周期事件

Claude Code 的整个工作流程被拆成了 22 个事件节点。你可以在任何一个节点上挂载 Hook。

最常用的五个:

事件 触发时机 典型用途
PreToolUse Claude 使用工具之前 拦截危险命令、保护文件
PostToolUse Claude 使用工具之后 自动格式化、跑类型检查
Stop Claude 停止工作时 质量关卡、跑测试
Notification Claude 发出通知时 桌面提醒、发消息到 Slack
ConfigChange 配置文件被修改时 审计追踪、阻止未授权修改

其中 PreToolUse 是最强大的。因为它能在 Claude 执行操作之前拦截——还没跑就被挡住了。这是你的安全层。

其他事件还包括:

  • SessionStart / SessionEnd:会话开始和结束
  • UserPromptSubmit:你提交 Prompt 之后、Claude 处理之前
  • PreCompact / PostCompact:上下文压缩前后
  • SubagentStart / SubagentStop:子智能体启动和完成
  • InstructionsLoaded:CLAUDE.md 或规则文件被加载时
  • WorktreeCreate / WorktreeRemove:Worktree 创建和移除
  • TaskCompleted:任务被标记完成时
  • Elicitation / ElicitationResult:MCP 服务器请求用户输入时

不需要全记住,用到再查。

四种 Handler 类型

Claude Code 支持四种类型的 Hook 处理器。这是它比 Cursor、Copilot 等工具更强大的原因之一。

1. Command(Shell 脚本)

最常用的类型。写一个 Shell 脚本,Hook 触发时自动执行。

脚本通过退出码(exit code)告诉 Claude 结果:

  • exit 0:没问题,继续
  • exit 2:有问题,阻止操作,并把 stderr 发给 Claude
  • 其他值:非阻塞性警告
1
2
3
4
5
6
7
8
9
10
#!/bin/bash
# 检查是否有危险命令
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

if echo "$COMMAND" | grep -qi "rm -rf /"; then
echo "危险!试图执行 rm -rf /" >&2
exit 2
fi
exit 0

2. Prompt(单轮 LLM 评估)

不用写脚本,直接让一个 LLM 来判断。适合需要”理解力”而不是简单模式匹配的场景。

1
2
3
4
5
{
"type": "prompt",
"prompt": "评估 Claude 是否应该停止工作。检查:1) 所有任务是否完成 2) 是否有未解决的错误。返回 JSON:{\"ok\": true} 或 {\"ok\": false, \"reason\": \"原因\"}",
"timeout": 30
}

3. Agent(多轮子智能体)

最强大的类型。一个有工具权限的子智能体,能读文件、搜索代码、多轮推理。

1
2
3
4
5
{
"type": "agent",
"prompt": "验证所有单元测试都通过。运行测试套件,读取结果,确认没有失败。",
"timeout": 120
}

4. HTTP(远程服务调用)

不需要本地脚本,直接把事件数据 POST 到一个 URL。适合团队共享的中心化校验服务,或者跟内部平台集成。

1
2
3
4
5
6
7
8
9
{
"type": "http",
"url": "http://localhost:8080/hooks/pre-tool-use",
"timeout": 30,
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}

注意两点:HTTP Hook 通过响应体返回 JSON 结果,格式和 Command Hook 一样;非 2xx 响应不会阻塞执行,只会产生一个非阻塞性错误。

选择建议: 永远从 Command 开始。需要”判断力”时升级到 Prompt;需要”调查能力”时用 Agent;需要跟远程服务集成时用 HTTP。Prompt 和 Agent 会消耗额外的 Token,HTTP 需要你维护一个服务端。

在哪里配置?

三个位置,优先级从高到低:

位置 作用范围 是否进 Git
.claude/settings.json 当前项目 是(团队共享)
.claude/settings.local.json 当前项目 否(个人配置)
~/.claude/settings.json 所有项目 否(全局配置)

团队规范的 Hook 放项目配置,个人习惯的 Hook 放全局配置。

你也可以在 Claude Code 里输入 /hooks 来可视化管理,不用手写 JSON。

七个最实用的 Hook

从实用度排序,这七个 Hook 几乎适用于所有项目。

1. 桌面通知:Claude 干完活提醒你

如果你经常让 Claude 在后台跑任务,自己去看别的东西,这个 Hook 必装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude 需要你的注意\" with title \"Claude Code\"'"
}
]
}
]
}
}

Linux 用户把 osascript 换成 notify-send "Claude Code" "Claude 需要你的注意"

2. 拦截危险命令

这应该是 Claude Code 的默认功能,但它不是。你需要自己加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
# .claude/hooks/block-dangerous.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

BLOCKED=("rm -rf /" "DROP TABLE" "DROP DATABASE" "git push --force" "git push -f")

for pattern in "${BLOCKED[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "安全拦截:包含 '$pattern'" >&2
exit 2
fi
done
exit 0

配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/block-dangerous.sh"
}
]
}
]
}
}

注意:用的是 PreToolUse,不是 PostToolUse。我们要在命令执行之前拦截,不是执行完了才发现。

3. 自动格式化

每次 Claude 编辑文件后,自动跑 Prettier:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null",
"timeout": 15
}
]
}
]
}
}

Claude Code 的创造者 Boris Cherny 自己就用这个设置。有一个细节要注意:如果格式化工具改了文件,Claude 每次都会收到文件变更的提醒,会吃上下文。更聪明的做法是改为在 Stop 事件时统一格式化,而不是每次编辑后都跑。

4. 类型检查

TypeScript 项目必备。每次 Claude 编辑 .ts.tsx 文件后,自动跑类型检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# .claude/hooks/typecheck.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.tsx ]]; then
exit 0
fi

OUTPUT=$(npx tsc --noEmit 2>&1)
if [ $? -ne 0 ]; then
echo "$OUTPUT" >&2
exit 2
fi
exit 0

这个 Hook 用 exit 2 阻塞。类型检查不通过,Claude 会收到错误信息,然后自动修复再继续。就像一个会自己检查编译的队友,而不是写完代码直接甩给你。

5. 保护关键文件

有些文件绝对不能让 AI 碰——.env、锁文件、迁移文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

PROTECTED=(".env" "package-lock.json" "pnpm-lock.yaml" ".git/")
for pattern in "${PROTECTED[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "已拦截:$FILE_PATH 是受保护文件" >&2
exit 2
fi
done
exit 0

6. 上下文压缩后自动恢复关键信息

Claude 的上下文窗口满了会自动压缩(Compaction),压缩后可能丢失一些重要的项目约定。用 SessionStart 事件配合 compact 匹配器,每次压缩后自动注入关键信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo '提醒:使用 pnpm 而不是 npm。提交前跑 pnpm test。当前迭代重点:认证模块重构。'"
}
]
}
]
}
}

echo 输出的内容会直接注入 Claude 的上下文。你也可以换成动态命令,比如 git log --oneline -5 来显示最近的提交。

7. 审计配置变更

团队协作中,配置文件被意外修改可能引发大问题。ConfigChange 事件能帮你追踪每一次变更:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"hooks": {
"ConfigChange": [
{
"hooks": [
{
"type": "command",
"command": "jq -c '{timestamp: now | todate, source: .source, file: .file_path}' >> ~/claude-config-audit.log"
}
]
}
]
}
}

每次配置变更都会被记录到日志文件。你甚至可以用 exit 2 阻止未授权的修改生效。

一个容易踩的坑:Stop Hook 死循环

这是新手最常犯的错误。

你写了一个 Stop Hook,在 Claude 停止时检查测试是否通过。测试没通过,Hook 返回 exit 2,阻止 Claude 停止。Claude 修复后试图再次停止,Hook 又触发,又检查——如果测试还是不通过,就无限循环了。

解决办法:在脚本开头检查 stop_hook_active 字段:

1
2
3
4
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # 已经在处理中,直接放行
fi

这是 Stop Hook 的第一条规则。每个 Stop Hook 都必须有这个检查。

Hooks vs CLAUDE.md vs Skills:什么时候用什么?

这三者经常被搞混。一个社区的总结非常清晰:

机制 类比 本质 保证执行?
CLAUDE.md 房子的规矩 指导性规则 不保证
Skills 按需启用的流程手册 可复用工作流 不保证
Hooks 房子的传感器和报警器 确定性自动化 保证

一个实用的判断标准:

  • “最好这样做” → 写进 CLAUDE.md
  • “这个流程经常重复” → 做成 Skill
  • “这件事必须每次都发生” → 用 Hook

安全相关的规则,永远用 Hook。因为安全不能是”建议”,必须是”强制”。

Matcher 的进阶用法

前面的例子里 Matcher 都比较简单,比如 "Bash""Edit|Write"。但 Matcher 实际上支持正则表达式,可以玩出更多花样。

匹配 MCP 工具:MCP 工具名遵循 mcp__<服务器>__<工具> 的命名规则。你可以精确匹配:

1
2
3
{
"matcher": "mcp__memory__.*"
}

这会匹配 memory 服务器的所有工具调用。mcp__.*__write.* 则匹配任何服务器的写操作。

另外,不是所有事件都支持 Matcher。UserPromptSubmitStopTaskCompleted 等事件没有 Matcher 过滤,每次都会触发。加了 Matcher 也会被忽略。

一个安全细节

Claude Code 在会话启动时会对你的 Hook 配置做一次快照,整个会话期间使用这份快照。会话中途修改 Hook 不会生效。 这意味着没有人——包括 AI 自己——能在运行时篡改 Hook。

这个设计,是让 Claude Code 可以无人值守运行的前提。

写在最后

Hooks 是 Claude Code 最被低估的功能。它不像 Plan Mode 那样直观,不像 Skills 那样容易上手,但它解决了一个最根本的问题:如何让 AI 可靠地执行规则?

我自己的经验是:从两个 Hook 开始——自动格式化和危险命令拦截。这两个能防住最常见的问题,零维护成本。

等用熟了,再加类型检查、测试关卡、桌面通知。

回到开头那次上线事故——如果当时装了 PostToolUse 的类型检查 Hook,那个错误根本不会进代码库。CLAUDE.md 告诉 AI 应该怎么做,Hooks 确保 AI 必须怎么做。两者配合,才是完整的 AI 开发工作流。