Claude Code Hooks:被忽视的最强功能

TAB 发布于 2026-06-25 阅读 126

本文深入讲解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 对象。每个键都是一个事件名称——PreToolUsePostToolUseStop。你希望在哪个节点干预?在这里注册该事件。

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 执行的每个关键节点:

→ 会话启动和停止
→ 工具调用前后
→ 权限请求
→ 文件变更
→ 子代理任务
→ 通知

大多数人会搞错的一件事是:事件是平级的,而不是父子关系。

PreToolUsePermissionRequest 经常会接连触发。这看起来像是其中一个引起另一个。但实际上不是。它们是完全独立的干预点。每个都有自己的匹配规则,并且互不干扰地执行。

事件分为两种类型:

主流程事件 — 贯穿核心执行路径。这些事件可以阻止 Claude。PreToolUsePermissionRequestPostToolUseStop

旁路事件 — 观察和通知通道。它们在正确时机触发,但不会改变主流程。NotificationConfigChange

它们之间的关键区别:

→ 阻塞: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 2exit 0 + JSON 才能真正影响执行流程。

图像

Hooks 存在于何处

Hooks 不仅仅存在于你的个人 settings.json 中。它们可以在 4 个不同的地方注册。每个都有不同的作用域和生命周期。

  1. 设置级 Hooks — 常驻 Hooks。写在用户、项目或本地级别的 settings.json 中。从 Claude Code 启动到结束都处于活动状态。在任务之间永远不会被清理。

    ~/.claude/settings.json ← 用户级别,你的机器
    .claude/settings.json ← 项目级别,与团队共享
    .claude/settings.local.json ← 本地覆盖,不提交

  2. 插件 Hooks — 随插件加载。一个插件捆绑了它自己的 CLAUDE.md、Skills 和 Hooks。当 Claude Code 加载插件时,它的 Hooks 会与主 Hooks 合并,并平等运行。没有优先级差异——它们共同参与。

    一个硬性限制:插件子代理不能定义 Hooks。这是有意为之。子代理是一个受限制的执行单元。如果允许它注册 Hooks,就会让一个低权限的角色有能力修改执行流程控制。这破坏了安全模型。

  3. 技能 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 更新计划状态。当技能结束时,它会运行一个最终检查。

  4. 子代理 Hooks — 作用域限定在子代理内。与技能 Hooks 类似。临时、自动清理。一个额外的行为:如果你在子代理的前置元数据中注册了一个 Stop Hook,它会在运行时自动转换为 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 件事

  1. Hooks 不是提示词。它们是代码。无论模型状态如何,它们都会运行。
  2. exit 1 没有任何作用。使用 exit 2 表示系统错误。使用 exit 0 + JSON 表示策略决策。
  3. 事件是平级的。PreToolUsePermissionRequest 独立触发。一个不会引起另一个。
  4. 最严格的结果胜出。来自任何层次的一个 deny 就会阻止操作。allow 需要所有人同意。
  5. 技能和子代理 Hooks 是临时的。它们在调用时注册,在完成时清理。它们不会污染全局状态。
  • 原文链接: x.com/sairahul1/status/2...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论