×−+
Hooks 钩子第 6 课 · Claude Code 教程
第 6 课 · HOOKS

Hooks:
到点就执行,一次都不漏

CLAUDE.md 里写"改完代码要格式化",Claude 大多数时候会照做,偶尔会忘。Hook 像一台自动打卡机:到了那个时刻,你设好的命令一定会跑。

约 45 分钟 11 节 · 8 个动手练习 章末测验 改编自 luongnv89/claude-howto(MIT)
→ →
执行
→ →
点一个打卡点
入门先用起来 原理弄懂为什么 实战动手练 深入进阶与避坑

时间紧就先看"入门"和"实战";"原理"讲清楚退出码和匹配规则,写错一个数字,你的拦截就会悄悄失效。

01入门

Hook 是什么?

Hook 是你写的命令,Claude Code 在固定的时刻自动执行它,不经过 Claude 的判断。

还记得第 2 课那句话吗:"必须每次都执行的事,要交给 Hook。"原因在这里:CLAUDE.md 是给 Claude 看的提醒,做不做由它判断;Hook 是 Claude Code 自己执行的程序,时刻一到就跑。同样是"改完文件跑一次格式化":

CLAUDE.mdHook
是什么写给 Claude 看的说明Claude Code 自动执行的命令
谁来执行Claude 读了、自己决定Claude Code 到点就跑,不问 Claude
能保证吗大多数时候照做,不保证每次都执行
能拦下操作吗不能能,比如拦下对 .env 的修改
适合约定、背景、做事的偏好格式化、拦截、通知、记录这类机械动作
Hook 能做的四类事

拦:Claude 改敏感文件、跑危险命令之前拦下来。补:改完自动格式化、自动记日志。提醒你:Claude 等你批准时弹桌面通知。提醒 Claude:对话开始或压缩后,把当前分支、冻结期这些信息塞进上下文。

02入门

第一个 Hook:Claude 等你时弹通知

让 Claude 干一件长活,你去做别的;它需要你批准时,屏幕角落弹个通知。三步搞定。

① 写进设置文件

打开 ~/.claude/settings.json(没有就新建),加上下面这段。选你的系统:

~/.claude/settings.json

  • 文件里已经有 "hooks" 的话,把 "Notification" 加进去就行,不要把整个 hooks 替换掉。
  • "matcher": "" 表示所有类型的通知都触发。只想在"等你批准"时响,改成 "permission_prompt"。

② 用 /hooks 确认

在 Claude Code 里输入 /hooks,会列出所有事件,配了 hook 的事件旁边有数字。选 Notification,能看到你刚加的命令和它来自哪个文件。这个菜单只能看不能改,要改就直接编辑 JSON,或者让 Claude 帮你改。

③ 试一下

按 Shift+Tab 切到需要批准的模式(状态栏显示 manual mode),让 Claude 做一件要你批准的事,然后切到别的窗口。几秒后应该能收到通知。

也可以让 Claude 帮你写

直接说"帮我加一个 hook:Claude 等我批准时弹桌面通知",它会改好 settings.json。改完照样用 /hooks 核对一遍。

03入门

配置长什么样

一个 hook 有三层:什么时候(事件)、对谁(matcher)、做什么(命令)。点带色条的行看解释。

.claude/settings.json

放在哪里

位置对谁生效能共享吗
~/.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。

04入门

常用的 8 个事件

官方有 30 多个事件,日常用得上的就这几个。关键是分清能不能拦。

事件什么时候能拦下吗常见用途
SessionStart会话开始、恢复、/clear 后、压缩后不能把分支、待办塞进上下文
UserPromptSubmit你发出消息、Claude 处理之前能,拦下并清掉这条消息拦下带密码的提问;补充背景
PreToolUseClaude 调用工具之前能保护文件、拦危险命令
PermissionRequest要弹权限窗口时用 JSON 决定允许或拒绝自动批准某一类请求
PostToolUse工具成功执行之后不能,已经做完了格式化、记日志、检查结果
NotificationClaude 等你批准、空闲太久等不能桌面通知、发消息给手机
StopClaude 回答完、准备停下能,让它接着干检查测试是否都过了
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。

练一练:该挂在哪个事件上?

05原理

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 的回答
接下来
最常见的 bug:想拦却写了 exit 1

只有 exit 2 才会拦下。脚本里习惯写的 exit 1,Claude Code 当成"脚本出错",操作照样执行。你以为挡住了,其实没有。脚本路径写错、忘了 chmod +x,也按"出错"处理:不拦,只在对话里显示一条 hook error。第一次装护栏类 hook,记得亲手试一次能不能拦住。

06原理

matcher 与 if:只对该管的事触发

不写 matcher,这个事件每次都触发。写了,就只在匹配时触发:工具类事件比工具名;别的事件比别的东西,比如 Notification 比通知类型(permission_prompt),SessionStart 比启动方式(compact)。

  • 空、* 或不写:全部匹配。
  • 只有字母、数字、_、-、空格、,、|:按名字精确匹配,| 或逗号分隔多个。Edit|Write 只匹配这两个工具。
  • 含有其他字符:当成正则表达式,而且不锚定,名字里任何位置匹配上都算。Edit.* 连 NotebookEdit 也会匹配。
  • 区分大小写:bash 匹配不到 Bash。

试一试:这个 matcher 会匹配哪些工具?

"matcher":

if:再按参数筛一道

matcher 只看工具名。想"只在 Claude 跑 git 命令时触发",在 hook 里加 if,写法和第 4 课的权限规则一样:

.claude/settings.json(节选)
{
  "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。
07原理

安全: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 课)。
08实战

配方库: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 版示例。

09实战

终端演练:给 my-app 装上护栏

让 Claude 写一个保护文件的 hook,再看它怎样拦下 Claude 自己。点"下一步"回放,放完可以自己输入。

my-app — claude — 90×28
>

终端画面为教学示意,与真实 Claude Code 的界面细节可能略有不同,但 hook 的工作方式一致。

10深入

进阶与避坑

hook 没反应、拦不住、停不下来……按症状查。

hook 配了,但根本没触发
  1. /hooks 看它在不在对应的事件下面。不在的话,检查 JSON 格式:不能有注释和多余的逗号。
  2. matcher 区分大小写,工具名要写对(Bash、Edit、Write)。
  3. 事件选对了吗?PreToolUse 在工具执行前,PostToolUse 在执行后。
  4. 交互模式下,还没信任这个文件夹时 hook 不会跑。
  5. 脚本没有执行权限:chmod +x。
  6. 你是不是在项目的子目录里启动的 claude?项目 .claude/settings.json 里的 hook 只从启动目录读,不会往上级目录找。回到项目根目录再启动。
脚本返回了 JSON,却没有效果
两个常见原因。一是字段放错层级:permissionDecision、additionalContext 要放在 hookSpecificOutput 里面,放在最外层会被悄悄忽略。二是JSON 前面混进了别的输出:比如 ~/.zshrc 里无条件 echo 了一句欢迎语,输出不再以 { 开头,就不会被当成 JSON。把那些 echo 包进 if [[ $- == *i* ]]; then … fi。
Stop hook 让 Claude 停不下来
Stop hook 每次拦下,Claude 就接着干,干完又触发 Stop……Claude Code 在连续拦 8 次后,会强制结束这一轮(不是退出会话)。脚本开头检查输入里的 stop_hook_active,是 true 就说明这次已经是被 hook 叫回来的,直接 exit 0 放它停下。
我的 PostToolUse 没看到 Claude 用命令改的文件
Edit|Write 只管编辑工具。Claude 用 sed、mv 这类 Bash 命令改文件时不会触发。要覆盖所有改动,加一个 Stop hook 每轮扫一次 git status --porcelain;只盯某几个文件的话,用 FileChanged 事件。另外,PostToolUse 撤销不了已经做完的操作,要拦就用 PreToolUse。
Windows 上保护路径的判断总是失败
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 跑太久、超时了
默认超时:命令类 10 分钟(UserPromptSubmit 只有 30 秒),prompt 类 30 秒,agent 类 60 秒;SessionEnd 所有 hook 加起来只有 1.5 秒。在 hook 里用 "timeout": 秒数 调整。
怎么调试
先在终端里手动喂一段 JSON:echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?,看输出和退出码对不对。在 Claude Code 里按 Ctrl+O 看完整记录;要看每个 hook 的退出码和输出,用 claude --debug-file /tmp/claude.log 启动,或在对话里输入 /debug。
原仓库的示例脚本能直接用吗
建议对照官方文档改过再用。我们核对时发现:
  1. pre-commit.sh 注释说"只在 git commit 时跑测试",但脚本没检查命令内容,Claude 每跑一条 Bash 命令都会跑一遍全部测试。应该加 "if": "Bash(git commit *)"。
  2. validate-prompt.sh 把 additionalContext 放在最外层,会被忽略;它先找不存在的 user_prompt 字段,找不到才退回官方的 prompt,多此一举。
  3. README 里 prompt 类 hook 的返回格式写成 {"decision": "approve"},官方现在是 {"ok": true} / {"ok": false, "reason": "…"};它也不只能用在 Stop 上。
  4. README 说"其他退出码只在详细模式显示 stderr",实际会在对话里显示一条 hook error 提示。
11检验

小测验

8 道题,每题选完会立刻看到解析。