Hooks
在CLAUDE.md文件中告诉Claude做某件事,问题在于:这只是一个请求,而不是保证。您可以写"编辑后始终进行格式化",Claude通常会听从。通常。但在一次长时间运行中,当您没有盯着的时候,"通常"还不够好。Hook可以解决这个问题。Hook是在循环中固定点运行的确定性代码,因此它可以保证行为,而不是寄希望于它。它把一条规则从"Claude通常会听从"变成了"Claude无法跳过它"。
这就是全部的卖点。现在让我们来看看它实际是如何运作的。
Hook事件
Claude Code在一次会话过程中会触发大约30个hook事件。您不需要了解所有这些事件。有一小部分您会反复用到,它们恰好对应于智能体循环中您希望介入的那些节点。
它们在循环中的位置是这样的:一个会话开始,提示进来,工具被调用,最终轮次结束。每一个这样的时刻都有一个您可以挂载代码的hook。
值得了解的几个:
- PreToolUse 在工具调用之前触发。这是您的强制执行原语。它是唯一能在事情发生之前阻止它的那个。
- PostToolUse 在工具调用成功之后触发。这通常是自动格式化或自动lint所在的位置。
- Stop 在Claude想要结束其轮次时触发。如果某个条件未满足,您可以拒绝并说"不,你还没完成"。还有一个对应的SubagentStop,用于子智能体完成时。
- PreCompact 和 PostCompact 分别在压缩之前和之后触发。
- InstructionsLoaded 在CLAUDE.md或规则文件加载时触发。对于审计实际进入上下文的内容很方便。
- SessionStart 在开始时触发,并为环境做准备。如果您只想在全新启动时触发,请使用
startup来源。
有一点容易让人踩坑:要在压缩后重新注入上下文,不要使用PostCompact。请使用带有compact匹配器的SessionStart。那才是真正能把输出重新带回对话中的那个。
PreToolUse:以JSON形式返回决策
PreToolUse是真正强大的地方,因为它可以在工具调用运行之前阻止它。您与Claude对话的方式是打印JSON并以零退出。关键字段是permissionDecision,它接受三个值之一:
allow— 放行该调用deny— 阻止该调用ask— 交还给用户决定
技术上还有第四个值defer,但它只适用于非交互式的-p运行,即调用进程暂停工具并在之后恢复它的情况。您很少会用到它。
其形状如下所示:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "...",
"updatedInput": {
"command": "..."
}
}
}注意updatedInput。您可以重写调用,而不是阻止它。这就是您如何从一个bash命令中编辑掉一个密钥,同时仍然让它运行的方法。有一个需要注意的地方:updatedInput会替换整个输入对象,因此您必须回传那些您没有更改的字段,否则就会丢失它们。
退出代码,适用于不返回JSON的hook
并非每个hook都需要使用JSON。对于更简单的hook,退出代码就能完成任务。有三个重要的数字。
- 0表示成功。 如果标准输出是JSON,Claude会解析它。在大多数事件中,纯文本会被忽略,但在SessionStart、UserPromptSubmit和UserPromptExpansion中,纯文本会被添加到上下文中。这正是状态保存hook能够工作的原因。
- 2表示阻塞性错误。 标准错误会作为上下文反馈给Claude。这几乎在所有地方都是阻塞性的退出代码。
- 其他任何值都是非阻塞性的。标准错误会被记录下来,Claude会继续执行。
让人踩坑的是退出代码1。它感觉像是一个错误,但它并不会阻塞。Claude仍然会运行该命令。所以如果您想要阻止某件事,请使用退出代码2,而不是1。
还有几个细节。退出代码2甚至可以阻塞Stop,这就是您告诉Claude它还没完成的方式。但PostToolUse是在工具已经运行之后触发的,所以在那里阻塞已经太晚了,无法阻止该调用,不过它仍然可以把文本反馈给Claude。还有一些事件完全忽略阻塞,比如Notification和SessionStart。SessionStart只会把您的标准错误显示给您自己,Notification则会忽略它,两者都会照常继续执行。
一个真实的护栏:编辑而非阻止
让我们用一个实际的例子把这些串联起来。假设您想在Bash工具上设置一个PreToolUse护栏。匹配器用来选定要监视的工具,一个可选的if子句可以将其缩小到特定命令。
显而易见的做法是返回deny并阻止一次危险的调用。这很好。但鲜为人知、也更有意思的做法是返回updatedInput来重写该调用。这就是您如何从命令中剥离出一个密钥,同时仍然让它运行的方法,而不是简单地拒绝它。
下面是这在实践中的样子。Claude被要求运行一个包含看起来是真实密钥的命令。Hook拦截了它,发现了sk_live_模式,并在命令真正执行之前将其替换为一个占位符。
命令仍然运行了。工作仍然完成了。但密钥从未被泄露出去。这就是阻止和编辑之间的区别,也是hook每一次都能强制执行的那种事情。
在压缩过程中保留状态
还有一个值得设置的模式。当Claude压缩一段长对话时,它会丢弃大量细节。带有compact匹配器的SessionStart hook会在压缩之后立即运行。让它打印一份您一直在处理的文件的简短摘要。该摘要会回到上下文中,这样Claude就能从中断的地方继续,而不是从零开始。
总结
Hook把一条Claude通常会遵循的规则变成了一条它始终会遵循的规则。不要只停留在自动格式化上:用PreToolUse来守护工具,用Stop来把关轮次,并在压缩过程中保留状态。这种设置需要一点前期投入,但在它第一次捕获到您甚至都没在盯着的运行中的问题时,就已经值回票价了。