RSS

第 7 章 / 共 10 章

产物三·钩子:唯一不靠模型判断的那一档

约 5 分钟 更新于

7.1 建议和强制的区别

前三章做的技能和子代理,有一个共同点:用不用,最后都是模型说了算description 写得再好,也只是提高模型想起它的概率。这对大多数事情够用了。

但有些事不能是概率。“每次编辑后格式化""提交前必须跑 lint""绝不许写 .env”——这些是纪律,漏一次就出事。对纪律,你需要的不是”建议模型这么做”,而是”不管模型怎么想,这件事都会发生”。

钩子就是那唯一确定性的一档。它挂在生命周期的特定事件上,由事件触发,不经过模型判断

7.2 写第一个 PostToolUse:编辑后自动格式化

我们要的第一条纪律是:只要 Claude 改了文件,就自动跑一遍格式化。对应的事件叫 PostToolUse——工具成功执行之后触发。

钩子配置写在 settings 文件里。项目级的放 .claude/settings.json(可提交,全队共享):

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs -r npx prettier --write"
          }
        ]
      }
    ]
  }
}

拆开看:PostToolUse 是事件名;matcherEdit|Write 只在这两种工具后触发;hooks 数组里 type: command 表示跑一条 shell 命令。命令本身从标准输入拿到本次事件的 JSON,用 jq 取出被改文件的路径,交给 prettier。

7.3 钩子从 stdin 拿到什么

钩子脚本不是凭空运行的——它从标准输入收到一段描述本次事件的 JSON。核心字段有:

{
  "session_id": "abc123",
  "hook_event_name": "PostToolUse",
  "cwd": "/home/you/project",
  "tool_name": "Edit",
  "tool_input": { "file_path": "src/app.ts", "old_string": "...", "new_string": "..." }
}

你要的文件路径在 tool_input.file_path 里,所以上面用 jq -r '.tool_input.file_path' 把它取出来。

这里要纠正一个常见的旧习惯:从 stdin 的 JSON 取字段,别依赖某个环境变量去拿文件路径。stdin 的 JSON 是稳定的取值方式,字段结构清楚,比记一堆环境变量名靠谱。

第7章:一次编辑触发 PostToolUse,钩子从 stdin 读 JSON 后用退出码决定结果
图 7.1:模型的一次 Edit 成功后,Claude Code 触发 PostToolUse,把事件 JSON 从标准输入喂给你的脚本;脚本做完事,用退出码告诉 Claude Code 结果。整条链路里没有任何一步是”模型决定要不要跑”——这就是确定性的含义。

7.4 退出码:0 放行,2 拦截

钩子脚本用退出码跟 Claude Code 通信,约定很简单:

  • 退出 0:成功。一切照常继续。
  • 退出 2:拦截。在可拦截的事件上,这次操作被挡下,脚本打到标准错误的内容会成为拦截理由反馈给模型。
  • 其他非零:非阻断错误,操作照常,但会显示一条错误提示。

PostToolUse 发生在工具已经执行之后,所以它拦不住”已经发生的编辑”,退出 2 在这里不阻断操作。真正用退出 2 来拦下动作的,是 PreToolUse——在工具执行之前触发的那个事件,正是下一章守门的主角。

广告位 · Multiplex 关联广告