Claude Code Hooks阻断工具调用阶段详解(事件名与决策机制全解)

Hooks 系统只在特定生命周期事件上具备”阻断工具调用”的能力,并不是所有事件都能拦下工具;按官方语义,能阻断的事件集中在 PreToolUse、PostToolUse、UserPromptSubmit、Stop、SubagentStop、PermissionRequest 等少数节点,每个事件通过”退出码 + JSON 输出”两种通道向 Claude Code 表达决策。理解”哪一类事件能拦、怎么拦、拦下后会发生什么”,是写团队级安全策略的前提。

一、先把 Hook 事件全貌摆出来

Claude Code 的 Hook 事件覆盖了会话从启动到关闭的完整生命周期。按”是否能阻断主流程”做一次切分,会更直观。

事件名 触发时机 是否能阻断 典型用途
PreToolUse 工具执行前 是 策略守卫、改写参数、强制审批
PostToolUse 工具成功完成后 部分(仅错误反馈) 自动格式化、注入额外上下文
PostToolUseFailure 工具执行失败后 否 失败重试建议、日志
UserPromptSubmit 用户提交提示后 是 注入上下文、阻断危险请求
PermissionRequest 权限弹窗展示时 是 自动放行或升级审批
Stop 主代理停止时 是(block 字段) 阻止过早结束(loop 模式)
SubagentStop 子代理停止时 是 校验子代理成果
Notification 通知发送时 否 桌面提醒、Slack 通知
SessionStart 会话开始/恢复 否 注入环境信息
SessionEnd 会话结束 否 清理与归档
PreCompact / PostCompact 压缩前后 否 备份会话、打印摘要

注:Claude Code 早期文档把 PostToolUse 描述为”能阻断”实为”只能返回错误反馈让 Claude 调整”,并非真正阻止已完成的工具结果;PreToolUse 才是真正能在工具执行前按下刹车的位置。

二、能阻断 vs 不能阻断的根本差异

事件是否”能阻断”取决于它在代理主循环里插入的时机点:发生在动作执行前的事件,hook 有机会拒绝或改写;发生在动作完成后的事件,hook 只能做事后补救。

  • 发生在前的:PreToolUse、UserPromptSubmit、PermissionRequest;
  • 发生在后但能”反向修正”的:PostToolUse(错误反馈让模型重试)、Stop(用 decision:block 阻止回合结束);
  • 纯通知型:Notification、SessionStart、SessionEnd、PreCompact、PostCompact。

写策略前,先把事件按”前/后”排清楚,PreToolUse 留给”绝不能放过去的红线”,PostToolUse 留给”放过去了但要补一刀的修复”。

三、PreToolUse 的三种决策路径

PreToolUse 是 Hooks 系统里最关键的事件,官方为它定义了三种决策表达方式:退出码、JSON permissionDecision、JSON updatedInput 改写。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 -c \"import json,sys; d=json.load(sys.stdin); p=d.get('tool_input',{}).get('file_path','') or d.get('tool_input',{}).get('command',''); sys.exit(2 if any(x in p for x in ['.env','secrets/','credentials']) else 0)\""
          }
        ]
      }
    ]
  }
}

当 hook 进程退出时,Claude Code 按下面三种语义处理:

  1. 退出码 0 表示”无意见”,PreToolUse 不会自动放行——还要走常规权限流程;
  2. 退出码 2 表示”阻断工具调用”,stderr 内容会作为反馈送给 Claude,让它调整;
  3. 退出码非 0 非 2 表示”非阻塞错误”,transcript 显示 hook error 但工具调用照常执行。

如果想要更精细的控制(自动放行、改写命令、给 Claude 补充上下文),可以在退出码 0 的同时,往 stdout 输出结构化 JSON:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "auto-approved by policy",
    "updatedInput": {"command": "npm run lint -- --fix"}
  }
}

permissionDecision 有四个值:allow 跳过交互式弹窗(仍受 deny 规则约束)、deny 阻断并把原因给 Claude、ask 走正常人工审批、defer 在 -p 非交互模式下保留工具调用由外层 SDK 决定。要注意 deny 比 allow 优先级高——团队 managed deny 规则始终胜出。

四、其他能阻断的事件与决策方式

不同事件走不同的决策通道,混用会失效。

事件 阻断字段 阻断后的反馈路径
PreToolUse hookSpecificOutput.permissionDecision = "deny" permissionDecisionReason → Claude
PreToolUse exit 2 stderr → Claude
PostToolUse decision: "block"(顶层字段) reason → Claude 作为反馈
Stop decision: "block" reason → Claude,强制继续工作
SubagentStop decision: "block" reason → Claude
UserPromptSubmit 退出码 2 或 JSON decision.behavior = "block" stderr → Claude
PermissionRequest hookSpecificOutput.decision.behavior = "allow/deny/ask" 走权限流程

PostToolUse 的”阻断”与 PreToolUse 不一样:工具已经执行完,hook 返回 block 只是把”hook 拒绝接受这个结果”的信息告诉 Claude,让它修正后续行为;而 PreToolUse 的 deny 是真的不让工具跑。

五、把”能阻断”这件事落到配置里

下面这段配置展示一个精简可用的”高安全等级”策略:用 PreToolUse 拦危险命令、用 PostToolUse 自动 lint、用 Stop 防早停。

{
  "hooks": {
    "PreToolUse": [
      {"matcher": "Bash", "hooks": [
        {"type": "command", "command": "jq -r '.tool_input.command' | grep -qE 'rm -rf|DROP TABLE|sudo' && { echo 'Blocked by policy' >&2; exit 2; } || exit 0"}
      ]},
      {"matcher": "Edit|Write", "hooks": [
        {"type": "command", "command": "jq -r '.tool_input.file_path' | grep -qE '\\.env|secrets/|\\.git/' && { echo 'sensitive file' >&2; exit 2; } || exit 0"}
      ]}
    ],
    "PostToolUse": [
      {"matcher": "Edit|Write|MultiEdit", "hooks": [
        {"type": "command", "command": "jq -r '.tool_input.file_path' | xargs -I{} npx prettier --write {}"}
      ]}
    ],
    "Stop": [
      {"hooks": [
        {"type": "prompt", "prompt": "Are all tasks complete? Return {ok:true} or {ok:false, reason:'why not'}", "timeout": 30}
      ]}
    ]
  }
}

关键点:PreToolUse 退出 2 直接阻断;PostToolUse 用 type: command 跑格式化;Stop 用 type: prompt 让模型判断是否要继续,达成”循环模式”效果。

六、阻断机制的边界与坑

Hook 阻断非常强力,但也并非万能,几个常见的边界要先认清:

  1. 同一事件多个 hook 并发执行:PreToolUse 上挂了多个 matcher,命中后会并发跑,按”deny 胜出”投票;
  2. 退出码与 JSON 不能混用:退出 2 时 Claude Code 直接忽略 stdout 的 JSON;要 structured 决策必须用退出 0 + stdout JSON;
  3. deny 规则始终胜出:即使 hook 返回 allow,被团队 managed deny 规则命中的工具调用依然会被拦下;
  4. Notification / SessionStart 等事件不能被阻断:退出 2 在这些事件上只会把 stderr 显示给用户,工具流程照常推进;
  5. hook 自身有超时:command 类型默认 600 秒超时,超时后会按非阻塞错误处理。

把 Hook 看作”在固定时间点插入的、跑在你自己环境里的小程序”——它的阻断是确定性的,但只覆盖它注册的那个事件点,没注册的位置无法被它拦下。

常见问题(FAQ)

Q1:所有 Hook 事件都能阻断工具吗?

不能。只有 PreToolUse、UserPromptSubmit、PermissionRequest、Stop、SubagentStop 等少数事件能阻断;Notification、SessionStart、SessionEnd 仅做通知。

Q2:退出码 2 一定能阻断吗?

只在支持阻断的事件上生效。Notification、SessionStart 等事件退出 2 只会把 stderr 展示给用户,工具继续执行。

Q3:PreToolUse 用 JSON 和退出码 2 哪个更好?

看场景。要简单拦截危险命令,退出 2 + stderr 最直接;要自动改写参数或注入额外上下文,用退出 0 + stdout JSON。注意两者不能混用。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
套餐观察官的头像套餐观察官普通用户

相关推荐

返回顶部