Hooks:
到点就执行,一次都不漏
CLAUDE.md 里写"改完代码要格式化",Claude 大多数时候会照做,偶尔会忘。Hook 像一台自动打卡机:到了那个时刻,你设好的命令一定会跑。
时间紧就先看"入门"和"实战";"原理"讲清楚退出码和匹配规则,写错一个数字,你的拦截就会悄悄失效。
Hook 是什么?
Hook 是你写的命令,Claude Code 在固定的时刻自动执行它,不经过 Claude 的判断。
还记得第 2 课那句话吗:"必须每次都执行的事,要交给 Hook。"原因在这里:CLAUDE.md 是给 Claude 看的提醒,做不做由它判断;Hook 是 Claude Code 自己执行的程序,时刻一到就跑。同样是"改完文件跑一次格式化":
| CLAUDE.md | Hook | |
|---|---|---|
| 是什么 | 写给 Claude 看的说明 | Claude Code 自动执行的命令 |
| 谁来执行 | Claude 读了、自己决定 | Claude Code 到点就跑,不问 Claude |
| 能保证吗 | 大多数时候照做,不保证 | 每次都执行 |
| 能拦下操作吗 | 不能 | 能,比如拦下对 .env 的修改 |
| 适合 | 约定、背景、做事的偏好 | 格式化、拦截、通知、记录这类机械动作 |
拦:Claude 改敏感文件、跑危险命令之前拦下来。补:改完自动格式化、自动记日志。提醒你:Claude 等你批准时弹桌面通知。提醒 Claude:对话开始或压缩后,把当前分支、冻结期这些信息塞进上下文。
第一个 Hook:Claude 等你时弹通知
让 Claude 干一件长活,你去做别的;它需要你批准时,屏幕角落弹个通知。三步搞定。
① 写进设置文件
打开 ~/.claude/settings.json(没有就新建),加上下面这段。选你的系统:
- 文件里已经有
"hooks"的话,把"Notification"加进去就行,不要把整个 hooks 替换掉。 "matcher": ""表示所有类型的通知都触发。只想在"等你批准"时响,改成"permission_prompt"。
② 用 /hooks 确认
在 Claude Code 里输入 /hooks,会列出所有事件,配了 hook 的事件旁边有数字。选 Notification,能看到你刚加的命令和它来自哪个文件。这个菜单只能看不能改,要改就直接编辑 JSON,或者让 Claude 帮你改。
③ 试一下
按 Shift+Tab 切到需要批准的模式(状态栏显示 manual mode),让 Claude 做一件要你批准的事,然后切到别的窗口。几秒后应该能收到通知。
直接说"帮我加一个 hook:Claude 等我批准时弹桌面通知",它会改好 settings.json。改完照样用 /hooks 核对一遍。
配置长什么样
一个 hook 有三层:什么时候(事件)、对谁(matcher)、做什么(命令)。点带色条的行看解释。
放在哪里
| 位置 | 对谁生效 | 能共享吗 |
|---|---|---|
~/.claude/settings.json | 你的所有项目 | 不能,只在你电脑上 |
.claude/settings.json | 这个项目 | 能,提交到 git,团队共用 |
.claude/settings.local.json | 这个项目,只有你 | 不能,不进 git |
| 组织策略设置 | 整个公司 | 由管理员统一下发 |
插件的 hooks/hooks.json | 启用了这个插件时(第 10 课) | 能,跟着插件走 |
| skill、子代理的 frontmatter(文件开头的配置区) | skill 被调用后的本次会话;子代理运行期间(第 5、8 课) | 能,写在文件里 |
和 CLAUDE.md 一样:团队都要遵守的放项目的 .claude/settings.json;桌面通知这种个人习惯放 ~/.claude/settings.json。
常用的 8 个事件
官方有 30 多个事件,日常用得上的就这几个。关键是分清能不能拦。
| 事件 | 什么时候 | 能拦下吗 | 常见用途 |
|---|---|---|---|
SessionStart | 会话开始、恢复、/clear 后、压缩后 | 不能 | 把分支、待办塞进上下文 |
UserPromptSubmit | 你发出消息、Claude 处理之前 | 能,拦下并清掉这条消息 | 拦下带密码的提问;补充背景 |
PreToolUse | Claude 调用工具之前 | 能 | 保护文件、拦危险命令 |
PermissionRequest | 要弹权限窗口时 | 用 JSON 决定允许或拒绝 | 自动批准某一类请求 |
PostToolUse | 工具成功执行之后 | 不能,已经做完了 | 格式化、记日志、检查结果 |
Notification | Claude 等你批准、空闲太久等 | 不能 | 桌面通知、发消息给手机 |
Stop | Claude 回答完、准备停下 | 能,让它接着干 | 检查测试是否都过了 |
SessionEnd | 会话结束 | 不能 | 清理临时文件、记录统计 |
其他事件一览
PostToolUseFailure 工具失败后 · PostToolBatch 一批并行工具都结束后 · PermissionDenied auto 模式拒绝后 · SubagentStart / SubagentStop 子代理开始和结束 · PreCompact / PostCompact 压缩前后 · InstructionsLoaded 加载了 CLAUDE.md 或规则文件 · ConfigChange 设置文件被改 · CwdChanged 工作目录变了 · FileChanged 盯着的文件变了 · PreModelSwitch / PostModelSwitch 换模型前后 · StopFailure 因 API 错误中断 · WorktreeCreate / WorktreeRemove · TaskCreated / TaskCompleted · Elicitation 等。完整列表见官方 Hooks reference。
练一练:该挂在哪个事件上?
Hook 怎么和 Claude Code 说话
进:Claude Code 把这次事件的信息写成 JSON,从标准输入交给你的命令。出:你的命令用退出码或输出的 JSON 回答"放行还是拦下"。
| 退出码 | 意思 | 拿 PreToolUse 来说 |
|---|---|---|
0 | 没意见 | 不等于批准:照常走权限流程,该问你还是会问 |
2 | 拦下 | 工具不执行;你写到 stderr 的话会交给 Claude,它会换个做法 |
其他(如 1) | 脚本出错,不拦 | 显示一条 "hook error" 提示,操作照常进行 |
想说得更细,就退出码 0,往标准输出打印一个 JSON,把 "permissionDecision" 放在 hookSpecificOutput 里(格式见下面的模拟器):deny 拒绝,allow 不用问直接执行,ask 一定弹窗问你。
试一试:同一个操作,不同的回答
选 Claude 想做的事、你的 hook 怎么回答,看结果:
Claude 想做
你的 PreToolUse hook
运行方式
Hook 收到的输入(stdin)
Hook 的回答
接下来
只有 exit 2 才会拦下。脚本里习惯写的 exit 1,Claude Code 当成"脚本出错",操作照样执行。你以为挡住了,其实没有。脚本路径写错、忘了 chmod +x,也按"出错"处理:不拦,只在对话里显示一条 hook error。第一次装护栏类 hook,记得亲手试一次能不能拦住。
matcher 与 if:只对该管的事触发
不写 matcher,这个事件每次都触发。写了,就只在匹配时触发:工具类事件比工具名;别的事件比别的东西,比如 Notification 比通知类型(permission_prompt),SessionStart 比启动方式(compact)。
- 空、
*或不写:全部匹配。 - 只有字母、数字、
_、-、空格、,、|:按名字精确匹配,|或逗号分隔多个。Edit|Write只匹配这两个工具。 - 含有其他字符:当成正则表达式,而且不锚定,名字里任何位置匹配上都算。
Edit.*连NotebookEdit也会匹配。 - 区分大小写:
bash匹配不到Bash。
试一试:这个 matcher 会匹配哪些工具?
if:再按参数筛一道
matcher 只看工具名。想"只在 Claude 跑 git 命令时触发",在 hook 里加 if,写法和第 4 课的权限规则一样:
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}
]
}if只能用在工具相关的事件上(PreToolUse、PostToolUse 等),加在别的事件上,hook 就不会运行。npm test && git push这种连起来的命令会拆开检查,git push匹配上就触发。- 这层筛选是"尽量":看不懂的命令,hook 照样运行。要硬性禁止某件事,用权限规则,不要只靠 hook。
安全:hook 是你电脑上的真命令
hook 用你的账户权限运行,能读、改、删你能碰的任何文件。它很有用,也要当心。
| 规则 | 意思 |
|---|---|
| 先信任,才运行 | 交互模式下,你接受"是否信任此文件夹"之前,所有设置文件里的 hook 都不会跑,连你自己的 ~/.claude/settings.json 也一样 |
-p 不问信任 | claude -p 把文件夹当成已信任,别人仓库里提交的 hook 会直接运行。跑陌生仓库前先看它的 .claude/,或者加 --bare(第 4 课) |
| hook 拦下,换什么权限模式都绕不过 | PreToolUse 返回拒绝或 exit 2,即使在跳过全部权限的模式下、有允许规则时也生效。前提是 hook 真的被触发了,所以硬性禁止还要配上权限规则 |
| hook 放行,不能越过你的规则 | hook 返回 allow 只是省掉弹窗;设置里的拒绝规则照样拦,询问规则照样弹窗。hook 只能收紧,不能放宽 |
| 多个 hook,最严格的算数 | 同一事件的多个 hook 并行运行,全部跑完再合并:deny 优先于 ask,ask 优先于 allow |
写 hook 的好习惯
- 变量加引号:写
"$FILE_PATH",不要写$FILE_PATH,路径里有空格就会出事。 - 脚本用绝对路径:写成
"$CLAUDE_PROJECT_DIR"/.claude/hooks/xxx.sh,不管 Claude 当时在哪个目录都找得到。 - 别信输入:检查路径里有没有
..;绕开.env、.git/、密钥这类文件。 - 要临时全部关掉:设置里写
"disableAllHooks": true,或启动时加--settings '{"disableAllHooks": true}'。没办法只关某一个,只能删掉它。它会顺带关掉自定义状态栏(第 9B 课)。
配方库:7 个拿来就能用的 hook
都改编自官方文档的示例。选一个,拷贝配置,按"怎么验证"试一遍。
Bash 版本的配方用到 jq 解析 JSON:macOS 用 brew install jq,Ubuntu 用 apt-get install jq。脚本文件记得 chmod +x。Windows 注意:Claude 在 Windows 上默认用 PowerShell 跑命令,matcher 只写 Bash 的 hook 基本不会触发。要管命令,matcher 写 Bash|PowerShell;拦删除还得认 Remove-Item -Recurse 这种写法,官方 Hooks reference 开头有完整的 Windows 版示例。
终端演练:给 my-app 装上护栏
让 Claude 写一个保护文件的 hook,再看它怎样拦下 Claude 自己。点"下一步"回放,放完可以自己输入。
终端画面为教学示意,与真实 Claude Code 的界面细节可能略有不同,但 hook 的工作方式一致。
进阶与避坑
hook 没反应、拦不住、停不下来……按症状查。
hook 配了,但根本没触发
/hooks看它在不在对应的事件下面。不在的话,检查 JSON 格式:不能有注释和多余的逗号。- matcher 区分大小写,工具名要写对(
Bash、Edit、Write)。 - 事件选对了吗?PreToolUse 在工具执行前,PostToolUse 在执行后。
- 交互模式下,还没信任这个文件夹时 hook 不会跑。
- 脚本没有执行权限:
chmod +x。 - 你是不是在项目的子目录里启动的 claude?项目
.claude/settings.json里的 hook 只从启动目录读,不会往上级目录找。回到项目根目录再启动。
脚本返回了 JSON,却没有效果
permissionDecision、additionalContext 要放在 hookSpecificOutput 里面,放在最外层会被悄悄忽略。二是JSON 前面混进了别的输出:比如 ~/.zshrc 里无条件 echo 了一句欢迎语,输出不再以 { 开头,就不会被当成 JSON。把那些 echo 包进 if [[ $- == *i* ]]; then … fi。Stop hook 让 Claude 停不下来
stop_hook_active,是 true 就说明这次已经是被 hook 叫回来的,直接 exit 0 放它停下。我的 PostToolUse 没看到 Claude 用命令改的文件
Edit|Write 只管编辑工具。Claude 用 sed、mv 这类 Bash 命令改文件时不会触发。要覆盖所有改动,加一个 Stop hook 每轮扫一次 git status --porcelain;只盯某几个文件的话,用 FileChanged 事件。另外,PostToolUse 撤销不了已经做完的操作,要拦就用 PreToolUse。Windows 上保护路径的判断总是失败
tool_input.file_path 用反斜杠(C:\project\src\index.ts),用 /src/ 去比对永远匹配不上,结果什么都没拦住。先统一成正斜杠:Bash 里 FILE_PATH="${FILE_PATH//\\//}",Python 里 path.replace("\\", "/")。不想写死规则,想让模型判断
"type": "prompt":Claude Code 把你的提示词和事件信息交给一个模型(默认 Haiku),它回答 {"ok": true} 或 {"ok": false, "reason": "…"}。需要读文件、跑命令才能判断时,用 "type": "agent"(实验功能)。还有 http(把事件发到一个网址)和 mcp_tool(调用 MCP 工具,第 7 课)两种类型。hook 跑太久、超时了
"timeout": 秒数 调整。怎么调试
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?,看输出和退出码对不对。在 Claude Code 里按 Ctrl+O 看完整记录;要看每个 hook 的退出码和输出,用 claude --debug-file /tmp/claude.log 启动,或在对话里输入 /debug。原仓库的示例脚本能直接用吗
pre-commit.sh注释说"只在 git commit 时跑测试",但脚本没检查命令内容,Claude 每跑一条 Bash 命令都会跑一遍全部测试。应该加"if": "Bash(git commit *)"。validate-prompt.sh把additionalContext放在最外层,会被忽略;它先找不存在的user_prompt字段,找不到才退回官方的prompt,多此一举。- README 里 prompt 类 hook 的返回格式写成
{"decision": "approve"},官方现在是{"ok": true}/{"ok": false, "reason": "…"};它也不只能用在 Stop 上。 - README 说"其他退出码只在详细模式显示 stderr",实际会在对话里显示一条 hook error 提示。
小测验
8 道题,每题选完会立刻看到解析。
