钩子:确定性自动化
钩子是 Claude Code 在其生命周期的指定节点自动运行的 shell 命令。权限决定某个操作是否被允许,而钩子让你在其前后运行确定性逻辑——格式化、校验、日志、关卡。它们是把行为从“请记得做”变成必然发生的方式。
- 何时该用钩子而不是一条指令或一项权限
- 钩子如何接线:事件、匹配器,以及 stdin 上的 JSON 负载
- 钩子拦截一个操作的两种方式——退出码 2 与 stdout 上的 JSON
- 把快速、安全的钩子与迟钝、静默的钩子区分开来的良好实践与常见错误
何时该用钩子
当你希望某个行为是必然发生的、而不仅仅是被请求时,就该用钩子。每个常见任务都对应一个生命周期事件:
- 每次文件编辑后自动格式化 / lint(
PostToolUse)。 - 在某个违规操作运行之前拦截它(
PreToolUse)。 - 会话结束或任务完成时通知或记录(
Stop)。 - 在会话开始时注入上下文。
钩子事件速览
按 Enter 或空格键翻转卡片。使用左右方向键在卡片之间切换。已显示术语。1 / 4
它们如何工作
你在 settings.json 里注册钩子,匹配一个事件(通常还配上一个工具匹配器)。当事件触发时,Claude 运行你的命令,并在 stdin 上传入一个 JSON 负载(工具名、它的输入、会话)。你命令的退出码和输出决定接下来发生什么。
Guided walkthrough1 of 4
- 在 settings.json 里,把钩子注册到你关心的生命周期事件下——例如 PostToolUse。
- 添加一个工具匹配器,让钩子只在相关工具上触发,例如对文件编辑使用 matcher "Edit|Write"。
- 当事件触发时,Claude 运行你的命令,并在 stdin 上管道传入一个 JSON 负载——工具名、它的输入、会话。
- 你命令的退出码和输出决定结果:让操作继续、运行你的逻辑,或拦截它。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
]
}
}
上面这个钩子从 stdin 的 JSON 里读出被编辑文件的路径(.tool_input.file_path)并对其格式化。不要假设某个环境变量持有该路径——从 stdin 读取它。 像 ${CLAUDE_PROJECT_DIR} 这样有用的路径占位符是可用的,用于定位脚本。
钩子如何拦截
有两种方式,取决于事件:
- 退出码 2 — 钩子让该操作失败,它写到 stderr 的任何内容会成为 Claude 看到的消息。简单,且对命令钩子有效。
- stdout 上的 JSON(退出 0) — 返回一个结构化决策。对
PreToolUse来说,那是一个值为deny的permissionDecision;对PostToolUse/Stop/等等来说,它是{"decision": "block", "reason": "…"}。
下面这个脚本是 Bash 工具上的一个 PreToolUse 钩子。从头读到尾:它从 stdin 里拉出命令,如果看起来具有破坏性,就把一个理由写到 stderr 并以 2 退出来拦截。
#!/usr/bin/env bash
# PreToolUse hook on the Bash tool: refuse to delete things.
command=$(jq -r '.tool_input.command' < /dev/stdin)
if [[ "$command" == rm\ * || "$command" == *"rm -rf"* ]]; then
echo "Blocked: destructive 'rm' is not allowed by policy." >&2
exit 2
fi
exit 0
心智模型
一个 PreToolUse 钩子在操作之前运行,可以拦截它;一个 PostToolUse 钩子在操作成功之后运行,对结果做出反应。
良好实践
- 让钩子又快又幂等——它们会频繁运行。
- 对真正的问题大声报错,但不要因为表面问题就拦截。
- 把钩子输出当作给 Claude 的反馈——一条清晰的消息能帮它自我纠正。
- 钩子以你 shell 的权限运行——对任何不是你自己写的钩子都要审查(审查第三方代码)。
常见错误
- 从环境变量读取文件路径。 路径在 stdin 的 JSON 里(
.tool_input.file_path),而不在$CLAUDE_FILE_PATH。把 stdin 通过jq管道传入。 - 静默拦截。 如果一个
PreToolUse钩子以退出码 2 退出却在 stderr 上什么都没写,Claude 被拦住了却不知道为什么,也无法适应。永远写一个清晰的理由。 - 慢钩子。 一个
PostToolUse钩子在每一次匹配的编辑后都会运行。一个 3 秒的 linter 会让整个会话感觉迟钝——保持钩子快速,且最好只对发生变化的内容动作。 - 过于宽泛的匹配器。
matcher: ".*"会在每个工具上触发。用一个精确的名字、一个Edit|Write列表,或每个处理器的if字段(例如"if": "Bash(git push *)")来收窄。 - 信任不是你自己写的钩子。 一个钩子会以你的权限运行任意 shell。先审查任何来自插件或模板的钩子——见审查第三方代码。
- 一个钩子会以你的权限运行任意 shell——绝不要在没有先读过它的情况下接入任何来自插件或模板的钩子。
可复制粘贴的起始模板见钩子与 settings.json 配方。
自动格式化被编辑的文件(在 Edit|Write 上的 PostToolUse)
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }
]
}
]
}
}自我检测
0/3- 钩子让行为必然发生,而非仅被请求——它们在操作前后运行确定性逻辑,而权限只能允许或拒绝这些操作。
- 在 settings.json 里针对一个事件加一个匹配器注册钩子;Claude 在 stdin 上管道传入一个 JSON 负载,并读取你的退出码和输出。
- 从 stdin 读取文件路径(.tool_input.file_path)——而不是从某个环境变量。
- 用退出码 2 拦截(stderr 会成为消息),或用 stdout 上的结构化 JSON 拦截(退出 0);永远包含一个清晰的理由。
- 保持钩子快速、幂等且匹配范围收窄——并审查任何不是你自己写的钩子,因为它会以你 shell 的权限运行。
下一步
- settings.json · 权限
- 技能——专长与自动化的区别
- 加固自主运行