用 Hooks 自动化
在 Claude Code 生命周期的特定节点运行你的命令:配置第一个 Hook、常用自动化示例、输入输出与退出码、matcher、prompt/agent/HTTP Hook 与排障。
Hooks 是用户定义的 shell 命令,Claude Code 在生命周期的特定节点运行它们。这给了你确定性的控制:某些动作总会发生,而不是依赖 LLM 去选择是否执行。用 Hook 来强制项目规则、自动化重复任务、把 Claude Code 和你已有的工具集成。
对于需要判断而非确定性规则的决策,也可以用基于提示词的 Hook 或基于智能体的 Hook,让 Claude 模型来评估条件。完整的事件结构、JSON 输入输出格式、异步 Hook 和 MCP 工具 Hook 等高级功能,见Hooks 参考。
配置你的第一个 Hook
创建 Hook 就是在设置文件里添加 hooks 块。下面这个例子创建桌面通知 Hook,当 Claude 在等你输入时提醒你,不用一直盯着终端。
打开 ~/.claude/settings.json(不存在就创建),添加一个 Notification Hook(下面用 macOS 的 osascript):
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}如果设置文件里已经有 hooks 键,要把 Notification 作为已有事件键的同级添加,而不是替换整个对象——每个事件名都是单个 hooks 对象里的一个键。也可以直接让 Claude 替你写 Hook。
输入 /hooks 打开 Hook 浏览器,会看到所有可用的 Hook 事件,已配置 Hook 的事件旁边有计数;选中 Notification 可以确认新 Hook 出现在列表里,并查看事件、matcher、类型、来源文件和命令。/hooks 菜单是只读的,要添加、修改或删除 Hook,直接编辑设置 JSON 或让 Claude 来改。
能自动化什么
Hooks 让你在关键节点运行代码:编辑后格式化文件、在命令执行前拦截、Claude 需要输入时发通知、会话开始时注入上下文等等。
编辑后自动格式化代码
在 Claude 编辑每个文件后自动运行 Prettier,让格式保持一致。这个 Hook 使用 PostToolUse 事件和 Edit|Write 匹配器,所以只在文件编辑类工具之后运行。命令用 jq 提取被编辑的文件路径并交给 Prettier。加到项目根目录的 .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}Hook 成功时对话里不会显示任何东西;要确认它运行过,看被编辑的文件是否被重新格式化。注意:这页的 Bash 示例用 jq 解析 JSON,macOS 上用 brew install jq 安装,Debian/Ubuntu 用 apt-get install jq。想不管文件怎么变化(包括被 Bash 命令改写)都重新格式化,改用 FileChanged Hook。
阻止编辑受保护的文件
阻止 Claude 修改 .env、package-lock.json 或 .git/ 里的任何东西。Claude 会收到说明为什么编辑被阻止的反馈,所以能调整做法。这个例子用一个单独的脚本文件:检查目标文件路径是否匹配受保护的模式,用退出码 2 阻止编辑。保存为 .claude/hooks/protect-files.sh:
#!/bin/bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# 规范化 Windows 的反斜杠分隔符,让下面的模式能匹配
FILE_PATH="${FILE_PATH//\\//}"
PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
exit 2
fi
done
exit 0Hook 脚本必须可执行:
chmod +x .claude/hooks/protect-files.sh然后在 .claude/settings.json 里加一个 PreToolUse Hook,在任何 Edit 或 Write 工具调用前运行这个脚本:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}其他官方示例包括:压缩后重新注入上下文、审计配置变更、目录或文件变化时重新加载环境、自动批准特定权限提示,见官方原文。
Hooks 如何工作
Claude Code 在生命周期的特定节点触发 Hook 事件。事件触发时,所有匹配的 Hook 并行运行。主要事件:
| 事件 | 何时触发 |
|---|---|
SessionStart | 会话开始或恢复时 |
UserPromptSubmit | 你提交提示后、Claude 处理之前 |
PreToolUse | 工具调用执行前,可以阻止它 |
PermissionRequest | 工具调用需要权限决定时 |
PostToolUse | 工具调用成功后 |
PostToolUseFailure | 工具调用失败后 |
Notification | Claude Code 发送通知时 |
SubagentStart / SubagentStop | 子智能体被派生 / 完成时 |
Stop | Claude 完成回复时 |
InstructionsLoaded | CLAUDE.md 或 .claude/rules/*.md 文件被加载进上下文时 |
ConfigChange | 会话期间配置文件变化时 |
CwdChanged | 工作目录变化时(如 Claude 执行了 cd),适合配合 direnv 做响应式环境管理 |
FileChanged | 被监视的文件在磁盘上变化时,matcher 指定要监视哪些文件名 |
PreCompact / PostCompact | 上下文压缩前 / 后 |
SessionEnd | 会话结束时 |
此外还有 Setup、UserPromptExpansion、PermissionDenied、PostToolBatch、TaskCreated、TaskCompleted、StopFailure、TeammateIdle、WorktreeCreate、WorktreeRemove、PreModelSwitch、PostModelSwitch、Elicitation 等事件,完整清单见参考。
每个 Hook 有一个决定其运行方式的 type。多数用 "type": "command"(运行 shell 命令)。另有四种:
"type": "http":把事件数据 POST 到一个 URL"type": "mcp_tool":调用已配置 MCP 服务器上的工具"type": "prompt":单轮 LLM 评估"type": "agent":带工具访问的多轮验证(实验性,可能变化)
合并多个 Hook 的结果
多个 Hook 匹配同一事件时,每个 Hook 的命令都会运行完毕后 Claude Code 才合并结果。一个 Hook 返回 deny 并不会阻止同级 Hook 的执行,所以不要依赖某个 Hook 的 deny 来抑制另一个 Hook 的副作用。合并 PreToolUse 的权限决定时,采用最严格的答案,顺序为 deny、defer、ask、allow;additionalContext 的文本会从每个 Hook 保留并一起交给 Claude。
读取输入、返回输出
Hook 通过 stdin、stdout、stderr 和退出码与 Claude Code 通信。事件触发时,Claude Code 把事件相关数据作为 JSON 传到脚本的 stdin。每个事件都包含 session_id(会话唯一 ID)和 cwd(事件触发时的工作目录)等通用字段,另外每种事件带各自的数据。Claude 运行 Bash 命令时,PreToolUse Hook 在 stdin 收到:
{
"session_id": "abc123",
"cwd": "/Users/sarah/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}脚本通过输出和退出码告诉 Claude Code 下一步做什么。下面的 PreToolUse Hook 阻止一条命令:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q "drop table"; then
echo "Blocked: dropping tables is not allowed" >&2 # stderr 成为 Claude 的反馈
exit 2 # exit 2 = 阻止这个动作
fi
exit 0 # exit 0 = 没有决定;走正常的权限流程退出码决定接下来发生什么:
- 退出 0:你的 Hook 通过退出码表示没有异议。对
PreToolUse来说这并不等于批准工具调用,正常的权限流程仍然适用;对UserPromptSubmit、SessionStart等,Claude Code 会把 stdout 作为纯文本加入 Claude 的上下文 - 退出 2:Claude Code 阻止这个动作。把原因写到 stderr,原因的去向取决于事件:有些事件把它作为反馈交给 Claude 让它调整,有些显示给用户。部分事件无法被阻止
- 任何其他退出码:对多数事件,结果取决于 Hook 往 stdout 打印了什么:能通过校验的 JSON 对象则只由 JSON 决定结果;无法解析或校验失败是非阻塞错误;纯文本或空输出则动作继续,作为非阻塞错误报告
结构化 JSON 输出:退出码只能让你阻止或保持沉默。想要更多控制,退出 0 并往 stdout 打印 JSON 对象。每个 Hook 选一种方式:退出 2 加 stderr 消息,或退出 0 加 JSON。例如 PreToolUse Hook 可以拒绝工具调用并告诉 Claude 原因,或升级给用户审批:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}PreToolUse 上各个 permissionDecision 值:"allow" 跳过交互式权限提示(拒绝和询问规则,包括企业托管的拒绝列表,仍然适用);"deny" 取消工具调用并把原因发给 Claude;"ask" 照常向用户显示权限提示。其他事件用不同的决策模式,例如 PostToolUse 和 Stop Hook 用顶层的 decision: "block" 字段。向 Claude 上下文注入文本用 hookSpecificOutput.additionalContext,必须嵌套在 hookSpecificOutput 里,放在 JSON 顶层会被静默忽略:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Current branch: release-42. Deploy freeze until Friday."
}
}用 matcher 过滤 Hook
没有 matcher 时,Hook 在其事件的每次发生时都会触发。matcher 让你缩小范围。例如想只在文件编辑后(而不是每次工具调用后)运行格式化器:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "prettier --write ..." }]
}
]
}
}"Edit|Write" 只在 Claude 用 Edit 或 Write 工具时触发,不会在 Bash、Read 等其他工具时触发,matcher 区分大小写。逗号同样分隔备选项,"Edit, Write" 等价。注意 Claude 也可以通过运行 shell 命令创建或修改文件,如果你的 Hook 必须看到每一次文件变化(比如合规扫描或审计日志),再加一个每回合扫描一次工作树的 Stop Hook,或者同时匹配 Bash|PowerShell。
每种事件类型匹配不同的字段:
| 事件 | matcher 过滤什么 | 示例值 |
|---|---|---|
PreToolUse、PostToolUse、PostToolUseFailure、PermissionRequest、PermissionDenied | 工具名 | Bash、Edit|Write、mcp__.* |
SessionStart | 会话如何开始 | startup、resume、clear、compact、fork |
SessionEnd | 会话为何结束 | clear、resume、logout、prompt_input_exit、other |
Notification | 通知类型 | permission_prompt、idle_prompt、auth_success 等 |
SubagentStart / SubagentStop | 智能体类型 | general-purpose、Explore、Plan 或自定义智能体名 |
PreCompact、PostCompact | 触发压缩的原因 | manual、auto |
ConfigChange | 配置来源 | user_settings、project_settings、local_settings、policy_settings、skills |
用 if 字段按工具名和参数过滤:if 字段使用权限规则语法同时按工具名和参数过滤,这样只有工具调用匹配时才会启动 Hook 进程。比如只在 Claude 用 git 命令(而不是所有 Bash 命令)时运行 Hook:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}
]
}
]
}
}对 Bash,每个子命令都会被检查(npm test && git push 会匹配 Bash(git *),$() 和反引号里的命令也会被检查)。当 Claude Code 无法判断 Bash 输入运行了哪些命令时,不管模式如何都会运行你的 Hook。if 只对工具事件有效(PreToolUse、PostToolUse、PostToolUseFailure、PermissionRequest、PermissionDenied),加到其他事件上会让 Hook 不运行。
Hook 配置放在哪
| 位置 | 范围 | 是否可共享 |
|---|---|---|
~/.claude/settings.json | 你的所有项目 | 否,仅本机 |
.claude/settings.json | 单个项目 | 是,可提交进仓库 |
.claude/settings.local.json | 单个项目 | 否,Claude Code 保存设置时会被 gitignore |
| 托管策略设置 | 整个组织 | 是,由管理员控制 |
插件的 hooks/hooks.json | 插件启用时 | 是,随插件打包 |
| Skill 前置信息 | Skill 被调用后会话的剩余时间 | 是,定义在 Skill 文件里 |
| 子智能体前置信息 | 该子智能体运行期间 | 是,定义在子智能体文件里 |
在设置文件里设 "disableAllHooks": true 可以禁用 Hook(托管设置里配置的 Hook 仍会运行,除非该设置也出现在托管层)。在 Claude Code 运行时直接编辑设置文件,文件监视器通常会自动识别 Hook 变化。
基于提示词的 Hook
对需要判断而不是确定性规则的决策,用 type: "prompt" Hook:Claude Code 把你的提示词和 Hook 的输入数据发给 Claude 模型来做决定(可以用 model 字段指定别的模型)。模型的唯一任务是以 JSON 返回决定:"ok": true 动作继续;"ok": false 的效果取决于事件:对 Stop 和 SubagentStop,reason 会反馈给 Claude 让它继续工作;对 PreToolUse,工具调用被拒绝。
这个例子用 Stop Hook 问模型是否所有请求的任务都已完成;如果条件未满足,模型返回 "ok": false,Claude 继续工作并把 reason 作为下一条指令:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
}
]
}
]
}
}基于智能体的 Hook
智能体 Hook 是实验性的,行为和配置在未来版本可能变化;生产工作流优先用命令 Hook。
当验证需要检查文件或运行命令时,用 type: "agent" Hook。与只做一次 LLM 调用的 prompt Hook 不同,agent Hook 会派生一个子智能体,在返回决定之前可以读文件、搜索代码、使用其他工具来验证条件。它使用同样的 "ok" / "reason" 响应格式,默认超时更长(60 秒),最多 50 个工具使用回合。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "agent",
"prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
"timeout": 120
}
]
}
]
}
}仅靠 Hook 输入数据就足以做决定时用 prompt Hook;需要对照代码库的实际状态验证时用 agent Hook。
HTTP Hook
用 type: "http" 把事件数据 POST 到 HTTP 端点,而不是运行 shell 命令。端点收到的 JSON 与命令 Hook 在 stdin 收到的相同,并通过 HTTP 响应体以相同的 JSON 格式返回结果。适合让 Web 服务器、云函数或外部服务来处理 Hook 逻辑,比如团队共享的审计服务:
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/tool-use",
"headers": {
"Authorization": "Bearer $MY_TOKEN"
},
"allowedEnvVars": ["MY_TOKEN"]
}
]
}
]
}
}要阻止工具调用,返回带相应 hookSpecificOutput 字段的 2xx 响应,仅靠 HTTP 状态码无法阻止动作。请求头的值支持用 $VAR_NAME 或 ${VAR_NAME} 做环境变量插值,但只有列在 allowedEnvVars 数组里的变量才会被解析,其他 $VAR 引用保持为空。
限制与排障
Hooks 与权限模式:PreToolUse Hook 在任何权限模式检查之前触发,在每种权限模式下都是(包括 dontAsk)。返回 permissionDecision: "deny" 的 Hook 即使在 bypassPermissions 模式或使用 --dangerously-skip-permissions 时也会阻止工具,所以你可以强制执行不允许用户绕过的策略。反过来则不成立:返回 "allow" 的 Hook 不会绕过设置里的拒绝规则。
Hook 没触发:运行 /hooks 确认 Hook 出现在正确的事件下;检查 matcher 与工具名是否完全匹配(区分大小写);确认你触发的是正确的事件类型(PreToolUse 在工具执行前,PostToolUse 在执行后)。
输出里出现 Hook 错误:脚本意外以非零码退出。手动测试,用管道传入样例 JSON:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $? # 检查退出码出现「command not found」时,用绝对路径或 ${CLAUDE_PROJECT_DIR} 引用脚本;出现「jq: command not found」时,安装 jq 或改用 Python/Node.js 解析 JSON;脚本根本没运行时,用 chmod +x ./my-hook.sh 让它可执行。
/hooks 显示没有配置 Hook:文件编辑通常会被自动识别,几秒后仍未出现,说明文件监视器可能漏了改动,重启会话强制重新加载;确认 JSON 有效(不允许尾逗号和注释);确认设置文件位置正确:项目 Hook 在 .claude/settings.json,全局 Hook 在 ~/.claude/settings.json。
Stop Hook 达到阻止上限:Claude 持续工作而不停止,最后以警告结束回合,称 Stop Hook 连续阻止次数过多。Claude Code 在 Stop Hook 连续阻止八次且没有进展后会覆盖它。你的脚本需要检查它是否已经触发过一次延续:解析输入 JSON 里的 stop_hook_active 字段,为 true 时提前退出:
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # 允许 Claude 停止
fi
# ... Hook 其余逻辑如果你的 Hook 确实需要超过八次迭代才能收敛,用 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP 提高上限。
Hook 打印了 JSON 却没有效果:常见原因一是 JSON 之前有额外输出:shell 配置文件里无条件的 echo 先写了 stdout,输出不再以 { 开头,Claude Code 就把整个 stdout 当作纯文本;修复办法是把 shell 配置里的 echo 包在交互式判断里(Hook 运行在非交互式 shell 里,$- 里没有 i):
# 在 ~/.zshrc 或 ~/.bashrc 里
if [[ $- == *i* ]]; then
echo "Shell ready"
fi原因二是字段放错层级:比如 permissionDecision 和 additionalContext 属于 hookSpecificOutput 内部,放在顶层会被静默忽略;用 claude --debug 可以看到被忽略的字段。
调试技巧:按 Ctrl+O 打开对话记录视图查看 Hook 运行结果:成功的运行什么都不显示(除非 Hook 的 JSON 产生了 systemMessage 或 Stop Hook 反馈之类的内容)。