用法
Claude Code Hooks 教程:怎么配置 hooks、选事件、套用现成示例
Hooks 是用户自定义的 shell 命令,Claude Code 在生命周期的特定时刻运行它们。执行的是 hook 而不是模型,所以这个动作一定会发生。这一页是操作步骤:hook 放在哪里、一条 hook 长什么样、哪些事件最常用、四个可以直接粘贴的示例、退出码怎么拦截操作、hook 没反应时看什么。内容按 Anthropic 的 Automate actions with hooks 和 Hooks reference 中文整理,2026 年 10 月 30 日对照原文核对。
- hook 就是设置文件里的一个
hooks块:先选事件,再加 matcher 过滤,最后定义要运行的 handler。放在~/.claude/settings.json对你所有项目生效,放在.claude/settings.json可以提交到仓库和团队共享。 - 在 Claude Code 里输入
/hooks,确认 hook 登记在正确的事件下面。 - 退出码 0 表示成功;退出码 2 在可以拦截的事件上阻止操作,你写到 stderr 的内容就是原因;其他退出码是非阻塞错误。
- command hook 以你的完整用户权限执行 shell 命令,添加之前先审查并测试每一条命令。
Hooks 是什么,什么时候该用
Hooks 是用户自定义的 shell 命令。Claude Code 在生命周期的特定时刻运行它们,让你获得确定性的控制:某些动作一定会发生,而不是依赖 LLM 自己决定要不要运行。可以用 hooks 来落实项目规则、自动化重复任务,以及把 Claude Code 接进你已有的工具。
Anthropic 给出的用法包括:编辑后格式化文件、在命令执行前拦截命令、Claude 需要输入时发通知、会话开始时注入上下文。需要判断而不是确定性规则的决定,指南指向 prompt hook 和 agent hook,它们用 Claude 模型来评估条件。想在每次会话开始时注入上下文,指南建议改用 CLAUDE.md。想给 Claude 额外的指令和可执行命令,见技能(skills)。
安全提醒:command hook 以你的完整用户权限执行 shell 命令,可以修改、删除或访问你的用户账号能访问的任何文件。添加到配置之前,先审查并测试所有 hook 命令。
Hooks 配置在哪里
Hooks 定义在 JSON 设置文件里。hook 加在哪里,决定了它的作用范围:
| 位置 | 作用范围 | 能否共享 |
|---|---|---|
~/.claude/settings.json | 你的所有项目 | 否,只在你的机器上 |
.claude/settings.json | 单个项目 | 是,可以提交到仓库 |
.claude/settings.local.json | 单个项目 | 否,Claude Code 往里保存设置时会被 gitignore |
| 托管策略设置(Managed policy settings) | 整个组织 | 是,由管理员控制 |
插件的 hooks/hooks.json | 插件启用时 | 是,随插件打包 |
| 技能(skill)的 frontmatter | 技能被调用之后的本次会话 | 是,定义在技能文件里 |
| 子代理(subagent)的 frontmatter | 该子代理运行期间 | 是,定义在子代理文件里 |
在 Claude Code 里输入 /hooks,会打开一个只读的 hooks 浏览器,列出已配置的 hooks,并标明每个 hook 的来源,例如用户设置、项目设置、本地设置、某个插件或当前会话。想停用 hooks,在设置文件里写 "disableAllHooks": true。Claude Code 运行时直接编辑设置文件,文件监视器通常会自动读取 hook 的改动。
在交互会话里,Claude Code 会先扣住所有设置文件里的 hooks,包括你自己的 ~/.claude/settings.json,直到你接受该文件夹的工作区信任对话框,或者接受某个信任范围覆盖到它的上级目录。
一条 hook 的结构
配置分三层嵌套:选一个要响应的 hook 事件,比如 PreToolUse 或 Stop;加一个 matcher 组来过滤触发时机,比如“只对 Bash 工具”;再定义匹配时要运行的一个或多个 hook handler。每个事件名都是同一个 hooks 对象里的键。下面是 Anthropic 指南里的第一个 hook,macOS 上的桌面通知:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
如果你的设置文件里已经有 hooks 键,把新事件作为已有事件键的同级项加进去,不要替换整个对象。
内层 hooks 数组里的每个对象是一个 hook handler。大多数 hook 用 "type": "command",运行一条 shell 命令。另外还有四种类型:"type": "http"、"type": "mcp_tool"、"type": "prompt" 和 "type": "agent"。Agent hook 是实验性的,可能会变。handler 的常用字段:
| 字段 | 必填 | 作用 |
|---|---|---|
type | 是 | "command"、"http"、"mcp_tool"、"prompt" 或 "agent" |
if | 否 | 用权限规则语法过滤这个 hook 何时运行,例如 "Bash(git *)" 或 "Edit(*.ts)"。只在工具事件上求值:PreToolUse、PostToolUse、PostToolUseFailure、PermissionRequest 和 PermissionDenied。在其他事件上,设了 if 的 hook 永远不会运行 |
timeout | 否 | 取消前等待的秒数。默认值:command、http、mcp_tool 为 600;prompt 为 30;agent 为 60 |
statusMessage | 否 | hook 运行期间显示的自定义转圈提示 |
Matcher 怎么写
没有 matcher 时,hook 在对应事件每次发生时都会触发。matcher 字段用来过滤触发时机,怎么求值取决于它包含哪些字符:
| Matcher 的值 | 求值方式 | 示例 |
|---|---|---|
"*"、"" 或省略 | 匹配全部 | 该事件每次发生都触发 |
只含字母、数字、_、-、空格、, 和 | | 精确字符串,或用 |、, 分隔的精确字符串列表,分隔符两侧可以有空白 | Bash 只匹配 Bash 工具;Edit|Write 和 Edit, Write 都精确匹配这两个工具中的任意一个 |
| 含有其他任何字符 | JavaScript 正则表达式,不锚定 | ^Notebook 匹配名字以 Notebook 开头的任何工具;mcp__memory__.* 匹配 memory 服务器的所有工具 |
每种事件匹配的字段不同:PreToolUse 和 PostToolUse 匹配工具名,SessionStart 匹配会话的启动方式(startup、resume、clear、compact、fork),Notification 匹配通知类型。Matcher 区分大小写。UserPromptSubmit、Stop 等少数事件不支持 matcher,每次发生都会触发。
if 字段比 matcher 更细,matcher 只在组的层面按工具名过滤。下面这个配置只在 Claude 用 git 命令时运行 hook,而不是所有 Bash 命令:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-git-policy.sh"
}
]
}
]
}
}
这个过滤是尽力而为的,所以指南说,要强制执行硬性的允许或拒绝,请用权限系统,不要用 hook。
最常用的事件
事件触发时,Claude Code 会并行运行匹配的 hooks。指南列出的事件远不止这些;下表是本页示例用到的,加上几个常见的。
| 事件 | 何时触发 | 能否拦截 |
|---|---|---|
SessionStart | 会话开始或恢复时 | 不能。退出码 2 只把 stderr 显示给用户 |
UserPromptSubmit | 提交提示词时,在 Claude 处理之前 | 能,拦截这条提示词 |
PreToolUse | 工具调用执行之前 | 能,拦截这次工具调用 |
PermissionRequest | 工具调用需要权限决定时 | 不能。不认退出码 2;要拒绝,改用 decision 对象 |
PostToolUse | 工具调用成功之后 | 不能。把 stderr 显示给 Claude;工具已经运行过了 |
Notification | Claude Code 发出通知时 | 不能。退出码和 stderr 都被忽略 |
Stop | Claude 回答完毕时 | 能,阻止 Claude 停下,对话继续 |
ConfigChange | 会话期间某个配置文件发生变化时 | 能,阻止这次变更生效(policy_settings 除外) |
CwdChanged | 工作目录变化时,例如 Claude 执行了 cd 命令 | 不能。只把 stderr 显示给用户 |
PreCompact | 上下文压缩之前 | 能,阻止压缩 |
SessionEnd | 会话结束时 | 不能。只把 stderr 显示给用户 |
四个可以直接粘贴的示例
每个都是 Anthropic 指南里现成可用的配置块。Bash 示例用 jq 解析 JSON:macOS 上用 brew install jq 安装,Debian 和 Ubuntu 上用 apt-get install jq,也可以看 jq 的下载页。
Claude 需要输入时收到通知
这个 hook 用 Notification 事件,Claude Code 在 Claude 等待输入或权限时触发它。macOS 的写法在上一节里;下面是 Linux 的,用 notify-send。加到 ~/.claude/settings.json:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' 'Claude Code needs your attention'"
}
]
}
]
}
}
空的 matcher 对所有通知类型都触发。只想在特定情况触发,把它设成某个通知类型,例如 permission_prompt(Claude 需要你批准一次工具使用)或 idle_prompt(Claude 大约 60 秒前回答完,之后你一直没有输入)。macOS 上没有通知时:osascript 通过内置的“脚本编辑器”(Script Editor)应用发通知,如果脚本编辑器没有通知权限,命令会静默失败。Linux 上,notify-send 需要桌面通知守护进程,无头服务器、SSH 会话和大多数容器都没有。
编辑后自动格式化代码
这个 hook 用 PostToolUse 事件加 Edit|Write matcher,所以只在文件编辑类工具之后运行。命令用 jq 取出被编辑的文件路径,再交给 Prettier。加到项目根目录的 .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
测试方法:让 Claude 往一个 JavaScript 文件里加一行带单引号字符串的代码,然后打开文件:用 Prettier 的默认设置,hook 会把它们改成双引号。hook 成功时,Claude Code 不会在对话里显示任何东西,所以要看被编辑的文件有没有被重新格式化。Bash 命令也可能改写文件;想让某个特定文件不管怎么变化都被重新格式化,改用 FileChanged hook。
拦截对受保护文件的编辑
这个示例阻止 Claude 修改敏感文件,比如 .env、package-lock.json,或者 .git/ 里的任何东西。Claude 会收到说明编辑为什么被拦截的反馈,从而调整做法。hook 调用一个脚本,脚本拿目标文件路径和一组受保护的模式比对,并用退出码 2 拦截编辑。把脚本存成 .claude/hooks/protect-files.sh:
#!/bin/bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Normalize Windows backslash separators so the patterns below match
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 0
hook 脚本必须可执行,Claude Code 才能运行它。在 macOS 和 Linux 上:
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"
}
]
}
]
}
}
测试方法:让 Claude 往你的 .env 文件里加一行注释。Claude Code 会在编辑执行之前拦截它,并把脚本的 Blocked: 消息作为反馈交给 Claude。
压缩之后重新注入上下文
Claude 的上下文窗口满了以后,压缩(compaction)会把对话总结一遍来腾出空间,这可能丢掉重要细节。用带 compact matcher 的 SessionStart hook,在每次压缩之后重新注入关键上下文。你的命令写到 stdout 的纯文本,Claude Code 会加进 Claude 的上下文。加到项目根目录的 .claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
}
]
}
]
}
}
可以把 echo 换成任何会产生动态输出的命令,比如用 git log --oneline -5 显示最近的提交。
退出码,以及拦截是怎么生效的
Hooks 通过 stdin、stdout、stderr 和退出码与 Claude Code 通信。事件触发时,Claude Code 把和事件相关的数据以 JSON 传给你脚本的 stdin。对一条 Bash 命令,PreToolUse hook 收到的字段包括 session_id、cwd、hook_event_name、tool_name 和 tool_input,其中 tool_input.command 是那条 shell 命令。下面这个脚本拦截一条命令:
#!/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 becomes Claude's feedback
exit 2 # exit 2 = block the action
fi
exit 0 # exit 0 = no decision; the normal permission flow applies
- 退出码 0 表示成功。
PreToolUsehook 退出码 0 时,这次工具调用仍然走正常的权限流程,因为退出码 0 并不等于批准。对UserPromptSubmit、UserPromptExpansion、SessionStart和PostModelSwitchhooks,Claude Code 会把纯文本 stdout 加进 Claude 的上下文。对其他大多数事件,stdout 写进调试日志,不会显示在对话记录里。 - 退出码 2 在可以拦截的事件上阻止操作。把原因写到 stderr:随拦截一起给出的消息就是 hook 的 stderr。取决于事件,Claude 会把它当作反馈来调整,或者用户能看到它,或者根本不显示消息。有些事件不能拦截:对
SessionStart等,退出码 2 只把 stderr 显示给用户,执行继续。 - 其他任何退出码是非阻塞错误:操作照常进行,但失败即拦截的 hook 除外,例如把
onFailure设为"block"的 hook。在PreToolUse等事件上,你会在对话记录里看到一条<hook name> hook error提示。
退出码为 0 的 hook,Claude 永远看不到它的 stderr。想从 PostToolUse hook 向 Claude 传一条警告,改用退出码 2,这样即使工具已经运行过,Claude 也能看到 stderr。PostToolUse hook 无法撤销已经发生的动作,因为工具已经执行了。
退出码只能让你拦截或保持沉默。想要更细的控制,退出码用 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"
}
}
返回 "deny" 时,Claude Code 取消这次工具调用,并把 permissionDecisionReason 反馈给 Claude。返回 permissionDecision: "deny" 的 hook,即使在 bypassPermissions 模式下或带 --dangerously-skip-permissions 时,也会拦截工具。多个 hook 匹配同一个事件时,每个 hook 的命令都会运行完,Claude Code 才合并结果,所以某个 hook 返回 deny 并不会阻止同级的其他 hook 执行。
怎么调试 hook
- 运行
/hooks,确认 hook 出现在正确的事件下面。 - 把一段示例 JSON 通过管道喂给脚本来测试,再检查退出码(命令见下方)。
- 按
Ctrl+O打开对话记录视图,找 hook 的结果。成功时什么都看不到,除非 hook 的 JSON 带出了什么内容。阻塞错误会显示随拦截给出的消息。非阻塞错误会显示一条<hook name> hook error提示和一句简短说明。 - 要看完整的执行细节,包括 hook 的退出码、stdout 和 stderr,读调试日志。用
claude --debug-file /tmp/claude.log启动 Claude Code,把日志写到一个已知路径,再在另一个终端里tail -f /tmp/claude.log。如果启动时没加这个参数,在会话中运行/debug开启日志并找到日志路径。
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh
echo $? # Check the exit code
claude --debug-file /tmp/claude.log
tail -f /tmp/claude.log
常见问题排查
| 现象 | 检查什么 |
|---|---|
| hook 从不触发 | 运行 /hooks,确认它出现在正确的事件下。检查 matcher 是否和工具名完全一致,matcher 区分大小写。确认你触发的是对的事件类型:PreToolUse 在工具执行前触发,PostToolUse 在之后触发。 |
/hooks 里没有 hooks | 几秒钟后还没出现,就重启会话强制重新加载。确认 JSON 合法:不允许尾逗号和注释。确认文件位置正确:项目 hooks 用 .claude/settings.json,全局 hooks 用 ~/.claude/settings.json。 |
| 对话记录里出现 “PreToolUse hook error: ...” | 你的脚本意外地以非零退出码退出。往它里面管道输入示例 JSON 手动测试。脚本根本没运行时,给它加可执行权限:chmod +x ./my-hook.sh。 |
| “command not found” | 引用脚本时用绝对路径或 ${CLAUDE_PROJECT_DIR}。 |
| “jq: command not found” | 安装 jq,或者改用 Python/Node.js 解析 JSON。 |
| hook 打印了 JSON 却没有效果 | 可能有别的东西先写了 stdout,通常是 shell 配置文件里无条件的 echo,输出就不再以 { 开头。把这些 echo 包起来,只在交互式 shell 里运行。也可能是字段放错了层级:permissionDecision 属于 hookSpecificOutput 内部,不是顶层。用 claude --debug 启动,在调试日志里搜 Hook output does not start with { 或 Hook JSON output had unrecognized keys。 |
| Claude 一直在干活,并警告 Stop hook 拦截次数太多 | Stop hook 在连续拦截八次、期间 Claude 没有任何工具调用时,Claude Code 会推翻它。从 JSON 输入里解析 stop_hook_active 字段,如果是 true 就提前退出。 |
stop_hook_active 的检查写法如下:
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # Allow Claude to stop
fi
# ... rest of your hook logic
常见问题
什么时候该用 hook,而不是在 CLAUDE.md 里写一条指令?
某个动作必须一定发生时用 hook。hook 是 Claude Code 在特定时刻运行的 shell 命令,不依赖 LLM 自己决定要不要运行。想在每次会话开始时注入上下文,Anthropic 的指南建议改用 CLAUDE.md;压缩之后重新注入上下文的例子,则是带 compact matcher 的 SessionStart hook。
hook 该放进哪个设置文件?
对你所有项目生效、又只留在你机器上的 hook,放 ~/.claude/settings.json。只针对一个项目、可以提交进仓库的,放 .claude/settings.json。只针对一个项目又不想共享的,放 .claude/settings.local.json。不同层级的 hook 条目是合并的,不会互相替换。
在绕过权限的模式下,hook 还能拦截吗?
能。返回 permissionDecision: "deny" 的 PreToolUse hook,即使在 bypassPermissions 模式下或带 --dangerously-skip-permissions 时也会拦截工具,因为 PreToolUse hooks 在任何权限模式检查之前触发。返回 "allow" 的 hook 绕不过设置里的 deny 规则。
来源
- Claude Code:Automate actions with hooks(第一个 hook、示例、matcher、排错,2026 年 10 月 30 日核对)
- Claude Code:Hooks reference(hook 位置、handler 字段、退出码、调试、安全注意事项,2026 年 10 月 30 日核对)