DeepDojo.AI发布 2026-11-13更新 2026-11-13English

Claude Code Hook 该挂在哪个事件上

Claude CodeAI 编程

用了一段时间 Claude Code,你可能想给它加一道保险:它要执行 rm -rf 这类危险命令时,自动拦下来。Hook 就是做这件事的,它能在 Claude Code 工作流程中的特定时刻,运行你写的检查脚本。

很多人第一次写这种拦截,会把脚本挂到“工具执行完毕”的事件上。报错日志写得再详细、红字再醒目,磁盘上的文件也已经删掉了,它能做的只剩提醒你去翻备份。

同样叫“自动检查”,切入的时间点不同,效果天差地别:

  • 代码刚写完落盘,可以挂一个 Hook 自动跑格式化,或者扫描有没有硬编码的 API 密钥;
  • Bash 命令即将执行的前一刻,可以挂一个 Hook 检查参数,判断有没有破坏性指令;
  • 用户刚提交提示词、Claude 还没开始处理时,可以做请求层面的预检。

所以写 Hook 的第一步是想清楚这次检查要改变什么:要阻止尚未发生的动作,就选执行前的事件;要查看已经产生的结果,就选执行后的事件。时机定了,再用 matcher 和 if 缩小范围、选合适的处理器类型。

01 先问检查要改变什么

选事件之前,先问问这次检查要改变什么:

想拦住尚未执行的命令
  → PreToolUse 匹配 Bash → 检查参数 → 返回拒绝
想查执行后的结果
  → PostToolUse → 反馈问题、提示补救

相同的危险命令规则,放到 PostToolUse 时,删除等动作已经发生。Claude Code 的 Hooks 参考写得很直白:PostToolUse 在工具成功执行之后触发,这时返回的信息 Claude 能看到,但工具已经运行过了。阻止操作就选执行前事件,查看结果就选执行后事件。

主流程仍由 Skill、Agent 或人工组织,Hook 只在选定的事件上检查、记录、补信息或阻断。

工具输入经过 PreToolUse、实际执行和 PostToolUse 时各自可做的控制

02 按控制对象找事件

Claude Code 的 Hook 事件很多,可以先按控制对象分成四类:工具动作看 Tool Hooks(如 PreToolUse、PostToolUse、PermissionRequest),会话收尾看 Session Hooks(如 SessionStart、Stop、SubagentStop),用户输入看 UserPromptSubmit 这类任务事件,配置、目录、文件、上下文压缩和工作树的变化看生命周期事件。

最常用的几个事件如下:

事件触发时机常见用途
PreToolUseClaude 生成工具参数后,工具实际执行前阻断危险命令、修改工具输入、权限策略
PostToolUse工具成功执行后格式化、扫描、记录日志、追加反馈
PostToolUseFailure工具执行失败后错误记录、失败诊断、恢复建议
UserPromptSubmit用户提交 Prompt 后,Claude 处理前校验用户请求、补充上下文、阻断危险请求
Stop主回复结束时完成度检查、质量门禁
SubagentStop子代理结束时检查子代理输出质量
SessionStart会话开始时初始化日志、加载环境提示
SessionEnd会话结束时清理、统计、最终日志
Notification出现通知时权限提示、空闲提醒
PermissionRequest出现权限请求时自动允许或拒绝特定请求

写 Hook 前先确定时机:阻断用执行前事件,检查结果用执行后事件,总结或收尾用 Stop、SubagentStop 或 SessionEnd。

SessionEnd 只在会话结束时触发,适合收尾记录,比如本次处理了哪些模块、遇到哪些问题、下次要继续什么;它无法阻止会话关闭,因此负责记录和清理,强制质量门禁要放到更早的可控节点。Stop 每轮回复结束都会触发,更适合轮次级检查。

03 配置放在哪里,脚本要先存在

Hook 脚本通常放在用户目录或项目目录中。配置可以放在三处:项目级 .claude/settings.json,本地个人的 .claude/settings.local.json,以及用户级 ~/.claude/settings.json。

最基本的配置结构如下:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/pre-tool-check.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/format-code.sh"
          }
        ]
      }
    ]
  }
}

配置里写了脚本路径,还要确认那个脚本已经存在。先保存、检查脚本,再加载配置,才有东西可执行。只建目录,行为不会变化。

接着想想两个检查有没有先后依赖。格式化和扫描都挂在同一事件上,看起来按数组排好了顺序,官方文档却说明:一个事件触发时,所有匹配的 Hook 并行运行。若扫描必须读取格式化后的文件,就让一个脚本依次调用它们,依赖关系才落实到了执行过程里。

04 matcher 选工具,if 看参数

matcher 用来匹配工具名或事件值。常见写法:

matcher含义
Write只匹配 Write 工具
Edit|Write匹配 Edit 或 Write
Bash匹配 Bash 工具
*匹配所有工具
mcp__memory__.*匹配某个 MCP server 下的一组工具

先用 matcher 选工具,例如 Edit|Write 表示二选一;再用 if 看路径、参数或命令内容。if 接受一条权限规则,按权限规则的语法写过滤条件。例如,只检查 Write 对工作目录下 src/ 的写入:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/check-source-write.sh",
            "if": "Write(src/**)"
          }
        ]
      }
    ]
  }
}

沿一次调用看这段配置,更容易分清两层条件:matcher 先选中 Edit 和 Write,处理器上的 if: "Write(src/**)" 再筛一次,最后只剩指定范围内的 Write。若以为前面的匹配已经覆盖 Edit,就会漏查。需要它时,再加一个 if: "Edit(src/**)" 的处理器。

if 与 type、command 同级。旧版本不支持这个字段时,把参数判断放进脚本。脚本拿到的文件路径是绝对路径,不能直接拿 ^src/ 去匹配,先解析路径,再判断是否落在目标目录。

假设 60 次调用中有 20 次 Bash,matcher 可以先排除其他工具。只查 Git 提交和推送时,分别设置 Bash(git commit *)、Bash(git push *) 两个处理器的 if。每项只写一条权限规则,用逗号拼接表示不了“或”。其他能写远端的命令,还要由权限和账号限制控制。

05 五类处理器按判断需要选

按判断需要选处理器。规则能写清楚的本地检查先用 command,需要服务、语义判断或调查时,再选其他类型:

类型适合场景
command运行 shell 命令或脚本,适合本地检查、格式化、日志
http调用 HTTP 服务,适合通知、外部审计、远程策略
prompt让模型根据 Prompt 做判断,适合自然语言质量检查
mcp_tool调用 MCP 工具,适合接入外部工具和数据源
agent调用 agent,适合复杂判断或专项审查

拿提交说明检查来看:

标准处理方式
首行长度、类型前缀、空行command 脚本
有没有解释修改原因prompt 判断,留意成本和边缘结果
两者都需要一个流程先查格式,通过后再查含义

Command Hook 最常见:

{
  "type": "command",
  "command": "python3 ~/.claude/hooks/security-scan.py",
  "timeout": 30
}

timeout 限制执行时间。每次动作匹配都会运行 Hook,处理逻辑尽量简短,减少工具调用的等待时间。Prompt Hook 常用于 Stop 或 SubagentStop,检查“这个任务是否完成”“输出是否满足要求”;Agent Hook 适合更复杂的审查,比如让安全审查 agent 检查某个子代理的输出。

06 脚本从 stdin 读事件信息

Claude Code 会把事件信息以 JSON 形式传给 Hook。不同事件字段略有差异,常见字段包括:

字段含义
session_id会话 ID
transcript_path会话 transcript 文件路径
cwd当前工作目录
permission_mode当前权限模式
hook_event_name触发事件名
tool_name工具名称
tool_input工具输入参数
tool_use_id工具调用 ID

在写真正的解析脚本前,先挑准你想抓的事件,搞清楚它给了哪些字段。比如工具调用会带 tool_input,普通的会话生命周期事件就没有这个字段。先用不含敏感信息的合成 JSON 验证解析逻辑,再用真实事件核对必要字段。

脚本一般从 stdin 读取 JSON:

#!/usr/bin/env bash
set -euo pipefail

INPUT="$(cat)"
EVENT="$(printf '%s' "$INPUT" | jq -r '.hook_event_name // ""')"
TOOL_NAME="$(printf '%s' "$INPUT" | jq -r '.tool_name // ""')"

echo "event=$EVENT tool=$TOOL_NAME" >> "$HOME/.claude/hook-debug.log"

本地没有 jq 时,也可以用 Python 的 json.load(sys.stdin) 解析。

07 写入后格式化:一个 PostToolUse 例子

自动格式化要读取写入后的文件,因此放在 PostToolUse,matcher 为 Write|Edit。脚本根据文件扩展名选择工具:

#!/usr/bin/env bash
set -euo pipefail

INPUT="$(cat)"
FILE_PATH="$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""')"

if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
  exit 0
fi

case "$FILE_PATH" in
  *.js|*.jsx|*.ts|*.tsx|*.json|*.css|*.md)
    if command -v prettier >/dev/null 2>&1; then
      prettier --write "$FILE_PATH" >/dev/null || { echo "Formatter failed; inspect the file." >&2; exit 1; }
    fi
    ;;
  *.py)
    if command -v black >/dev/null 2>&1; then
      black "$FILE_PATH" >/dev/null || { echo "Formatter failed; inspect the file." >&2; exit 1; }
    fi
    ;;
  *.go)
    if command -v gofmt >/dev/null 2>&1; then
      gofmt -w "$FILE_PATH" >/dev/null || { echo "Formatter failed; inspect the file." >&2; exit 1; }
    fi
    ;;
esac

exit 0

这段脚本有个需要看清的选择:缺少 formatter 时直接跳过,工具运行失败时才报错。因此“Hook 没报错”可能只表示本机没有那个工具。若项目要求格式必须通过,就在提交或 CI 阶段再运行确定性检查,这里的跳过只说明没检查过。

成对的 Hook 也能做轮次级统计,比如在 UserPromptSubmit 和 Stop 各记一次会话文本长度,看哪一轮异常冗长。这只是排查线索:想看当前上下文还剩多少,直接用 /context,文本增量换算不出 token、费用或剩余空间。

08 组件级 Hook、MCP 匹配和上线前检查

除了全局设置,Skill、子代理等组件也可以声明自己的 Hooks,例如安全操作 Skill 在 frontmatter 中声明:运行 Bash 前先执行一次检查脚本。组件级 Hook 的作用域更窄,只影响对应组件,适合局部质量门禁。对子代理来说,frontmatter 里写的 Stop Hook 会在运行时转换为 SubagentStop,只在该子代理结束时触发。

MCP 工具也可以用 matcher 匹配,形式通常是 mcp__<server>__<tool>,例如 mcp__memory__.* 匹配 memory server 下的一组工具。MCP Tool Hooks 可以记录外部调用、限制敏感工具或增加审计。设计时先收紧权限边界:外部系统可能连接数据库、工单、文件服务或部署平台,一次误操作的影响范围更大。

共享给团队前,核对这些安排:

检查面做法
职责和耗时一项 Hook 做一类短检查,设置超时
阻断和副作用规则明确,测试正常任务是否被误伤
凭据和日志不硬编码密钥,记录前脱敏
上线使用先本地验证,再放项目配置;写清拦什么、记什么、改什么

共享前可以数一数:一次普通编辑会触发几次检查,每次要等多久?前置 Hook 每次调用都要等它跑完,经验上单次控制在半秒左右,开发者的思路才不容易被打断。

规则能明确写出的,就用短小的命令脚本。确实需要语义判断,再考虑 Prompt 或 Agent,承担相应成本和不确定性。长流程仍交给 Skill 或 Subagent,避免一次工具调用就带出一轮完整审查。

时机和匹配选对了,门禁仍可能失效:脚本自己崩溃时调用照常放行、字符串黑名单被换个写法绕过、Stop 检查陷入循环。这些问题和测试方法见 Claude Code Hook 为什么没拦住。斜杠命令里的 allowed-tools 只负责免确认,真正在动作前拦截要靠 Hook 或权限规则,两者的区别见Claude Code 只读命令为什么还能改文件。

选事件的顺序可以压成三步:先问检查要改变什么,再定在执行前还是执行后,最后才轮到 matcher、if 和处理器类型。