Claude Code Hook 的核心特征拆解(触发时机、确定性、配置层级三视角)

把 Claude Code 的 hook 视作”由宿主在固定时间点自动触发的可编程拦截器”,就抓住了它的全部关键特征。三个视角同时成立:触发时机由 Claude Code 宿主在请求循环的固定节点触发,确定性来自宿主而非模型自行决定,配置层级则由 settings.json 的写入位置决定作用域。三者互相支撑,缺一个就会把 hook 退回”提示词建议”。

一、hook 是什么:一次正名

hook 是在 Claude Code 会话生命周期中,由宿主在特定时间点自动触发的 shell 命令、HTTP 端点、模型提示或代理逻辑。把它和 CLAUDE.md 里的”建议型指令”做一次区分最直观:CLAUDE.md 是概率性提示,模型”应该”做;hook 是确定性命令,宿主”必须”做。文档明确写明”hooks are deterministic because the harness runs them, not the model”,这句话把 hook 的本质点透——执行权握在 Claude Code 宿主手里。

理解这一点,就能避开一类常见误区:以为”我多写一段提示词,模型就会自动跑格式化”。模型不跑,宿主跑;宿主怎么知道要跑?靠事件触发。

二、第一个核心特征:触发时机

hook 不会自己启动,它要等到宿主宣布”某个事件发生了”才执行。Claude Code 在请求循环里安排了 25+ 种事件,按”每会话一次、每回合一次、每工具调用一次”三档节奏分布。

事件 触发节奏 典型用途
SessionStart / SessionEnd 每会话一次 注入 git 状态、加载 TODO 列表
UserPromptSubmit / Stop 每回合一次 追加冲刺上下文、回合收尾校验
PreToolUse / PostToolUse 每工具调用一次 阻断危险命令、运行格式化

PreToolUse 选得最多的原因藏在这张表里:它执行于”工具调用真正落地之前”,是唯一允许阻断的时机;PostToolUse 在工具跑完之后才到,错误已经发生,只能注入上下文补救。官方博客在介绍 8 种事件时反复强调”firing after Claude chooses a tool but before the tool actually executes”,这种”前/后”的语义错位是选型时最容易栽的坑。

三、第二个核心特征:确定性

确定性指”该不该跑”由宿主决定,不依赖模型判断。落地有三层证据。

第一层证据是退出码语义。PreToolUse、PermissionRequest、UserPromptSubmit、Stop 以及 config-change 类事件,退出码 2 都会阻断后续动作;PostToolUse、Notification 等事后事件,退出码 2 仅把 stderr 回灌给 Claude,已发生的事改不了。退出码是宿主读到的硬信号,不是给模型参考的”建议”。

第二层证据是匹配器过滤。matcher 字段用三种语法筛目标:* 或空字符串匹配全部;纯字母数字加 | , - 走精确字符串;包含其他字符的串走 JavaScript 正则。例如 "Edit|Write" 只对编辑类工具生效,匹配器是宿主解析,不是模型判断。

第三层证据是多 hook 合并策略。同一个事件匹配到多个 hook 时,宿主并行执行所有命令,自动去重;PreToolUse 类的权限决策按”最严格胜出”合并(deny > ask > allow)。这条规则在官方文档里写得很直白:don’t rely on one hook’s deny to suppress side effects in another hook,因为每个 hook 都真实地执行完,宿主再统一合并。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "jq -r .tool_input.command >> ~/.claude/bash.log" },
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh" }
        ]
      }
    ]
  }
}

这段配置展示了三层证据同时生效:宿主在 PreToolUse 时点触发,退出码语义定义阻断,并行执行两个 hook,按最严格策略合并。

四、第三个核心特征:配置层级

配置层级决定”这条规则谁会受影响”。Claude Code 提供四档位置,从高到低分别覆盖组织、当前会话、当前项目、当前用户。

位置 作用域 是否可分享 写入方式
托管设置 组织 是 管理员下发
.claude/settings.json 当前项目 是(提交到仓库) 手动编辑
.claude/settings.local.json 当前项目 否(gitignored) 手动编辑
~/.claude/settings.json 当前用户所有项目 否 手动编辑

另外还有 Plugin hooks/hooks.json 一档,写在外挂里、随插件分发。企业管理员可以用 allowManagedHooksOnly 把用户/项目/插件 hook 全部关掉,强制走组织统一分发的 hook 列表,托管设置下”enabledPlugins”强制启用的外挂 hook 是唯一例外。

把三特征串起来看:触发时机回答”什么时候跑”,确定性回答”怎么跑”(宿主而非模型),配置层级回答”谁被它影响”。任何一段 hook 配置写完,先对这三个问题逐项自问一遍,比对着模板抄更不易翻车。

五、把三特征落到一次落地

按下面的步骤把”阻断 rm -rf”这条规则装到项目里,能完整走过三个特征:

  1. 在仓库根目录建 .claude/hooks/block-rm.sh,内容是从 stdin 读 tool_input.command,命中 rm -rf 时输出 permissionDecision: "deny",否则 exit 0;
  2. 把上面那段 PreToolUse 配置写进 .claude/settings.json,matcher 用 "Bash" 收窄,$CLAUDE_PROJECT_DIR 引用仓库根;
  3. 在 .claude/settings.local.json 写一份个人覆盖版本,仅匹配本机路径,留给本地调试;
  4. 故意让 Claude 跑一次 rm -rf /tmp/build,观察宿主是否先并行执行两个 hook、再按 deny 阻断、最后在上下文回灌 permissionDecisionReason;
  5. 跑一次 rm file.txt,确认未命中模式时 hook 静默通过,不污染对话。

效果:危险命令在执行前被宿主拦截;普通命令不会被额外打扰。注意点:matcher 不要写 *,否则连文件写入都会被无谓触发;hook 命令建议放仓库内脚本,方便团队复用。

六、易混淆点对照

下面把和 hook 长得像但本质不同的两件事放一起,避免混淆。

项 执行者 是否依赖模型 退出码生效
Hook 宿主 否 是
CLAUDE.md 指令 模型 是 否
PostToolUse 注入上下文 宿主(事件后) 否 仅回灌 stderr

最后这栏尤其关键:PostToolUse 跑在”事件之后”,退出码 2 不能阻止工具执行,只能让宿主把 stderr 塞回给 Claude 看到。所以拿 hook 做”事后阻断”是徒劳的,要阻断必须选 PreToolUse 或 PermissionRequest。

常见问题(FAQ)

Q1:hook 和 CLAUDE.md 指令会冲突吗?

不会。hook 由宿主强制执行,CLAUDE.md 是模型参考建议,宿主永远先按 hook 决定。

Q2:matcher 写 * 会出什么问题?

宿主会对所有事件触发该 hook,无谓开销陡增,且 PreToolUse 类的退出码 2 会批量阻断正常工具调用。

Q3:PostToolUse 退出码 2 能回滚工具吗?

不能。PostToolUse 跑在工具完成之后,退出码 2 只把 stderr 注入上下文,工具动作已经发生。

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

相关推荐

返回顶部