Claude Code Hooks:被忽视的最强功能
本文深入讲解Claude Code中的Hooks系统,它不同于CLAUDE.md的软性指导,而是通过可编程的检查点在工具调用前、后等关键时刻执行代码,实现对模型行为的精确控制。文章详细介绍了Hooks的事件系统、如何通过退出码或JSON结果阻止操作、四种注册位置(设置、插件、技能、子代理)及其合并规则、优先级(严格结果优先),并给出了保护生产配置文件的实用示例。核心思想:Hooks是代码而非提示,具有确定性保障。

CLAUDE.md 告诉 Claude 如何表现。
Hooks 让 Claude 真正 服从。
这就是大多数人忽略的区别。
你可以在 CLAUDE.md 里写“不要修改 prod.env”。但 Claude 是否遵守,完全取决于它此刻的判断。
Hooks 绕过了这种判断。它们是在 Claude Code 执行流程中运行的可编程检查点——在动作发生之前,而不是之后。
这是完整的指南。
Hooks 解决的问题
Claude Code 很强大。它读取文件、编写代码、执行命令、调用 API。
你给它的权限越多,它就越有用,但也越危险。
是什么阻止它在凌晨 2 点修改生产配置?是什么每次都能强制执行你的 lint 规则?是什么记录每一次敏感文件的读取,而不依赖 Claude 记住要这么做?
CLAUDE.md 是指导。Hooks 是保证。
区别在于:Hooks 运行的是真实代码。它们的逻辑不依赖于模型是否理解或记住规则。它依赖于你提前编写的代码。

Hook 到底是什么
Hook 不是提示词。它不是另一种注入上下文的方式。它是一种可编程的控制机制,运行在 Claude Code 的执行流程中。
当 Claude Code 即将调用工具、写入文件或执行命令时——Hook 会在动作发生之前介入并决定:
→ 允许它 → 阻止它 → 请求人类确认
决定是由你提前编写的代码做出的,而不是模型。
以下是 settings.json 中最简单的 Hook 配置:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "echo 'about to write a file'"
}
]
}
]
}
}
单词 "hooks" 出现了三次。每一次的含义都不同。
让我一次性拆解这三个层次。
Layer1 — 事件注册。外层的 hooks 对象。每个键都是一个事件名称——PreToolUse、PostToolUse、Stop。你希望在哪个节点干预?在这里注册该事件。
Layer2 — 匹配规则。每个事件下:一个包含 matcher 对象的数组。matcher: "Write" 表示这个组仅在 Claude 尝试调用 Write 工具时触发。没有 matcher = 匹配所有内容。
第 3 层 — 实际的 Hook。每个 matcher 内部:hooks 数组。这是真正的逻辑。type: command 运行一个 shell 脚本。type: http 调用一个 URL。type: mcp_tool 调用一个 MCP 工具。
hooks ← 整个系统的入口
└── PreToolUse ← 事件:在任何工具调用之前触发
└── matcher ← 过滤器:仅匹配 "Write" 工具
└── hook ← 动作:运行此 shell 命令

Hook 事件系统
Claude Code 有 28 个 Hook 事件。它们覆盖了 Claude 执行的每个关键节点:
→ 会话启动和停止
→ 工具调用前后
→ 权限请求
→ 文件变更
→ 子代理任务
→ 通知
大多数人会搞错的一件事是:事件是平级的,而不是父子关系。
PreToolUse 和 PermissionRequest 经常会接连触发。这看起来像是其中一个引起另一个。但实际上不是。它们是完全独立的干预点。每个都有自己的匹配规则,并且互不干扰地执行。
事件分为两种类型:
主流程事件 — 贯穿核心执行路径。这些事件可以阻止 Claude。PreToolUse、PermissionRequest、PostToolUse、Stop。
旁路事件 — 观察和通知通道。它们在正确时机触发,但不会改变主流程。Notification、ConfigChange。
它们之间的关键区别:
→ 阻塞:Hook 的结果决定 Claude 下一步做什么。主流程会暂停,直到 Hook 返回。
→ 非阻塞:Hook 运行,但 Claude 不等待它。适用于日志记录、推送通知、同步状态。

如何实际阻断 Claude
这是文档中隐藏的部分。
有两种方式可以阻塞一个阻塞事件。它们意味着完全不同的事情。
退出码 2 → 系统错误
Claude 认为某些东西出错了。工具不可用、资源缺失、环境损坏。它可能会尝试理解错误,也可能会尝试寻找替代方案。
#!/bin/bash
# 通过系统错误信号进行阻止
if [[ "$TOOL_INPUT" == *"prod.env"* ]]; then
echo "Environment error: target file locked" >&2
exit 2
fi
退出码 0 + JSON → 策略拒绝
Claude 会理解这是一个明确的业务规则,表明该操作不被允许。它不会试图绕过它。它接受决定并调整行为。
#!/bin/bash
# 通过策略决策阻止(对规则更友好)
TOOL_INPUT=$(cat)
FILE=$(echo "$TOOL_INPUT" | jq -r '.path // ""')
if [[ "$FILE" == *"prod.env"* ]]; then
echo '{
"decision": "deny",
"reason": "Writing to prod.env is not allowed. Use staging.env instead."
}'
exit 0
fi
# 允许其他所有操作
echo '{"decision": "allow"}'
exit 0
JSON 方法还能做更多事情:
→ 附加可读的拒绝原因
→ 通过 updatedInput 修改工具输入参数,然后再交给 Claude 使用
→ 通过 "decision": "ask" 请求人工确认,而不是自动拒绝
每个人都犯的错误:exit 1 没有任何作用。在 Unix 惯例中,它表示失败。但在 Claude Code 的 Hook 系统中,exit 1 是非阻塞的。Claude 会忽略它并继续执行。
只有 exit 2 或 exit 0 + JSON 才能真正影响执行流程。

Hooks 存在于何处
Hooks 不仅仅存在于你的个人 settings.json 中。它们可以在 4 个不同的地方注册。每个都有不同的作用域和生命周期。
-
设置级 Hooks — 常驻 Hooks。写在用户、项目或本地级别的
settings.json中。从 Claude Code 启动到结束都处于活动状态。在任务之间永远不会被清理。~/.claude/settings.json← 用户级别,你的机器
.claude/settings.json← 项目级别,与团队共享
.claude/settings.local.json← 本地覆盖,不提交 -
插件 Hooks — 随插件加载。一个插件捆绑了它自己的 CLAUDE.md、Skills 和 Hooks。当 Claude Code 加载插件时,它的 Hooks 会与主 Hooks 合并,并平等运行。没有优先级差异——它们共同参与。
一个硬性限制:插件子代理不能定义 Hooks。这是有意为之。子代理是一个受限制的执行单元。如果允许它注册 Hooks,就会让一个低权限的角色有能力修改执行流程控制。这破坏了安全模型。
-
技能 Hooks — 作用域限定在技能内。在技能被调用时注册。在技能完成时自动清理。它们不会污染全局环境。
--- name: planning-with-files hooks: PreToolUse: - matcher: "Write|Edit|Bash|Read" hooks: - type: command command: "cat task_plan.md 2>/dev/null | head -30 || true" PostToolUse: - matcher: "Write|Edit" hooks: - type: command command: "echo 'File updated. Update task_plan.md status.'" Stop: - hooks: - type: command command: "./scripts/final-check.sh" ---每当这个技能调用 Write、Edit 或 Bash 时,它首先打印任务计划的前 30 行。每次文件写入后,它会提醒 Claude 更新计划状态。当技能结束时,它会运行一个最终检查。
-
子代理 Hooks — 作用域限定在子代理内。与技能 Hooks 类似。临时、自动清理。一个额外的行为:如果你在子代理的前置元数据中注册了一个
StopHook,它会在运行时自动转换为SubagentStop。因为结束的是子代理,而不是整个会话。

多个 Hooks 如何合并
在任何时刻,来自多个层次的 Hooks 可能同时处于活动状态。
当一次 Write 操作触发 PreToolUse 事件时,它可能会同时匹配来自用户设置、项目配置和当前活动技能的 Hooks。
合并遵循三条规则。
规则 1:并行执行
所有匹配的 Hooks 同时运行。不是串行,也不是按优先级顺序。你的日志 Hook 和安全检查 Hook 同时开始,独立完成。Claude 等待所有结果后再做决定。
规则 2:自动去重
如果两个层次注册了完全相同的 Hook——相同的事件、相同的匹配器、相同的命令字符串——Claude Code 只保留一个副本并运行一次。
这在实践中很重要:如果同一个脚本出现在多个配置文件中,你不需要担心它会运行两次。但相反的情况:如果你希望同一个脚本在两个不同的条件下分别运行,请确保它们的命令字符串不同。
规则 3:最严格的结果胜出
当多个 Hooks 返回不同的决定时,Claude 选择最严格的那个。
deny > ask > allow
一个 deny 就够了。它来自哪个层次并不重要。
用户级 Hook: allow (普通写入没问题)
项目级 Hook: ask (.env 文件需要确认)
插件 Hook: deny (prod.env 被禁止)
─────────────────────────────
最终决定: deny (一个否决就够了)
为什么这样设计是有道理的:在安全系统中,允许需要所有人同意。拒绝只需要一个否决。这是每个严肃安全模型的工作原理。

两个值得研究的生产环境 Hook
理论很简单。让我们看两个生产环境的 Hook,并理解它们为什么这样设计。
案例 1:Superpowers 插件 — 在会话启动时注入上下文
Superpowers 插件为工程纪律提供了一个完整的技能系统:需求澄清、规划、测试驱动开发、代码审查。但它只注册了一个 Hook。
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|clear|compact",
"hooks": [
{
"type": "command",
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd\" session-start"
}
]
}
]
}
}
一个事件。一个 Hook。它是做什么的?它将 Superpowers 技能指令作为额外的上下文注入到每个会话中。每次会话启动时,Claude 自动获得正确的初始上下文。
其中的洞见:Hooks 并不总是需要阻止或批准。有时在正确的时机引入正确的信息就是全部工作。
案例 2:claude-code-warp 插件 — 将 Claude 的生命周期桥接到终端
Warp 是一个终端。当你在 Warp 中使用 Claude Code 时,Warp 根本不知道 Claude 在做什么——工作中、等待中、请求权限、完成。
claude-code-warp 插件用 6 个 Hooks 解决了这个问题:
SessionStart→ 向 Warp 发送初始化信息UserPromptSubmit→ 告诉 Warp:Claude 开始工作了PostToolUse→ 告诉 Warp:阻塞状态已清除Notification→ 当 Claude 空闲时触发 Warp 通知PermissionRequest→ 向 Warp 发送工具名称和输入预览Stop→ 读取会话、提取摘要、发送完成通知
Stop Hook 是最有趣的一个。它不仅仅说“完成”。它读取当前会话内容,提取最后一条用户提示和 Claude 的响应,截取到适合通知的长度,并向 Warp 的通知中心发送一个结构化的摘要。一个 Hook 将 Claude Code 的内部会话记录转化为用户实际上可以阅读的完成通知。
其中的洞见:Hooks 可以作为事件桥接器——将 Claude Code 的执行状态同步到本身不知道 Claude 的外部系统。

今天可以使用的实用 Hook
保护你的生产环境文件。一个脚本。复制粘贴即可使用。
创建 .claude/hooks/protect-prod.sh:
#!/bin/bash
# 从标准输入读取工具输入
TOOL_INPUT=$(cat)
# 从输入 JSON 中提取文件路径
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.path // .file_path // ""')
# 检查目标是否为生产配置文件
PROTECTED_PATTERNS=("prod.env" ".env.production" "prod-secrets" "production.yaml")
for pattern in "${PROTECTED_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo '{
"decision": "deny",
"reason": "'"$FILE_PATH"' is a protected production file. Use staging environment instead. If you genuinely need to edit production config, do it manually."
}'
exit 0
fi
done
# 不是受保护文件——允许
echo '{"decision": "allow"}'
exit 0
在 .claude/settings.json 中注册它:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/protect-prod.sh"
}
]
}
]
}
}
使其可执行:
chmod +x .claude/hooks/protect-prod.sh
现在,每次针对生产配置文件的写入和编辑操作,在 Claude 接触文件之前就会被阻止——并附带明确的原因。不是因为 Claude 记住了规则,而是因为代码强制执行了规则。

让 Hooks 发挥作用的思维模型
将 Claude Code 的执行流程想象成一个管道。
没有 Hooks:Claude 运行整个管道。CLAUDE.md 指导它。Skills 组织它。但执行本身是不间断的。
有 Hooks:你在管道的任何位置插入检查点。每个检查点运行真实的代码。它可以检查 Claude 即将做什么,决定是否允许,并可以选择在 Claude 使用输入之前修改它。
该系统有三个层次协同工作:
→ CLAUDE.md — 告诉 Claude 如何理解项目
→ Skills — 将 Claude 组织成可靠的工作流程
→ Hooks — 在关键执行点守卫边界
没有一个能替代其他。
没有 Hooks 的 CLAUDE.md:良好的指导,不一致的执行。没有 CLAUDE.md 的 Hooks:对模型不理解规则的严格执行。两者结合:在每个层面都有可靠的行为。
在你去构建 Hooks 之前,有一个警告。
Hooks 运行真实的代码。效果是即时的,有时是不可逆的。一个返回错误退出码的脚本可能会意外中断流程。一个退出逻辑处理不当的 Stop Hook 可能会让会话陷入循环。测试每个 Hook,就像测试生产代码一样。处理边界情况。处理错误路径。显式地记录失败。
Hooks 的力量在于它们不能被忽略。同样的特性也使得 Hooks 中的 BUG 代价高昂。
需要记住的 5 件事
- Hooks 不是提示词。它们是代码。无论模型状态如何,它们都会运行。
exit 1没有任何作用。使用exit 2表示系统错误。使用exit 0+ JSON 表示策略决策。- 事件是平级的。
PreToolUse和PermissionRequest独立触发。一个不会引起另一个。 - 最严格的结果胜出。来自任何层次的一个
deny就会阻止操作。allow需要所有人同意。 - 技能和子代理 Hooks 是临时的。它们在调用时注册,在完成时清理。它们不会污染全局状态。
- 原文链接: x.com/sairahul1/status/2...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~