Hooks 钩子
本教程共 34 篇 · 第 22 篇 · 更新于 2026-07-26 · 约 11 分钟阅读
22. Hooks 钩子
本节目标:搞懂钩子(Hook)是什么、为什么比提示词约束更可靠。学会用 PreToolUse、PostToolUse、SessionStart 等事件,配匹配器过滤,用退出码和 JSON 控制行为。学完你能让某些操作「必定发生」—编辑后自动格式化、阻止改敏感文件、压缩后重注入上下文—不再赌 LLM 心情。
钩子解决什么问题
你可能在 CLAUDE.md 里写过「提交前跑测试」「别动 .env 文件」。问题是,这些只是建议,Claude 大概率会照做,但不是 100%。它哪天偷懒跳过测试,或者手滑改了不该改的文件,你拦不住。
钩子(Hook)就是把「建议」变成「硬规则」。它是你定义的 shell 命令,在 Claude Code 生命周期的特定节点确定性地执行。不依赖 LLM 选择,只要事件触发就一定跑。
打个比方:CLAUDE.md 里写「记得锁门」像贴张便签提醒,钩子像装了自动门锁—到点就锁,你忘不忘都一样。
钩子典型用途:
- 强制规则:阻止 Claude 改
.env、package-lock.json等敏感文件 - 自动化:编辑后自动跑 Prettier 格式化
- 通知:Claude 等你输入时弹桌面通知
- 上下文注入:压缩后重新塞入关键信息
- 审计:记录 Claude 执行的每条命令
Warning钩子用你当前系统环境的凭证(环境变量、用户权限)运行。恶意钩子可能泄露 API 密钥或误删文件。注册前务必逐行审查命令逻辑,别跑来路不明的脚本。
钩子放在哪配置
钩子写在 settings.json 的 hooks 块里。位置决定生效范围:
| 位置 | 范围 | 能共享 |
|---|---|---|
~/.claude/settings.json | 你的所有项目 | 否,本地 |
.claude/settings.json | 单个项目 | 是,提交到仓库 |
.claude/settings.local.json | 单个项目 | 否,gitignore |
| 托管策略设置 | 组织范围 | 是,管理员控 |
插件 hooks/hooks.json | 启用插件时 | 是,随插件 |
| Skill/Agent frontmatter | 组件激活时 | 是,定义在组件里 |
在 Claude Code 里输入 /hooks 能浏览所有已配置的钩子,按事件分组。这个菜单是只读的,增删改要直接编辑 JSON。
Tip文件监视器通常会自动拾取设置文件的改动。如果改完
/hooks没更新,重启会话强制重载。
钩子的基本结构
一个 hooks 块长这样:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
拆开看:
- 事件名(
PostToolUse):什么时候触发 - matcher(
"Edit|Write"):过滤条件,限定只对哪些工具/场景生效 - hooks 数组:具体执行的命令列表
- type:
command跑 shell 命令(最常用),还有http、prompt、agent等 - command:实际跑的 shell 命令
空 matcher 表示对该事件的所有情况触发。多个事件就并列写:
{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "..." }] }
],
"Notification": [
{ "matcher": "", "hooks": [{ "type": "command", "command": "..." }] }
]
}
}
常用事件一览
Claude Code 在生命周期很多节点都留了钩子口。最常用的几个:
| 事件 | 触发时机 | 能否阻止 |
|---|---|---|
PreToolUse | 工具调用执行前 | 能 |
PostToolUse | 工具调用成功后 | 否(已执行) |
UserPromptSubmit | 你提交提示词,Claude 处理前 | 能注入上下文 |
SessionStart | 会话开始或恢复 | 能注入上下文 |
Stop | Claude 完成响应 | 能让它继续干 |
Notification | Claude Code 发通知 | 否 |
ConfigChange | 配置文件在会话中变更 | 能 |
SubagentStart / SubagentStop | 子代理创建/结束 | - |
PreCompact / PostCompact | 压缩前后 | - |
SessionEnd | 会话结束 | - |
完整列表还有 PermissionRequest、PostToolUseFailure、PostToolBatch、FileChanged、CwdChanged、WorktreeCreate 等 30 来个。
Note
PreToolUse在任何权限模式检查之前触发。返回deny的钩子能阻止工具,哪怕在bypassPermissions模式或用了--dangerously-skip-permissions。这让你能强制执行用户绕不开的策略。但反过来,钩子返回allow不能绕过设置里的拒绝规则—钩子只能收紧,不能放松。
匹配器:精准过滤
不加 matcher,钩子对该事件的每次出现都触发。matcher 让你缩小范围。
对工具类事件(PreToolUse、PostToolUse 等),matcher 匹配工具名:
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt" }]
}
这只在 Claude 调 Bash 时记日志。用 | 或 , 分隔多个工具:
"matcher": "Edit|Write"
"matcher": "Edit,Write"
不同事件 matcher 过滤的内容不一样:
| 事件 | matcher 过滤 | 示例 |
|---|---|---|
PreToolUse / PostToolUse 等 | 工具名 | Bash、Edit|Write、mcp__.* |
SessionStart | 会话如何启动 | startup、resume、clear、compact |
Notification | 通知类型 | permission_prompt、idle_prompt |
ConfigChange | 配置源 | user_settings、project_settings、skills |
FileChanged | 文件名 | .envrc|.env |
用 if 字段按参数过滤
matcher 只在工具名级别过滤。要按参数一起过滤,用 if 字段,语法和权限规则一样:
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}
]
}
这只在 Claude 跑 git 命令时触发,其他 Bash 命令不触发。if 只对工具事件有效:PreToolUse、PostToolUse、PostToolUseFailure、PermissionRequest、PermissionDenied。
Warning
if过滤是尽力而为,Bash 命令解析失败时会开放(钩子照跑)。要硬性允许/拒绝,用权限系统,别用钩子。
钩子怎么收输入、给输出
钩子通过 stdin、stdout、stderr、退出码和 Claude Code 通信。
输入:stdin 收 JSON
事件触发时,Claude Code 把事件数据作为 JSON 喂给脚本的 stdin。比如 PreToolUse 在 Claude 跑 Bash 时收到:
{
"session_id": "abc123",
"cwd": "/Users/sarah/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}
脚本解析这个 JSON,就能拿到工具名、命令、文件路径等,据此决定怎么做。
输出:退出码 + 文本
最简单的控制方式是退出码:
- 退出 0:没意见,操作正常进行。对
UserPromptSubmit、SessionStart,你写到 stdout 的内容会加进 Claude 上下文 - 退出 2:阻止操作。写到 stderr 的原因会反馈给 Claude,让它调整
- 其他退出码:操作继续,但成绩单显示
<hook name> hook error
一个阻止删表的例子:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q "drop table"; then
echo "Blocked: 不允许删表" >&2
exit 2
fi
exit 0
结构化 JSON 输出
退出码只能阻止或沉默。要更精细控制,退出 0 并往 stdout 打印 JSON:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "用 rg 代替 grep,性能更好"
}
}
PreToolUse 的 permissionDecision 有四个值:
"allow":跳过交互式权限提示(但拒绝规则仍生效)"deny":取消工具调用,原因反馈给 Claude"ask":照常弹权限提示"defer":非交互模式下保留工具调用供稍后处理
Note用退出 2 + stderr 阻止,或用 JSON + 退出 0 做结构化控制。别混着用—退出 2 时 Claude Code 会忽略 JSON。
几个实战配置
编辑后自动格式化
Claude 每次编辑文件后自动跑 Prettier:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
jq 从输入 JSON 提取文件路径,xargs 把它传给 Prettier。
阻止改敏感文件
把检查逻辑放脚本里,更清晰。先建 .claude/hooks/protect-files.sh:
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH 匹配受保护模式 '$pattern'" >&2
exit 2
fi
done
exit 0
macOS/Linux 上记得加可执行权限:
chmod +x .claude/hooks/protect-files.sh
然后注册 PreToolUse 钩子:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
Claude 收到阻止反馈后会换路子,不会卡死。
压缩后重注入上下文
上下文压缩会丢细节。用 SessionStart 钩子在每次压缩后塞回关键信息:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo '提醒:用 Bun 不用 npm。提交前跑 bun test。当前迭代:auth 重构。'"
}
]
}
]
}
}
echo 可以换成 git log --oneline -5 这种动态命令。如果只是会话开始时注入,用 CLAUDE.md 更合适。
Claude 等输入时弹通知
切到别的窗口干活,Claude 完成后通知你。Windows 上:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude Code 需要你', 'Claude Code')\""
}
]
}
]
}
}
macOS 用 osascript,Linux 用 notify-send。matcher 可以细化到只在 idle_prompt(等输入)或 permission_prompt(等批准)时触发。
自动批准特定权限
每次计划做好都弹确认很烦。用 PermissionRequest 钩子自动批准 ExitPlanMode:
{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
}
]
}
]
}
}
Warningmatcher 尽量窄。匹配
.*或留空会自动批准所有权限提示,包括文件写入和 shell 命令,很危险。
多钩子怎么合并
同一事件配多个钩子时,每个钩子的命令都会跑完,Claude Code 再合并结果。一个钩子返回 deny 不会阻止兄弟钩子执行。
对 PreToolUse 权限决策,最严格的答案获胜,优先级:deny > defer > ask > allow。additionalContext 文本从每个钩子保留并一起传给 Claude。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "jq -r .tool_input.command >> ~/.claude/bash.log" },
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh" }
]
}
]
}
}
Claude 跑 rm -rf /tmp/build 时,两个钩子并行:日志钩子写日志后退出 0(无决策),防护钩子退出 2(拒绝)。拒绝获胜,命令被阻止,但日志已经写了。
不止 command:其他钩子类型
除了 "type": "command",还有几种:
"type": "prompt":基于提示的钩子。不跑 shell,而是把你的提示和钩子输入发给 Claude 模型(默认 Haiku)做是/否判断。返回 {"ok": true} 继续,{"ok": false, "reason": "..."} 阻止。适合需要判断而非硬规则的场景:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "检查所有任务是否完成。没完成就返回 {\"ok\": false, \"reason\": \"还剩什么\"}。"
}
]
}
]
}
}
"type": "agent":基于代理的钩子(实验性)。生成一个子代理,能读文件、搜代码、用工具验证条件,再返回决策。比 prompt 钩子更强,默认超时 60 秒,最多 50 轮工具调用:
{
"type": "agent",
"prompt": "验证所有单元测试通过。运行测试套件检查结果。$ARGUMENTS",
"timeout": 120
}
"type": "http":把事件数据 POST 到 HTTP 端点,端点用相同 JSON 格式返回结果。适合接外部审计服务、云函数。
Tip钩子输入数据本身够做决策时用 prompt 钩子。要根据代码库实际状态验证时用 agent 钩子。需要确定性规则时用 command 钩子。
组件级钩子
Skill 和子代理的 frontmatter 里也能内嵌 hooks,只在对应组件激活时生效,组件执行完自动清理,不污染全局会话。
组件级钩子支持的 once: true(仅 Skill):设为 true 时,该钩子整个会话只跑一次,首次成功后自动移除。
组件级钩子和全局钩子会并行执行,互不冲突,匹配器规则和全局一致。
超时和限制
设计钩子要注意这些约束:
- 超时:
command/http/mcp_tool默认 10 分钟(UserPromptSubmit降到 30 秒,MessageDisplay降到 10 秒);prompt30 秒;agent60 秒。用timeout字段按钩子覆盖 - PostToolUse 不能撤销:工具已经执行了,钩子只能反馈让 Claude 调整
- PermissionRequest 不在非交互模式触发:自动化权限决策用
PreToolUse代替 - Stop 阻止上限:连续阻止 8 次没进展,Claude Code 会覆盖钩子。脚本要检查
stop_hook_active字段,为true就提前退出 0
钩子不触发怎么排查
钩子配了不执行:
/hooks确认钩子在正确事件下- 检查 matcher 是否精确匹配工具名(区分大小写)
- 确认事件类型对:
PreToolUse在执行前,PostToolUse在之后 - 非交互模式别用
PermissionRequest,换PreToolUse
成绩单报 PreToolUse hook error:
- 脚本意外非零退出,手动测一下:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh echo $? command not found用绝对路径或${CLAUDE_PROJECT_DIR}引用脚本jq: command not found装 jq,或改用 Python/Node 解析 JSON- 脚本没跑,macOS/Linux 上
chmod +x加可执行权限
JSON 验证失败但 JSON 看着没错:shell 配置文件里的 echo 语句会污染输出。把 echo 包进交互式判断里:
if [[ $- == *i* ]]; then
echo "Shell ready"
fi
调试技巧:用 claude --debug-file /tmp/claude.log 启动,另一个终端 tail -f /tmp/claude.log 看完整执行详情—哪些钩子匹配、退出码、stdout、stderr 都有。会话中运行 /debug 也能开启日志并显示路径。
钩子 vs 技能 vs 权限
容易混的三个,理一下:
- 权限规则:允许/拒绝哪些工具操作,是准入控制。最硬,但只能 allow/deny
- 钩子(Hook):在生命周期节点跑命令,能做更复杂的事—格式化、通知、注入上下文、审计日志、阻止操作。确定性执行
- 技能(Skill):给 Claude 额外指令和可执行命令,按需加载。影响 Claude「知道什么、怎么做」
简单说:权限管「能不能做」,钩子管「做了之后/之前自动干啥」,技能管「怎么做更好」。