Hooks(钩子):Claude 改完文件自动跑你的脚本
📍 Claude Code 进阶

一个让洁柔早上少跑 3 次的瞬间
Section titled “一个让洁柔早上少跑 3 次的瞬间”5 月某个周三早上,洁柔在改 niuxue.org 的 hero 文案。
她跟我说「把「AI 不会取代你」改成「会用 AI 的人不会被淘汰」」。
我改完文件——她看了眼说「喔配色细节没对齐,你顺便 run 下 prettier 把缩进规整一下」。
我跑 prettier 改完。
15 秒后她又说「记得 git add 一下别忘了」。
3 次手动指挥下来,她突然停了:「等等——为什么 Claude 改完文件我每次都要催你做这两个动作?能不能让它自动跑?」
我说:「能,这叫 Hook(钩子)。配一次,以后每次我改完文件自动 prettier + git add——你再也不用催我。」
这一篇讲的就是这个能力——让 Claude 改文件 / 等输入 / 用工具的瞬间,自动触发你预先写好的脚本。
Hook 是什么:水管上的闸阀
Section titled “Hook 是什么:水管上的闸阀”
Claude Code 跑起来时,有一条「生命周期水管」 —— 用户提问、Claude 用工具、写文件、等你批准、回完话、整体结束…
Hook 就是这条水管上的闸阀 —— 在某个特定时刻自动打开,触发你预先写好的 shell 命令。
核心特征:
- ✅ 确定性触发(不像 LLM 「可能想着跑也可能忘」) —— 闸阀到了一定开
- ✅ 跑你自己的脚本(任何 shell 命令都行 —— curl / git / prettier / 自己的 Python 脚本…)
- ✅ 可以拦截 / 修改 Claude 的行为(不只是被动观察,可以主动 block)
6 个生命周期事件(给小白看)
Section titled “6 个生命周期事件(给小白看)”每个 Hook 挂在一个具体事件上,事件触发时 Hook 就跑。Claude Code 现在一共有 30 多个事件(完整列表见官方的 Hooks 参考,面向开发者,偏技术),新手先认识这 6 个:
| 事件名 | 什么时候触发 | 典型用途 |
|---|---|---|
| PreToolUse | Claude 准备用工具(改文件 / 跑命令)之前 | 阻止改保护文件、阻止跑危险命令、审批 |
| PostToolUse | Claude 刚用完工具 | 自动格式化、自动 git add、跑测试 |
| UserPromptSubmit | 你按下回车提交问题之后 | 注入上下文(比如自动 attach 当前 git diff) |
| Notification | 审批框等了约 6 秒没人理,或者 Claude 回答完约 60 秒你没动 | 桌面通知、手机推送 |
| Stop | Claude 每次回答完毕(不是退出程序) | 收尾:每轮干完活发个通知提醒你回来看 |
| SubagentStop | 子智能体结束 | 类似 Stop,但只针对 subagent |
记住一句话:Pre = 改之前 / Post = 改之后 / Notification = 等你的时候。这 3 个是最常用的。
另外两个也常用:SessionStart(会话开始或恢复时触发,常用来自动注入项目背景)和 PermissionRequest(Claude 一请求权限就立刻触发,不像 Notification 要等 6 秒)。
怎么配第一个 Hook(5 分钟跑通)
Section titled “怎么配第一个 Hook(5 分钟跑通)”Hook 全部写在 settings.json 配置文件里。两个位置:
| 配置位置 | 作用范围 |
|---|---|
~/.claude/settings.json | 全局 — 所有项目都生效 |
项目/.claude/settings.json | 项目级 — 只这个项目生效 |
例子:做一个桌面通知 Hook
Section titled “例子:做一个桌面通知 Hook”需求:Claude 等你批准、或者回答完一直等你回话时,系统弹一个桌面通知(你切到别的窗口干别的也能收到提醒)。

打开 ~/.claude/settings.json(没有就建一个),加一个 hooks 段落:
Mac:
{ "hooks": { "Notification": [ { "matcher": "", "hooks": [ { "type": "command", "command": "osascript -e 'display notification \"Claude 在等你\" with title \"Claude Code\"'" } ] } ] }}Linux:
{ "hooks": { "Notification": [ { "matcher": "", "hooks": [ {"type": "command", "command": "notify-send 'Claude Code' '在等你'"} ] } ] }}Windows(PowerShell):
{ "hooks": { "Notification": [ { "matcher": "", "hooks": [ {"type": "command", "command": "powershell.exe -Command \"[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('Claude 在等你', 'Claude Code')\""} ] } ] }}各平台先确认通知本身能弹出来:
- Mac:
osascript的通知是借系统自带的「脚本编辑器」发的,它没开通知权限时命令会悄悄失败,系统也不提示。先在终端运行一次osascript -e 'display notification "test"',再到系统设置的「通知」里找到「脚本编辑器」,打开「允许通知」 - Linux:
notify-send需要桌面通知服务,SSH 远程登录、没有图形界面的服务器和大多数容器里都没有;提示找不到命令时,Debian / Ubuntu 装libnotify-bin - Windows:这条命令弹的是对话框,可能藏在终端窗口后面
然后测试:存盘 → 回到 Claude Code → 先按 Shift+Tab,直到状态栏显示「⏸ manual mode on」(Manual,手动确认模式)→ 让它做一个需要你批准的操作(比如改文件)→ 审批框出来后切到别的窗口,别再敲键盘,等 6 秒以上 → 系统通知会弹出来。
为什么要先切模式?新版 Claude Code 默认是 auto 模式(以状态栏显示为准,几种模式的区别见让 Claude 跑命令),改文件一般不再弹审批框,也就没有「等你批准」的通知。不切也不是完全收不到:上面 matcher 写的是空字符串,对所有通知类型都触发,Claude 回答完约 60 秒你没动,也会弹一次。
验证 Hook 是否注册成功
Section titled “验证 Hook 是否注册成功”在 Claude Code 里输入:
/hooks会列出所有已注册的 Hook 跟它们挂的事件。如果 Notification 那一行有 1 个 hook,就是配对了。
5 个最实用的真实场景
Section titled “5 个最实用的真实场景”⚠️ 先装 jq:场景 1、2、3 都用 jq(在命令行里读 JSON 的小工具)从 Hook 收到的数据里取出文件路径。Mac:
brew install jq;Ubuntu / Debian:sudo apt install jq;Windows:winget install jqlang.jq(Windows 上 Hook 默认用 Git Bash 执行,Git Bash 不自带 jq)。装完在终端跑jq --version,能显示版本号再往下。没装 jq 时这些 Hook 不会报错拦你,而是悄悄不起作用——对场景 2 这种「保护文件」的 Hook 来说,就是保护直接失效。
场景 1:Claude 改完文件,自动跑 Prettier 格式化 ✨
Section titled “场景 1:Claude 改完文件,自动跑 Prettier 格式化 ✨”
配 PostToolUse + Edit|Write 匹配器,每次 Claude 用 Edit / Write 工具改完文件,自动跑 prettier 把那个文件格式化:
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "jq -r '.tool_input.file_path' | { read -r f && npx prettier --write --ignore-unknown \"$f\"; }" } ] } ] }}说明:
matcher: "Edit|Write"—— 只在 Claude 用 Edit 工具或 Write 工具时触发jq -r '.tool_input.file_path'—— 从 Hook 收到的 JSON 输入里抽出文件路径{ read -r f && npx prettier --write --ignore-unknown "$f"; }—— 把路径读进变量f,再用双引号包着交给 prettier(代码格式化工具;npx会运行它,项目里没装时先临时下载)格式化。文件名里有空格、引号也不会拆错(很多教程用xargs,它默认按空格切分,文件名带空格就会出错)&&表示读到了路径才往下跑:没装 jq、读不到路径时就停手,不然 prettier 拿到空路径会把整个项目目录都格式化一遍;--ignore-unknown让它跳过不认识的文件类型(比如.py),免得每改一次都报错
效果:从此 Claude 改文件你完全不用管缩进 / 引号 / 分号——存盘自动整齐。
场景 2:阻止 Claude 改保护文件 🛡️
Section titled “场景 2:阻止 Claude 改保护文件 🛡️”
配 PreToolUse + Edit|Write 匹配器,Claude 准备改 .env / production.yml / 密钥文件之前自动拦截:
{ "hooks": { "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "jq -r '.tool_input.file_path' | grep -qE '\\.env|production\\.yml|secrets/' && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"该文件受保护,禁止 Claude 修改\"}}' || true" } ] } ] }}效果:Claude 一旦想用 Edit / Write 动这些文件,会立刻收到一条拒绝消息,改不进去。PreToolUse Hook 在权限模式判断之前就跑,所以 auto 模式、甚至 bypassPermissions 模式下也照样拦。不靠它「自己记得不要改」。
⚠️ 但它有两个前提,用之前一定要知道:
- 依赖 jq:没装 jq 时这条命令不会报错,而是悄悄放行,保护直接失效。配好后一定要实测:让 Claude 在
.env里加一行注释,看到被拦下才算生效 - 只管 Claude 自带的 Edit / Write 工具:Claude 用 Bash 命令(比如
sed -i、echo … > 文件)改文件时,这个 Hook 看不到
更稳的做法:真要保护文件,优先在 settings.json 里写权限规则 permissions.deny:
{ "permissions": { "deny": ["Edit(./.env)", "Edit(./production.yml)", "Edit(./secrets/**)"] }}它由 Claude Code 程序直接执行、不依赖 jq,在所有权限模式下都生效,还能拦住 Bash 里的 sed、tee 和 > 重定向(拦不住 Python、Node 脚本自己去写文件,要做到系统级隔离得开 /sandbox 沙箱)。Hook 适合用来补规则写不出来的复杂判断。
场景 3:Claude 改完文件,自动 git add ➕
Section titled “场景 3:Claude 改完文件,自动 git add ➕”{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ {"type": "command", "command": "jq -r '.tool_input.file_path' | { read -r f && git add -- \"$f\"; }"} ] } ] }}(老教程里常见的 Edit|Write|MultiEdit 可以省掉 MultiEdit:它是已经淘汰的旧工具,现在改文件就是 Edit 和 Write。)
效果:Claude 改的每一个文件都自动进 git 暂存区(staging area)。你随时跑 git status / git diff --cached 看它干了啥,review 工作流变快。
场景 4:Claude 结束对话,自动发 Telegram 总结 📱
Section titled “场景 4:Claude 结束对话,自动发 Telegram 总结 📱”配 Stop 事件:
{ "hooks": { "Stop": [ { "matcher": "", "hooks": [ { "type": "command", "command": "curl -sS -X POST \"https://api.telegram.org/bot$TELEGRAM_TOKEN/sendMessage\" -d \"chat_id=$TELEGRAM_CHAT_ID&text=Claude 这次任务结束啦\"" } ] } ] }}效果:Claude 跑完一个长任务(比如批量改 50 个文件),手机上立刻收到通知。你可以放心去做别的。
Telegram 在中国大陆无法直连。国内读者把 curl 的地址换成飞书、钉钉、企业微信群机器人的 Webhook 地址(请求格式按各家文档改),原理一样。
场景 5:每次提问自动 attach 当前 git diff 📎
Section titled “场景 5:每次提问自动 attach 当前 git diff 📎”配 UserPromptSubmit —— 每次你提问之前,自动把 git diff --stat(改动统计)塞进 context,Claude 拿到你的话自带「项目当前未提交改动」信息。UserPromptSubmit 是少数几个会把 Hook 的普通文字输出交给 Claude 看的事件之一:
{ "hooks": { "UserPromptSubmit": [ { "matcher": "", "hooks": [ { "type": "command", "command": "printf '\\n--- 当前 git diff(改动统计)---\\n%s\\n' \"$(git diff --stat)\"" } ] } ] }}效果:你说「帮我 review 这次改动」,Claude 不用再问「你改了什么」 —— 改了哪些文件、各改了多少行,已经在它眼前了。
注意 git diff --stat 只有统计、没有具体改动内容;想让它看到完整 diff,可以换成 git diff | head -200 这种有上限的写法。而且这个 Hook 每次提问都会注入,闲聊时也会带上,输出别太长。
Hook 的输入跟输出(进阶)
Section titled “Hook 的输入跟输出(进阶)”
Hook 收到什么(输入)
Section titled “Hook 收到什么(输入)”每次 Hook 触发,Claude Code 通过 stdin(喂给命令的输入)发一份 JSON 给你的 shell 命令。结构大致:
{ "session_id": "abc123", "transcript_path": "/home/you/.claude/projects/.../abc123.jsonl", "cwd": "/home/you/projects/my-site", "permission_mode": "default", "hook_event_name": "PostToolUse", "tool_name": "Edit", "tool_input": { "file_path": "/home/you/projects/my-site/.../some-file.md", "old_string": "...", "new_string": "..." }, "tool_response": "..."}你可以用 jq 解析(像场景 1 的 jq -r '.tool_input.file_path')拿想要的字段。事件名在 hook_event_name 字段里(不是 event);常用的还有 cwd(当前工作目录)、permission_mode(当前权限模式,Manual 模式在这里写作 default)、transcript_path(这次对话记录文件的路径)。
Hook 输出怎么影响 Claude(控制)
Section titled “Hook 输出怎么影响 Claude(控制)”最简单的拦截:退出码 2。 命令以 exit 2 结束,并把理由写到 stderr(错误输出,比如 echo "理由" >&2),Claude Code 就会拦下当前动作(对能拦的事件而言:PreToolUse 拦下这次工具调用,UserPromptSubmit 拦下你的提问……),理由会反馈给 Claude(UserPromptSubmit 例外:理由只显示给你看)。注意只有 2 才算拦截,exit 1 只算「Hook 自己出错了」,动作照常进行。
想更精细地控制,就让命令正常结束(exit 0),在 stdout(命令打印出来的输出)里输出一份 JSON。不同事件的格式不一样:
PreToolUse(改之前)用 hookSpecificOutput 里的 permissionDecision:
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "理由文字会反馈给 Claude" }}permissionDecision 可选值:
deny—— 阻止这次工具调用,理由发给 Claudeask—— 弹审批框让你确认allow—— 跳过审批框直接放行(但越不过你在设置里写的 deny / ask 规则)defer—— 只在claude -p这类非交互调用里有用,新手用不到
PreToolUse 以前用顶层的 "decision": "block" / "approve",这种写法已经弃用,目前还会被自动当成 deny / allow,新写的别再用。
PostToolUse、Stop、UserPromptSubmit 等事件用顶层的 decision,而且只有一个取值 "block",不写就是放行(reason 在 PostToolUse、Stop 里会交给 Claude,在 UserPromptSubmit 里只显示给你看):
{ "decision": "block", "reason": "理由文字"}普通文本输出 Claude 看得到吗? 大多数事件(PreToolUse、PostToolUse、Notification、Stop 等)看不到,普通 stdout 只写进调试日志;只有 UserPromptSubmit、SessionStart 等少数几个事件会把普通文本当成上下文交给 Claude(场景 5 用的就是这一点)。其他事件想让 Claude 看到一段话,用 JSON 里的 additionalContext 字段,或者 exit 2 加 stderr。
3 个新手最容易踩的坑
Section titled “3 个新手最容易踩的坑”❌ 坑 1:Stop Hook 让 Claude 停不下来
Section titled “❌ 坑 1:Stop Hook 让 Claude 停不下来”Stop Hook 返回 block(或者 exit 2),Claude 就会接着干活。如果你的判断条件永远满足不了(比如「测试没全过就不许停」,而测试一直过不了),它就会一轮接一轮地停不下来。Claude Code 有个保险:连续拦了 8 次后会强行让它停下,但前面这 8 轮的 token 已经花出去了。
修正:在脚本开头检查输入里的 stop_hook_active 字段——它是 true 说明这一轮已经是被 Hook 拦回来接着干的,直接放行:
#!/bin/bashINPUT=$(cat)if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then exit 0 # 已经拦过一次了,这次放 Claude 停下fi# ……下面写你的检查逻辑,比如测试没过就把理由写到 stderr 再 exit 2顺带澄清一个常见误会:Hook 跑的是 shell 命令,调不动 Claude 的 Write 工具;Hook 自己用 prettier、sed 改文件,也不会再次触发 PostToolUse(只有 Claude 的工具调用才会触发)。所以场景 1、3 那种「改完文件再格式化」不会自己套自己。
❌ 坑 2:Hook 安全漏洞
Section titled “❌ 坑 2:Hook 安全漏洞”Hook 是你自己写的 shell 命令——但它处理 Claude 发来的 JSON。如果 Claude 收到的 file_path 是恶意构造的(比如 ; rm -rf ~ #),拼接到命令里就完了。
修正:
- 用
jq -r把字段读出来后,存进变量、用双引号包住再用(写"$f",不要写$f)。jq -r只是去掉 JSON 的引号,不会替你做任何 shell 转义 - 别把字段直接拼进命令字符串,也别指望
xargs兜底:它默认按空格和引号切分,文件名带空格就会拆错。上面的场景都用{ read -r f && 命令 "$f"; }这种写法 - 检查路径里有没有
..,防止跳出项目目录 - 涉及删除 / 执行的 hook,只在沙箱环境跑
❌ 坑 3:Hook 输出太多,白白占上下文
Section titled “❌ 坑 3:Hook 输出太多,白白占上下文”只有 UserPromptSubmit、SessionStart 等少数事件的普通输出会进 Claude 的上下文,而且每次触发都进——像场景 5 那样每次提问都注入,输出越长越费 token。Claude Code 给 Hook 输出设了 1 万字符的上限,超出的部分会存成文件、只给 Claude 前 2000 字的预览,所以撑爆倒不至于,但截断以后 Claude 可能恰好看不到你最想让它看的那部分。
修正:会进上下文的 Hook 输出要精简:用 head -n 20、git diff --stat 这类有上限的写法;需要完整日志就另写到文件里。
一份完整的 settings.json 示例
Section titled “一份完整的 settings.json 示例”下面是洁柔家 Claude Code 用过的一份真实 hooks 配置(脱敏版,并按 2026-09 的官方格式更新过,你直接抄;通知渠道当时走 Telegram,换成你在用的任何带 API 的通知服务——ntfy、Bark、飞书机器人——原理一样):
{ "hooks": { "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "jq -r '.tool_input.file_path' | grep -qE '\\.env|secrets/|production' && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"保护文件不许动\"}}' || true" } ] } ], "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ {"type": "command", "command": "jq -r '.tool_input.file_path' | { read -r f && npx prettier --write \"$f\" 2>/dev/null; git add -- \"$f\"; }"} ] } ], "Notification": [ { "matcher": "", "hooks": [ {"type": "command", "command": "osascript -e 'display notification \"Claude 在等你\" with title \"Claude Code\"'"} ] } ], "Stop": [ { "matcher": "", "hooks": [ {"type": "command", "command": "curl -sS -X POST \"https://api.telegram.org/bot$TG_TOKEN/sendMessage\" --data-urlencode 'text=Claude 任务结束' -d \"chat_id=$TG_CHAT\" > /dev/null"} ] } ] }}这一份 4 个 Hook 一起跑(前两个依赖 jq;Notification 那条是 Mac 的写法,Linux / Windows 换成前面对应的命令):
- ✅ 改保护文件 → 自动拦截(记得实测一次;真要保护,再加上场景 2 说的
permissions.deny规则) - ✅ 改普通文件 → 自动格式化 + git add
- ✅ Claude 等你 → 桌面通知
- ✅ 任务结束 → 手机 Telegram 通知
装好之后, Claude Code 用起来跟之前完全不一样 —— 自动化层全开。
读完这一篇你应该会:
- 理解 Hook = Claude Code 生命周期闸阀
- 配第一个 桌面通知 Hook
- 知道 5 个实用场景(自动格式化 / 阻止改保护文件 / git add / 手机通知 / 注入 git diff)
接下来「Claude Code 进阶」板块还有:
→ MCP(外部服务连接器):给 Claude 接上外部工具
→ 自定义 Slash Command:把团队规范变成 /xxx
→ Subagents(子智能体):多个 Claude 并行干活
→ Git Worktree:多个 Claude 各改各的不打架
想及时看到更新,可以收藏 niuxue.org 主页。