1. 那个瞬间 · 当你发现"求 AI 帮忙"根本不够
每个用 AI 的项目都会走到一个节点,你不再指望模型的一片好心。你在 AGENTS.md 里写了让它格式化代码、别碰敏感文件、跑测试。90% 的时候它都做到了。但剩下那 10% 恰恰就是咬你一口的那一次:它改了生产环境 .env 的那一次,它跳过格式化、把提交搞脏的那一次,它对着错误的文件夹执行 rm -rf 只因为"看起来像临时的"那一次。
hooks 的瞬间就是你想到:"要是这件事不取决于模型自己决定做对呢?要是它就是会发生、总是发生、不取决于它的判断呢?"。这正是 Claude Code 官方文档在 hooks 页面正中央摆出的承诺:它们给了对 agent 行为的 "确定性控制","保证某些动作总是发生,而不是取决于 LLM 是否选择执行它们"(忠实翻译自英文原文)。这句话就是整个资源的核心论点。
2. 痛点 · 它从哪来,为什么会发生
痛点不是一场末日,而是一种侵蚀。你让 AI 去做某件"总是"要做的事,可它有时不做,因为 LLM 不是确定性的:每一轮它都重新决定,而在一个塞满上下文的轮次里,你那句"记得格式化"的提醒可能被上千个别的 token 埋掉。不是 AI 不听话——而是你在向一个概率系统索要一致性。这就像求一个聪明但心不在焉的人永远别忘了关煤气阀:大多数晚上他都关了,而这恰恰是你放松警惕的原因。
- 你在每次会话里重复同一条指令("格式化"、"别碰 X"、"跑测试"),可它有时还是跳过。
- 你事后才发现 AI 改了一个它绝对不该碰的文件:
.env、某个package-lock.json、.git/里的某样东西。 - 一次提交带着不一致的格式混了进来,把整个团队的 diff 搞脏了。
- 你有点不敢给 AI 大范围的权限,因为没有一张不依赖它自己的安全网。
- 你的 AGENTS.md 里用散文写着关键规则,而"写下来"和"有保证"根本不是一回事。
这里就是让这个资源进阶的角度:如果你已经读过关于为 agent 写一份好合同(AGENTS.md)的内容,那么 hooks 就是下一级台阶。你从"写合同"(声明式:告诉它你要什么)跨到"让合同自己执行"(确定性:机器替你落实它)。这是一部写在纸上的法律,和一个站在街角的警察之间的区别。
3. hook 是什么(不玩虚的,一句话)
一个 hook 是 Claude Code 在其生命周期某个具体节点自动执行的一条 shell 命令。你在一个 settings.json 文件里声明它一次;从那以后,每当到达生命周期的那个节点,你的命令就会运行。它不是由模型凭判断触发的:它由事件触发。魔法就在这里——它是确定的,不容商量。
4. 生命周期事件(你挂钩的那些节点)
官方文档列出了许多生命周期事件——几乎 agent 工作的每个时刻都有一个。别去背数量或整个列表:理解这个思路,记住你 95% 的时候真正会用到的那几个。以下就是它们,连同官方文档里原样出现的真实名称:
- `SessionStart` — 当一个会话启动或恢复时。适合注入新鲜的上下文(比如你的约定、最近的提交)。它输出到 stdout 的内容会被加入 Claude 的上下文。
- `UserPromptSubmit` — 就在你发送一个 prompt、Claude 还没处理它之前。你可以添加上下文,甚至阻止这个 prompt。
- `PreToolUse` — 在一个工具执行之前。这个能阻止动作。它是你的保镖:在这里你拦下一个
rm -rf,或对受保护文件的编辑。 - `PostToolUse` — 在一个工具成功之后。在这里你格式化刚编辑的文件、跑一个 lint,随你想干什么。
- `Notification` — 当 Claude 需要你时(请求权限或正在等待)。用来发一个桌面通知再合适不过。
- `Stop` / `SubagentStop` — 当 Claude(或某个子 agent)回答完毕时。适合在把这一轮判定为结束前做一次最终检查。
- `PreCompact`(以及带
compactmatcher 的SessionStart)— 围绕上下文压缩,好在记忆被压缩时不丢掉重要的东西。 - `SessionEnd` — 当会话结束时。用来清理临时文件、收尾。
Pre。如果你想对已经发生的事做出反应(格式化、提醒、记录),挂到 Post。5. 一个 hook 怎么声明(JSON 的解剖)
每个 hook 都住在你 settings.json 里的一个 "hooks" 块中。结构永远是同一个套娃:事件名 → 一个 matcher(应用到什么上)→ 一个 hooks 列表,里面是 type: "command" 和要运行的 command。看一遍你就再也忘不掉:
{
"hooks": {
"PostToolUse": [ // 1. 生命周期的 EVENTO(事件)
{
"matcher": "Edit|Write", // 2. 应用到 QUÉ(哪些工具)
"hooks": [
{
"type": "command", // 3. 这是一条 shell 命令
"command": "...tu comando..." // 4. 发生时 QUÉ(运行什么)
}
]
}
]
}
}PreToolUse、PostToolUse)它按工具名过滤:"Edit|Write" 意思是"只在用 Edit 或 Write 时","Bash" 只针对 shell 命令。一个空的 matcher("")总是触发,对一切生效。竖线 | 分隔备选项(在较新版本的 Claude Code 里逗号也可以:"Edit, Write" 是等价的)。6. 一个 hook 住在哪里(以及位置为什么重要)
你放 hook 的位置定义了它的作用范围。这很关键:一个项目级 hook 会跟你的团队共享(它被提交进仓库);一个全局的只属于你。官方表格总结了每样东西该放哪:
- `~/.claude/settings.json` — 全局,应用到你所有的项目。不共享,是你机器上的。这里放你的个人 hooks(比如桌面通知)。
- `.claude/settings.json`(在仓库里)— 只针对那个项目,而且可以提交:你整个团队都继承这条规则。这里放项目规则(格式化、受保护文件)。
- `.claude/settings.local.json` — 只针对那个项目,私有:Claude Code 在创建它时会加进 gitignore。用于你不想共享的本地设置。
.claude/settings.json 并被提交。这样合同就不再住在某一个人的脑子里,而是跟着代码一起走。这才是真正的力量:纪律成了仓库的一部分,而不是某人得记住的便利贴。7. 三个复制粘贴、字字千金的 hook
理论够了。这里有三个真实的 hook,取自官方文档,拿来即贴。从今天最让你痛的那个开始。
a) 每次编辑后自动格式化(PostToolUse)
经典款。每当 Claude 编辑或写入一个文件,自动过一遍 Prettier。再也不会有格式不一致的提交了。放进你项目的 .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}jq -r '.tool_input.file_path' 从那个 JSON 里提取被编辑文件的路径,然后 xargs npx prettier --write 把它传给 Prettier。jq 是一个命令行 JSON 阅读器——在 macOS 上用 brew install jq 安装,在 Debian/Ubuntu 上用 apt-get install jq。b) 阻止像 rm -rf 这样的危险命令(PreToolUse)
这个能帮你摆脱恐惧。一个 PreToolUse hook,在每条 Bash 命令执行之前审查它,如果含有破坏性的东西就否决它。这里有一个文档讲得极清楚的技术细节很重要:通过以退出码 2 结束来阻止,而你写到 stderr 的内容会作为解释传给 Claude,让它自我纠正。先看脚本:
#!/bin/bash # .claude/hooks/block-rm-rf.sh INPUT=$(cat) COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command') if echo "$COMMAND" | grep -q "rm -rf"; then echo "Bloqueado: 'rm -rf' no está permitido. Borra rutas concretas, no recursivo-forzado." >&2 exit 2 # exit 2 = bloquea la accion; stderr le llega a Claude como feedback fi exit 0 # exit 0 = sin objecion; sigue el flujo normal de permisos
让它可执行,并在你的 settings 里指向这个脚本来注册它:
chmod +x .claude/hooks/block-rm-rf.sh
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
}
]
}
]
}
}grep -q "rm -rf" 能抓住那个显而易见的情况,但它会漏掉 rm -fr、带奇怪空格的 rm -rf,或者 rm --recursive --force。它当一张看得见的网、当给 AI 的教育性反馈是有用的,但别把它当成盔甲。文档本身直言不讳:hooks 的过滤是尽力而为(best-effort)且"失败即放行"(如果它无法解析命令,就让它过),所以对于一条谁都不能绕过的硬禁令,用权限系统(permissions.deny),而不是一个 hook。正确的模式是:hook 用来反应和提醒;permission rules 用来做真正的锁。PreToolUse 会否决该工具,即使在 `bypassPermissions` 下或带着 `--dangerously-skip-permissions`。也就是说:哪怕你(或 AI)已经把所有权限护栏都放下了,你的阻止 hook 依然屹立。这条规则是不对称的,而且是故意这么设计的:hooks 可以收紧策略,永远无法把它放松到超出权限规则允许的程度。一个阻止 hook 是一条连你自己都不会意外绕过的规则。c) 当 AI 需要你时发桌面通知(Notification)
这样你就不用一直盯着终端了。当 Claude 完成或需要你的权限时,弹出一个桌面通知,你就可以去做别的事。放进你全局的 ~/.claude/settings.json(macOS 用 osascript 的示例):
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code necesita tu atención\" with title \"Claude Code\"'"
}
]
}
]
}
}notify-send 'Claude Code' 'Claude Code necesita tu atención';在 Windows(PowerShell) 上用 System.Windows.Forms 的一个 MessageBox。JSON 的结构完全一样——只有 command 那一行变。指南本身记录的一个 macOS 小技巧:如果通知没出现,先跑一次 osascript -e 'display notification "test"',然后在"系统设置 → 通知"里给 Script Editor 授予权限。8. 习惯 · 什么时候该想到 hook(什么时候不该)
hooks 不是万能的。心里的规则很简单:如果某件事必须总是发生、机械地、无需判断,那就是一个 hook。如果某件事需要逐案的判断,别用一个确定性的 hook 去硬来——让模型来决定(或用文档同样支持的 prompt/agent 类型 hook,不过那是另一个层级的东西)。
- 每当你发现自己在 AGENTS.md 里第三次写下"记得……"。如果你在重复它,就把它变成 hook。
- 当有一个动作绝对不该发生时(碰
.env、递归删除、写入.git/):那是一个阻止型PreToolUse——由一条权限规则做硬锁托底。 - 当有一个动作在编辑后总该发生时(格式化、排序 import、跑一个 lint):那是一个
PostToolUse。 - 当你想让某个关键上下文在每次压缩后被重新注入,好让 AI 不会在会话中途"忘记"你的约定时。
PostToolUse:它无法撤销已经发生的事——它只反应。要阻止,永远用 PreToolUse。往一个 hook 里塞太多复杂逻辑会让它变脆;让它们保持简短、单一目的。9. 使用协议 · 搭起你第一个有用的 hook
这里有个实用细节:文档本身建议你把 hook 描述给 Claude,让它替你写出来。但要写得好——而且安全——最好给它一个结构化的委托,而不是一句"给我弄个 hook"。这就是本资源的那个主 prompt:唯一一个、强大的、把安全刹车都装好的、来设计你第一个 hook 的 prompt。
我想搭起我第一个 Claude Code hook,来自动化我工作流里的一条规则。我想要永远保证的那条规则是:[DESCRIBE LA REGLA,例如:"用 prettier 格式化你编辑的每个文件" / "阻止任何以递归方式删除文件的命令" / "禁止编辑 .env 或 .git/ 里的任何东西" / "完成时用通知提醒我"]。 在写任何东西之前,用清晰的语言(我不会编程)回答这些,帮我把它设计好: 1. 这条规则对应生命周期里正确的 EVENTO(事件)。如果规则是要 IMPEDIR(阻止)某事,它必须是 PreToolUse(发生在之前且能阻止)。如果是对已经做完的事做出 REACCIONAR(反应)(格式化、提醒、记录),它必须是 PostToolUse 或 Notification。告诉我你选哪个以及为什么。 2. 精确的 MATCHER(应用到哪些工具:Edit|Write、Bash,或空表示全部)以及为什么是这个而不是别的。让它尽可能地窄:一个过宽的 matcher 会在不该触发的地方触发 hook。 3. settings.json 该住的 LUGAR(位置): - 如果这条规则必须由这个仓库里整个团队遵守 → .claude/settings.json(会被提交)。 - 如果只针对我的机器和我所有的项目 → ~/.claude/settings.json。 - 如果是本地且私有的 → .claude/settings.local.json。 告诉我哪个以及为什么。 4. 完整的 JSON 块,拿来即贴,hook 已经装好。如果它需要一个单独的脚本(带 PreToolUse 的阻止里很典型),也给我完整的 .sh 脚本,并提醒我用 chmod +x 让它可执行。 5. 如果它是一个阻止型 hook:用 exit 2 加一条清晰的 stderr 消息,向我解释为什么被阻止,好让你自己(Claude)收到理由并自我纠正。别让它静默。而且要 AVÍSAME(提醒我)这条规则是否还值得配一条权限规则(permissions.deny)当硬锁,因为一个 hook 里的 grep 是尽力而为、可以被绕过的。 6. 安全,必填:用一句话向我解释这个 hook 会跑 QUÉ(什么)shell 命令,并向我确认它不做任何破坏性的事、也不把数据发到任何地方。提醒我一个 hook 是用我的权限跑 shell 的,我只该保存我理解的命令。 7. 我怎么测试它:告诉我怎么验证 hook 已注册(Claude Code 里的 /hooks 命令),以及一种在不破坏任何真实东西的前提下测试它确实会触发的方法。 先只给我看 SOLO(仅)设计(事件、matcher、位置),然后是 JSON,如果需要再给脚本,最后是安全解释和测试。别现在就改我的 settings.json:我想在自己贴之前先审查这个块。
注意这个模式:先设计,再审查安全,然后由你来贴块。你绝不让一个 hook 在你还没理解它之前就出现在你的配置里。这跟保护任何强大自动化的两步纪律是一样的:机器提议,你批准。
10. 最省事的路径 · 什么通过聊天做,什么通过文件做
为了别把你绕晕,这就是你可以通过聊天委托给 AI 的,以及依然归你自己管的:
- 如果你把规则描述给它,替你写出完整的 hook JSON 块(官方文档明确推荐这么做)。
- 替你生成阻止型 hook 的
.sh脚本,带上它的jq、它的exit 2和它给 stderr 的消息。 - 向你解释哪个事件和哪个 matcher 契合你的规则,以及它该住在哪个
settings.json里。 - 提醒你安装步骤:脚本的
chmod +x,缺jq的话brew install jq。
- 把块贴进你的
settings.json——在读过并理解之后。安全网不能是你没审查过的代码。 - 选择位置(共享仓库 vs. 全局 vs. 本地):这决定谁继承这条规则。
- 确认 hook 的命令是安全的(它用你的权限跑 shell:如果你不理解,就不保存)。
- 在 Claude Code 里用 `/hooks` 验证它已注册,并测试它在不破坏任何真实东西的前提下会触发。
总结 · 你的 hooks 清单
- 我选了正确的事件:
PreToolUse用于阻止,PostToolUse/Notification用于反应。 - matcher 尽可能地窄(不在不该触发的地方触发)。
- 它在正确的位置:共享仓库、你自己的全局,或本地私有。
- 如果它阻止,用 `exit 2` 加一条清晰的 stderr 消息让 AI 自我纠正——如果是硬禁令,我用一条权限规则给它托底。
- 我理解它运行的命令(用我的权限跑 shell):我没保存任何我没审查过的东西。
- 我用 `/hooks` 验证过它,并测试了它在不破坏任何真实东西的前提下会触发。
- 我把规则从我 AGENTS.md 的散文里拿了出来,变成了总是会发生的东西。
PreToolUse。这是一个 hook 的同一种哲学,只不过应用到了一整个团队而不是一个终端上:让质量不是某人记得的一个承诺,而是传送带上一个绕不过去的传感器。你把规则放一次;系统永远替你落实它。