乐于分享
好东西不私藏

Claude Code 源码揭秘:19 个钩子点,5 种钩子类型,把 Agent 行为拿捏得死死的

Claude Code 源码揭秘:19 个钩子点,5 种钩子类型,把 Agent 行为拿捏得死死的

💡 阅读前记得关注+星标,及时获取更新推送

「Claude Code 源码揭秘」系列的第十篇,上一篇《Claude Code 源码揭秘:19 个钩子点,5 种钩子类型,把 Agent 行为拿捏得死死的》,聊扩展性系统的时候,提到 Plugins 可以注册钩子:hooks.on('beforeMessage', ...)

钩子这东西,做过后端开发的应该都不陌生。Git 有 pre-commit 钩子,可以在提交前执行检查;Webpack 有各种生命周期钩子,可以在打包的不同阶段插入自定义逻辑;Kubernetes 的 Webhook 本质上也是一种钩子机制。类似的还有 React/Vue 的生命周期钩子、数据库的触发器、Spring AOP 切面等——核心思想都是在特定时机插入自定义逻辑,实现解耦和扩展。

Claude Code 的钩子系统更复杂一些——19 个钩子点,5 种钩子类型,能让你在 Agent 执行的各个关键节点插入自己的逻辑。

翻 src/hooks/index.ts,1400 多行代码。这套系统的设计目标很明确:不改源码,也能定制 Agent 的行为

我之前做过一个审批流引擎,也有类似的钩子设计——流程节点前后都能挂钩子,用来做校验、通知、日志记录。那套东西帮我们拦了不少违规操作,上线三年没出过大事故。Claude Code 的钩子系统,思路是一样的,只不过它面对的是 AI Agent 这个更复杂的场景。

先看有哪些钩子点。

19 个钩子事件

分两大类:工具级别和 CLI 级别。

工具级别的钩子,跟工具执行相关:

PreToolUse        - 工具执行前,可以阻止执行PostToolUse       - 工具执行后,可以记录或处理输出PostToolUseFailure - 工具执行失败后,包含错误信息PermissionRequest - 权限请求时,可以自动决策UserPromptSubmit  - 用户提交提示时,可以做输入验证Stop              - Claude 停止生成前SubagentStart     - 子代理启动时SubagentStop      - 子代理停止时PreCompact        - 上下文压缩前,可以阻止或提供自定义指令Notification      - 通知事件触发SessionStart      - 会话开始SessionEnd        - 会话结束

CLI 级别的钩子,跟启动流程相关:

BeforeSetup       - 设置前AfterSetup        - 设置后CommandsLoaded    - 命令加载完成ToolsLoaded       - 工具加载完成McpConfigsLoaded  - MCP 配置加载完成PluginsInitialized - 插件初始化后AfterHooks        - Hooks 执行后

最常用的是 PreToolUse 和 PostToolUse。前者可以在工具执行前拦截危险操作,后者可以记录执行日志。

我之前帮一个金融客户做合规审计,最头疼的就是「事后追溯」——出了问题才发现没有日志。如果当时有 Claude Code 这种钩子机制,在 PostToolUse 里挂一个日志记录,所有操作都有迹可循,审计就简单多了。

5 种钩子类型

光有钩子点还不够,钩子要干什么事?Claude Code 定义了 5 种类型:

command —— 执行 shell 命令

{  "type": "command",  "command": "echo '[$(date)] Tool: $TOOL_NAME' >> /tmp/claude.log",  "blocking":false}

最灵活的类型。你可以跑任何脚本,输入通过环境变量和 stdin 传递,输出通过 stdout 返回。

prompt —— 用 LLM 评估

{  "type": "prompt",  "prompt": "检查以下命令是否安全: {TOOL_INPUT}",  "blocking":true}

让另一个 Claude 来判断是否放行。这个设计有点意思——用 AI 审核 AI。

agent —— 代理验证器

{  "type": "agent",  "agentType": "file-operation-validator",  "blocking":true}

比 prompt 更复杂,可以配置专门的验证代理。

mcp —— 调用 MCP 服务器工具

{  "type": "mcp",  "server": "security-scanner",  "tool": "scan_command",  "blocking":true}

如果你有现成的 MCP 服务器,可以直接调用它的工具来做验证。

url —— HTTP 远程回调

{  "type": "url",  "url": "https://webhook.company.com/audit",  "method": "POST",  "blocking":false}

把事件发送到远程服务器,适合集成企业级的监控和审计系统。

这 5 种类型覆盖了几乎所有场景。本地脚本、AI 评估、MCP 集成、远程服务… 总有一款适合你。

我之前做的审批流引擎,钩子只支持 HTTP 回调一种方式。结果用户反馈说,有些场景只需要跑个本地脚本,还得专门起个 HTTP 服务来接收回调,太麻烦了。Claude Code 这里提供 5 种类型,就不会有这个问题。

command 钩子的执行细节

command 类型用得最多,看看它是怎么实现的:

async function executeCommandHook(hook: CommandHookConfig, input: HookInput) {  // 1. 变量替换  const command = replaceCommandVariables(hook.command, input);  // 2. 设置环境变量  const env = {    ...process.env,    ...hook.env,    CLAUDE_HOOK_EVENT: input.event,    CLAUDE_HOOK_TOOL_NAME: input.toolName || '',    CLAUDE_HOOK_SESSION_ID: input.sessionId || '',  };  // 3. 通过 stdin 传递完整的 JSON 输入  const inputJson = JSON.stringify({ ...input });  // 4. spawn 执行,启用 shell  const proc = spawn(command, hook.args || [], {    env,    stdio: ['pipe', 'pipe', 'pipe'],    shell: true,  });  // 5. 写入 stdin  proc.stdin.write(inputJson);  proc.stdin.end();  // 6. 超时处理  const timeoutId = setTimeout(() => {    proc.kill('SIGKILL');  }, hook.timeout || 30000);  // 7. 处理退出码  // exit 0: 成功  // exit 非0: 检查 stdout 中的 blocked 标志}

几个设计亮点:

双重输入:环境变量传简单信息(事件名、工具名),stdin 传完整的 JSON 数据。这样脚本既可以快速读环境变量,也可以解析 JSON 拿到所有细节。

超时强杀:30 秒超时,直接 SIGKILL。不给钩子脚本卡死的机会。

退出码约定:0 表示通过,非 0 要看 stdout 里有没有 blocked 字段。这个设计让脚本可以返回结构化的拒绝原因。

我之前做 CI/CD 系统的时候,也用过类似的退出码约定。0 是成功,1 是失败,2 是跳过… 标准化的约定让脚本之间能互相配合。

阻塞与非阻塞

每个钩子都有一个 blocking 属性:

{  "type": "command",  "command": "...",  "blocking": true   // 阻塞模式}

blocking: true 意味着钩子可以阻止后续操作。比如 PreToolUse 钩子返回 blocked: true,工具就不会执行。

blocking: false 是「放行但记录」模式。钩子执行完,不管结果如何,后续操作都会继续。适合日志记录、通知这类场景。

执行多个钩子的时候,遇到阻塞就停:

for (const hook of matchingHooks) {  const result = await executeHook(hook, input);  results.push(result);  // 阻塞型钩子返回 blocked,停止后续钩子  if (result.blocked && hook.blocking) {    break;  }}

这个设计很像责任链模式。链条上的每个节点都可以选择「放行」或「拦截」,一旦拦截,后面的节点就不用跑了。

Matcher:精确匹配和正则匹配

有时候你只想对特定工具挂钩子。比如只对 Bash 工具做安全检查,对 Read 工具不管。这就用到 matcher:

{  "type": "prompt",  "prompt": "检查命令安全性...",  "matcher": "Bash",          // 只匹配 Bash 工具  "blocking":true}

还支持正则:

{  "type": "agent",  "agentType": "file-validator",  "matcher": "/^(Write|Edit)$/",  // 匹配 Write 或 Edit  "blocking":true}

匹配逻辑:

function getMatchingHooks(event: HookEvent, toolName?: string) {  return hooks.filter((hook) => {    if (hook.matcher && toolName) {      // 正则匹配:/regex/      if (hook.matcher.startsWith('/') && hook.matcher.endsWith('/')) {        const regex = new RegExp(hook.matcher.slice(1, -1));        return regex.test(toolName);      }      // 精确匹配      return hook.matcher === toolName;    }    return true;  // 没有 matcher 的钩子对所有工具生效  });}

没有 matcher 的钩子是「通配」的,对所有工具生效。有 matcher 的钩子只对匹配的工具生效。

用户怎么配置钩子?

钩子配置放在 .claude/settings.json 里:

{  "hooks": {    "PreToolUse": [      {        "type": "prompt",        "prompt": "检查 Bash 命令安全性: {TOOL_INPUT}",        "matcher": "Bash",        "blocking":true,        "timeout": 30000      }    ],    "PostToolUse": [      {        "type": "command",        "command": "echo '[$(date)] $TOOL_NAME executed' >> ~/.claude/audit.log",        "blocking":false      },      {        "type": "url",        "url": "https://audit.company.com/log",        "method": "POST",        "blocking":false      }    ],    "SessionStart": [      {        "type": "command",        "command": "./scripts/session-init.sh"      }    ]  }}

配置文件的查找顺序:

项目级:.claude/settings.json全局级:~/.claude/settings.json

项目级的配置会覆盖全局级。这个设计让你可以给不同项目配置不同的钩子策略。

实际应用场景

场景一:安全拦截

在 PreToolUse 挂一个安全检查钩子,对 Bash 命令做审核:

{  "hooks": {    "PreToolUse": [      {        "type": "prompt",        "prompt": "你是安全审核员。检查以下 Bash 命令是否安全,是否包含危险操作(rm -rf、sudo、curl | bash 等)。命令:{TOOL_INPUT}。如果安全返回 {\"ok\": true},如果危险返回 {\"ok\": false, \"reason\": \"原因\"}",        "matcher": "Bash",        "blocking":true      }    ]  }}

这样即使模型生成了危险命令,也会被钩子拦截。

场景二:审计日志

在 PostToolUse 挂一个日志记录钩子:

{  "hooks": {    "PostToolUse": [      {        "type": "url",        "url": "https://siem.company.com/api/events",        "method": "POST",        "headers": {          "Authorization": "Bearer ${SIEM_TOKEN}"        },        "blocking":false      }    ]  }}

所有工具执行都会发送到企业的 SIEM 系统,方便事后审计。

场景三:自动授权

在 PermissionRequest 挂一个自动决策钩子:

{  "hooks": {    "PermissionRequest": [      {        "type": "command",        "command": "./scripts/auto-approve.sh",        "blocking":true      }    ]  }}

脚本里可以根据工具名、输入参数等自动判断是否授权,减少人工干预。

我之前在一个项目里,运维同事天天被权限申请烦死——大部分都是常规操作,但还是要人工点批准。后来我们写了个自动审批脚本,根据申请人、操作类型、时间段等规则自动放行,人工审批量减少了 80%。Claude Code 的这个钩子机制,可以实现类似的效果。

钩子执行的完整链路

把钩子系统在整个 Agent 执行中的位置画出来:

钩子执行完整链路

钩子就像是在这条流水线上设置的「检查站」。每个检查站可以检查、记录、甚至拦截经过的「货物」。

与插件系统的集成

在 Plugins 里注册钩子很简单:

export default {  async activate(context) {    // 注册钩子    context.hooks.on('PreToolUse', async (input) => {      if (input.toolName === 'Bash') {        // 检查逻辑...        return { blocked: false };      }    });    context.hooks.on('PostToolUse', async (input) => {      // 记录日志...    });  }};

插件里的钩子和配置文件里的钩子是一起执行的,按注册顺序来。

错误处理

钩子执行出错了怎么办?三层兜底:

// 第一层:单个钩子执行的 try-catchtry {  const result = await executeHook(hook, input);} catch (err) {  return { success: false, error: err.message };}// 第二层:超时强杀setTimeout(() => proc.kill('SIGKILL'), timeout);// 第三层:配置加载的容错try {  const config = JSON.parse(content);} catch (err) {  console.error(`Failed to load hooks: ${err}`);  // 继续运行,不影响主流程}

钩子出错不会导致 Agent 崩溃。最坏情况就是这个钩子没生效,主流程继续跑。

这个设计思路是对的。钩子是「附加功能」,不能因为钩子挂了就把整个 Agent 搞崩。我之前见过一个系统,日志钩子抛了个异常,直接把主流程拖死了。后来加了错误隔离,问题才解决。

翻完这部分代码,我总结一下这套钩子系统的设计精髓:

事件驱动 —— 19 个钩子点覆盖 Agent 生命周期的关键节点类型多样 —— 5 种钩子类型适配不同场景精准匹配 —— matcher 支持精确和正则,可以细粒度控制阻塞机制 —— blocking 属性区分「拦截」和「记录」两种模式错误隔离 —— 钩子出错不影响主流程

这套机制让 Claude Code 变成了一个「可编程」的 Agent。不改源码,通过配置文件就能定制它的行为。这对企业级应用来说太重要了——安全合规、审计日志、自定义策略… 都可以通过钩子实现。k8s里面的webhook也是这么个思路,好的设计都是相通的。

下一篇聊 System Prompt 的动态构建。Claude Code 的系统提示不是写死的,而是根据当前状态动态拼接的。CLAUDE.md、工具列表、权限配置… 这些东西是怎么组合成最终的 system prompt 的?

本文基于 Claude Code 2.0.76 版本源码分析,主要文件:src/hooks/index.ts

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Claude Code 源码揭秘:19 个钩子点,5 种钩子类型,把 Agent 行为拿捏得死死的

评论 抢沙发

3 + 6 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮