CoreCode博客

用法

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;工具已经运行过了
NotificationClaude Code 发出通知时不能。退出码和 stderr 都被忽略
StopClaude 回答完毕时能,阻止 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 的 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

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 规则。

来源