第5章:Hooks 事件驱动自动化

在 AI Agent 辅助开发过程中,仅依赖提示词或规范文档并不足以保证关键操作的安全性。

例如,CLAUDE.md 可以提供项目约定,Skills 可以提供操作指导,但它们本质上仍属于“建议”或“引导”,无法强制阻止高风险行为。一旦模型或使用者在执行过程中遗漏检查,仍可能出现误提交敏感文件、执行危险命令、修改关键配置等问题。

Hooks 的作用,就是在 AI Agent 执行关键操作的前后插入强制校验与控制逻辑。它不是给模型的建议,而是一套可执行的拦截机制。

从设计思想上看,Hooks 类似于 Web 开发中的中间件、拦截器或请求处理链。无论是 Express Middleware、Django Middleware,还是 Spring Interceptor,其核心都是在目标操作执行前后加入额外处理逻辑。

在 AI Agent 场景中,Hooks 可以用于提交前检查、命令执行拦截、敏感信息扫描、权限控制以及操作审计,从而把“依赖自觉遵守规范”升级为“通过机制强制保障安全”。

整理后的 Markdown 如下:

5.1 Hooks 在 Claude 扩展体系中的定位

在深入分析技术细节之前,我们先将 Hooks 置于已学的扩展体系中进行定位。

前 4 章介绍的 3 种机制构成了一条“影响力递增”的谱系:CLAUDE.md 确立项目规范,由 Claude 在每次对话启动时读取;Skills 封装领域工作流,可在 Claude 需要时自动或手动触发;而子智能体则通过隔离上下文来实现任务委派。

这 3 种机制存在一个共同特征:它们均作用于 Claude 的认知层面。CLAUDE.md 告诉 Claude “应遵守何种规范”,Skills 指导其“遇到此类问题该如何处理”,子智能体则指示其“应将子任务委派给谁”。然而,这些本质上仍属于建议性指导。作为语言模型,Claude 在理论上具备忽略 Prompt 中任何约束的能力。

Hooks 的工作层面截然不同。它不作用于 Claude 的认知层,而是直接在系统执行层拦截其行为

当 Claude 试图调用 Bash 工具执行 rm -rf / 时,PreToolUse Hook 并非“劝说” Claude 放弃该操作,而是在系统层面直接阻断该工具调用的执行,这意味着 Claude 的决策被强制推翻,无法落地。

这种区别,用软件工程的术语来说,正是策略(Policy)与机制(Mechanism)的分离。CLAUDE.md 和 Skills 定义的是策略,即“应该怎么做”;而 Hooks 提供的是机制,即“一旦违反策略,将被物理阻止”。正如操作系统的文件权限管理不再依赖于应用程序的“自觉”,Hooks 对 Claude 的约束也不再依赖于 Prompt 的“引导”,而是通过系统底层的强制力来保障执行。

下表从 4 个维度对比了 CLAUDE.md、Skills 与 Hooks 三者的差异。

机制 工作层面 触发方式 约束性质 对 Claude 的控制 类比
CLAUDE.md 认知层 始终加载 建议 引导行为 交通标志
Skills 认知层 语义匹配 / 显式触发 指导 规范工作流 驾驶手册
Hooks 系统执行层 事件自动触发 强制 拦截 / 阻止 路障 / 限速器

这就好比,交通标志(CLAUDE.md)告诉你“这里限速 60 km/h”,驾驶手册(Skills)教你“弯道应该减速”,而路障 / 限速器(Hooks)则直接让你的车速达不到 120 km/h。

三者配合使用时效果最佳:交通标志让你知晓规则,驾驶手册让你掌握操作方法,而路障 / 限速器确保在你忘记规则或判断失误时,系统依然能守住安全底线。

5.2 事件生命周期: 17个事件

Claude Code 的事件系统已从早期版本的7个扩展至17个,全面覆盖 AI 会话从启动到终止、从主对话到子智能体协作的完整生命周期。

本节将这17个事件分为会话级事件、工具调用事件、子智能体事件、完成事件与较新的事件类型。我们不需要死记硬背全部17个事件,关键在于掌握其内在的分类逻辑与核心特征。

5.2.1 会话级事件

会话级事件负责管理整个会话的生命周期,主要包含以下3个关键事件。

1.SessionStart

在会话启动或恢复时触发。其核心能力是通过 CLAUDE_ENV_FILE 注入环境变量。Hook 脚本可向该文件写入 export 语句,使变量在后续所有 Bash 命令中生效。这意味着你可以在会话开始时自动配置开发环境。请看以下代码示例。

#!/bin/bash
# 指定该脚本使用 bash 解释器执行

# session-init.sh — SessionStart Hook
# 这是注释,说明这个脚本文件名可能叫 session-init.sh,用途是作为 SessionStart Hook

if [ -n "$CLAUDE_ENV_FILE" ]; then
# 判断 CLAUDE_ENV_FILE 变量是否存在且不为空
# CLAUDE_ENV_FILE 是 Claude 提供的环境变量注入文件路径
# 只有该文件路径存在时,才继续向其中写入环境变量配置

echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"
# 向 CLAUDE_ENV_FILE 文件中追加一行 export 语句
# 用于设置 NODE_ENV=development
# 后续 Claude 调用 Bash 工具执行命令时,会自动处于开发环境

echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
# 向 CLAUDE_ENV_FILE 文件中追加一行 export 语句
# 用于设置 DEBUG_LOG=true
# 后续 Bash 命令可以读取该变量,从而开启调试日志

fi
# if 判断结束

exit 0
# 脚本正常退出
# 退出码 0 表示执行成功

2.SessionEnd

在会话终止时触发。其匹配器(matcher)可区分不同的终止原因:clear(用户清除)、logout(登出)或 prompt_input_exit(用户退出输入)。

该事件常用于清理临时资源或记录会话统计信息。

3.PreCompact

在上下文压缩前触发。其匹配器可区分 manual(用户主动执行/compact)和 auto(上下文窗口满后自动压缩)。此事件适合在压缩前备份完整的对话记录。

5.2.2 工具调用事件

这是最核心的事件类别,涵盖了 Claude 每次工具调用的完整生命周期。工具调用事件主要包含以下5个关键事件。

1.PreToolUse(整个 Hooks 系统中最强大的事件)

在 Claude 决定调用某个工具之后、工具实际执行之前触发。它支持3种操作:允许(allow,绕过权限检查直接执行)、拒绝(deny,阻止执行并说明原因)、修改(updatedInput,调整输入参数后执行)。

“调整输入参数”这一能力尤其强大:它允许你在不中断操作的前提下,静默地为命令添加安全参数。请看以下代码示例。

{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"command": "rm -rf /tmp/test --dry-run"
}
}
}

上述示例将原本危险的 rm-rf /tmp/test 命令静默修改为 rm-rf /tmp/test --dry-run,既让 Claude 继续完成任务,又避免了文件被真正删除的风险。

2.PostToolUse

在工具成功执行后触发。虽然它无法撤销已发生的操作,但具备两项核心功能:一是通过 additionalContext 向 Claude 反馈额外信息(如代码 Lint 检查结果);二是对输出进行后处理(如自动格式化刚写入的文件)。此外,针对 MCP 场景,该事件还拥有专属能力:可通过 updatedMCPToolOutput 字段直接替换 MCP 工具的原始输出内容。

3.PostToolUseFailure

在工具执行失败后触发,主要用于错误告警以及提供纠正性反馈。

4.PermissionRequest

在权限对话框即将弹出时触发。它与 PreToolUse 的关键区别在于触发时机:PreToolUse 会在每次工具调用前无条件触发,而 PermissionRequest 仅在 Claude 需要用户手动确认权限时才被激活。通过该事件,你可以以编程方式自动批准或拒绝权限请求。请看以下代码示例。

{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedPermissions": {}
}
}
}

5.UserPromptSubmit

在用户提交输入后、Claude 开始处理之前触发。该事件常用于输入预处理或上下文注入场景,例如,在用户每次发送消息时,自动附加当前的 Git 分支信息。

5.2.3 子智能体事件

子智能体事件可与第3章介绍的子智能体机制协同使用。

1.SubagentStart

在子智能体启动时触发。其匹配器可根据子智能体的类型名称进行筛选,既支持内置类型(如 Bash、Explore、Plan),也涵盖在.claude/agents/目录中定义的自定义子智能体(如 code-reviewer)。虽然 SubagentStart 无法阻止子智能体的启动,但运行时会通过 additionalContext 注入关键上下文信息,例如,每当启动 code-reviewer 时,自动加载团队的编码规范。

2.SubagentStop

在子智能体完成任务后触发。其行为逻辑与全局 Stop 事件完全一致:既可以放行停止操作,也可以拦截该请求,强制子智能体继续工作直至满足特定的质量标准。此外,SubagentStop 的输入数据包含两个关键路径—transcript_path(主会话记录)和 agent_transcript_path(子智能体自身的对话记录)。借助这些信息,Hook 脚本能够复盘子智能体的完整工作流程,从而对其产出质量进行精准评估。

5.2.4 完成事件

1.Stop

在 Claude 完成整轮响应时触发。这是实现“质量门控”机制的核心:如果检测到输出内容未满足预设标准(如代码规范、安全策略等),可以通过设置 decision:”block”阻止会话结束,强制 Claude 继续修正或完善工作,直至符合要求。

2.Notification

在 Claude 发送系统通知时触发。其匹配器能够精准区分不同类型的通知,如 permission_prompt(权限请求)、idle_prompt(空闲提示)或 auth_success(认证成功)等。该事件常用于自定义通知渠道的集成,例如,将关键警报转发至 Slack 或钉钉,或在本地触发桌面弹窗提醒。

5.2.5 较新的事件类型

在2025年到2026年间,Claude Code 扩展了其事件体系,新增了几个关键事件类型以支持更复杂的协作与运维场景。

1.Teammateldle 与 TaskCompleted

专为多智能体团队协作设计。前者在队友智能体即将进入空闲状态时触发,后者在任务被标记为完成时触发。

2.ConfigChange

在配置文件发生变更时触发。该事件主要用于审计与合规,帮助开发者追踪设置变化历史,防止未经授权的配置修改。

3.WorktreeCreate 与 WorktreeRemove

分别对应 GitWorktree 的创建与删除操作。通过拦截这些事件,用户可以自定义版本控制工作流的初始化设置(如自动安装依赖)或清理逻辑(如删除临时构建产物)。

5.2.6 “能否阻止”: 最关键的维度

在全部17个事件中,“能否阻止”是最核心的分类维度,它决定了事件是用于“控制流程”还是仅用于“观察记录”。

具备阻止能力的事件包括 PreToolUse、PermissionRequest、UserPromptSubmit、Stop、SubagentStop、TeammateIdle、TaskCompleted、ConfigChange、WorktreeCreate。

其余事件(如 PostToolUse、Notification、SubagentStart 等)属于只读模式。它们主要用于读取上下文。注入额外的信息或触发侧边效应(如发送通知),但无法直接阻止或修改 Claude 的核心执行逻辑。

在日常开发与运维中,最常用的 3 个事件是:PreToolUse(工具执行前的“守门员”)、PostToolUse(工具执行后的“质量守卫”)、Stop(任务完成时的“质量门控”)。如果时间有限,优先精通 PreToolUse、PostToolUse 和 Stop,即可构建出健壮的自动化闭环。

5.3 配置体系: 6个位置,6种用途

Claude Code 的配置系统采用分层叠加机制,优先级从上至下依次降低。Hooks 的配置采用标准的 JSON 格式,并严格遵循六层优先级架构。这种设计允许开发者根据作用域灵活部署自动化逻辑。

Hooks 配置的 6 个层级与作用域

企业策略配置(managed-settings.json)
组织强制规定

项目配置(.claude/settings.json)
团队共享约定

项目本地配置(.claude/settings.local.json)
个人本地覆盖

用户全局配置(~/.claude/settings.json)
跨项目个人偏好

插件内置 Hooks (hooks/hooks.json)
随插件分发

子智能体 frontmatter 内联 Hooks
仅在该子智能体运行期间生效

项目配置(.claude/settings.json)是团队协作的核心载体。将其提交至 Git 仓库后,所有成员在克隆项目时即可自动同步团队约定的安全检查与自动化规则。项目本地配置(.claude/settings.local.json)已被 .gitignore 忽略,适用于需要覆盖团队默认配置的个人场景。用户全局配置(~/.claude/settings.json)则用于管理跨项目的个人偏好,如自定义日志格式或桌面通知方式。

配置结构采用3层嵌套设计:事件类型→matcher 组→Hook 处理器列表。请看以下代码示例。

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/block-dangerous.sh",
"timeout": 30
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "prettier --write \"$CLAUDE_FILE_PATH\""
}
]
}
]
}
}

matcher 字段用于指定该组 Hook 适用的工具范围。

  • Bash:匹配所有 Bash 调用。

  • Write|Edit:匹配 Write 或 Edit 工具(管道符|表示逻辑“或”)。

  • *:匹配所有工具。

对于 Stop、Notification、UserPromptSubmit 等生命周期事件,matcher 字段将被忽略,因为这些事件不针对特定工具。而在 SubagentStart 或 SubagentStop 事件中,matcher 匹配的是子智能体类型名称,而非工具名称。

PreToolUse:

“type”: “command” 表示这个 hook 的执行方式是:运行一条命令行命令

当 Claude 准备调用 Bash 工具时,会先执行:./.claude/hooks/block-dangerous.sh

PostToolUse:

$CLAUDE_FILE_PATH表示刚刚被 Claude 写入或编辑的文件路径。

例如 Claude 修改了 src/App.tsx 那么 hook 实际执行的命令大概就是 prettier --write "src/App.tsx" 所以这个 hook 的核心作用是:Claude 修改文件后,自动用 prettier 格式化该文件。

5.4 3种处理器类型: 确定性的阶梯

Hook 处理器包含 3 种类型,构成了一个“确定性递减、灵活性递增”的阶梯。具体选择取决于验证逻辑所需的判断力度。

5.4.1 command 类型: 确定性规则

该类型用于执行 Shell 命令或脚本。作为最常用且最可靠的类型,确定性规则永远比大模型的判断更为可信。请看以下代码示例。

{
"type": "command",
"command": "./.claude/hooks/check-security.sh",
"timeout": 30
}

command 类型的 Hook 通过 stdin 接收 JSON 格式的上下文数据(包含 session_id、tool_name、tool_input 等),通过 stdout 输出 JSON 格式的决策,并依据退出码表达最终意图。

  • 退出码 0:表示成功。系统将 stdout 中的 JSON 解析结果作为决策依据。

  • 退出码 2:表示有意阻止。系统将 stderr 的内容作为错误原因反馈给 Claude。

  • 其他退出码:表示脚本异常。stderr 内容仅在调试模式下可见,但不会阻断主流程。

退出码 2 的设计至关重要,它严格区分了“有意阻止操作”与“脚本自身故障”。脚本自身故障不应阻碍正常工作流,这正如烟雾报警器自身发生故障时,不应因此禁止人员进出大楼。

5.4.2 prompt 类型: 单次大模型评估

当验证逻辑需要一定的判断力,但不需要执行多步操作时,建议使用 prompt 类型。该类型会调用小型的模型(通常为 Haiku)对当前情况进行评估。请看以下代码示例。

{
"type": "prompt",
"prompt": "评估这段代码修改是否引入了安全漏洞。$ARGUMENTS",
"model": "claude-haiku-4-5",
"timeout": 30
}

其中,$ARGUMENTS 为占位符,运行时将被替换为 Hook 接收到的完整输入 JSON。大模型的响应需要遵循以下 JSON 格式。

允许通过的 JSON 格式。

{"ok": true, "reason": "代码修改安全,未引入已知漏洞模式"}

拒绝操作的 JSON 格式。

{"ok": false, "reason": "检测到潜在的 SQL 注入风险:用户输入未经转义直接拼接到查询字符串中"}

5.4.3 agent 类型: 多轮子智能体验证

当验证逻辑需要实际查看代码文件、执行搜索或多步操作才能得出结论时,应使用 agent 类型。该类型会启动一个子智能体,以便能够利用 Read、Grep、Glob 等工具进行多轮深度验证。请看以下代码示例。

{
"type": "agent",
"prompt": "检查所有修改的文件是否通过了单元测试。运行测试套件并验证结果。$ARGUMENTS",
"timeout": 120
}

agent 类型的子智能体最多运行50轮对话/操作后必须返回决策。 其响应格式与 prompt 类型完全一致,返回包含 ok(布尔值)和 reason(字符串)的 JSON 对象。


选择 Hook 处理器类型时,应遵循“能用 command 类型的不用 prompt 类型,能用 prompt 类型的不用 agent 类型”的降级原则(见下图)。

image-20260619151400096

确定性规则(如模式匹配、文件名检查、正则表达式)在速度和可靠性上永远优于大模型判断。只有当验证逻辑确实需要“理解力”(语义分析)或“检查代码能力”(多文件上下文检索)时,才考虑升级到 prompt 类型或 agent 类型。

5.5 hookSpecificOutput: 与 Claude 交流的协议

PreToolUse Hook 的输出格式于2025年迎来了重要升级。早期版本采用顶层的 decision 和 reason 字段,而新版本则推荐使用嵌套在 hookSpecificOutput 对象中的 permissionDecision 格式。尽管目前两种格式均受支持,但新代码应遵循新规范。请看以下代码示例。

{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "此命令试图删除受保护的系统目录",
"additionalContext": "受保护的路径模式:/etc, /usr, /var"
}
}

permissionDecision 支持 3 种值。

  • allow:绕过权限系统直接执行。

  • deny:阻止执行。

  • ask:交由用户确认。

其中,ask 是一个微妙却实用的选项,它并非自动拒绝,而是表达“我不确定,请由人来决定”的态度。

additionalContext 字段适用于所有事件类型,其内容将被注入 Claude 的上下文中。这一机制构建了一个高效的反馈闭环,例如,PostToolUse Hook 可通过 dditionalContext 将代码静态分析(Lint)结果反馈给 Claude,Claude 在接收到这些信息后会自动修复问题,整个过程不需要人工干预。

此外,所有事件均支持以下几个通用的顶层字段。

{
"continue": false,
"stopReason": "检测到安全违规,会话已终止",
"suppressOutput": false,
"systemMessage": "警告:此操作已被安全策略拦截"
}

"continue": false:相当于“紧急制动”。无论当前处于何种事件阶段,该设置都会立即终止Claude的处理。

"suppressOutput": false 的意思是:不要隐藏输出内容

"systemMessage":该字段的内容将直接显示给用户,而不会传递给 Claude.

5.6 工程实战一: 安全防护体系

让我们回到本章开篇那个令人棘手的案例。现在,我们将利用 Hooks 构建一套完整的安全防护体系,从以下 3 道防线为项目保驾护航:危险命令拦截、敏感文件保护及全量操作审计。

5.6.1 完整配置

将 5.6.2 小节到 5.6.4 小节的3个独立的脚本整合进 .claude/settings.json,可以形成了一套严密的纵深防御体系。具体配置如下。

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/block-dangerous.sh" // 危险命令拦截
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/protect-files.sh" // 敏感文件保护
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "./.claude/hooks/audit-log.sh" // 全量操作审计
}
]
}
]
}
}

这套配置构建了一个涵盖“事前拦截、事中防护、事后审计”的完整安全闭环:危险命令拦截→敏感文件保护→全量操作审计。通过这套组合机制,我们将原本依赖“人工谨慎”的脆弱流程,升级为“代码即法律”的自动化安全体系。

5.6.2 PreToolUse: 危险命令拦截

第一道防线旨在拦截可能引发灾难的 Bash 命令。该脚本在设计上蕴含了几个关键细节。

  • 调试信息输出至标准错误(stderr,即>&2),而非标准输出(stdout)。这是因为 stdout 必须严格保留用于输出 JSON 格式的决策结果。

  • 使用 jq 工具解析输入的 JSON 数据,避免了脆弱的手动字符串匹配。

  • 每一次拦截操作都附带清晰、具体的原因说明。

上面第一点的意思:

echo "DEBUG: Checking command: $COMMAND" >&2 里面的 >&2 很关键。它表示:把这条调试信息输出到 stderr,而不是 stdout。也就是标准错误输出。

如果写成 echo "DEBUG: Checking command: $COMMAND" 那它默认会输出到 stdout。结果 stdout 里可能变成这样:

DEBUG: Checking command: rm -rf /
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "拦截危险命令模式: rm -rf /"
}
}

这样就坏了,因为 stdout 不再是一个纯 JSON 了。前面多了一行:

DEBUG: Checking command: rm -rf /

Claude Code 解析 stdout 的时候可能会认为:

“这不是合法 JSON,我无法判断这个 hook 到底是 allow 还是 deny。”

请看以下代码示例 block-dangerous.sh

#!/bin/bash
# 声明这是 Bash 脚本
# .claude/hooks/block-dangerous.sh

# 读取 Claude Code 传进来的输入
set -e
INPUT=$(cat)

# 从输入 JSON 里提取 command 命令(调试信息输出至 stderr)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
echo "DEBUG: Checking command: $COMMAND" >&2

# 危险命令模式列表
DANGEROUS_PATTERNS=(
"rm -rf /"
"rm -rf ~"
"rm -rf \$HOME"
"> /dev/sd"
"mkfs."

":(){ :|:&};:" # Fork bomb(Fork 炸弹)
"chmod -R 777 /"
"git push --force origin main"
"git push --force origin master"
"git reset --hard origin"
"DROP DATABASE"
"DROP TABLE"
"TRUNCATE"
"curl.*| sh" # 危险的管道执行
"curl.*| bash"
)

# 遍历危险命令模式列表中的每一个 pattern
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
# 判断当前 Claude 准备执行的命令 COMMAND 里面,
# 是否包含某个危险模式 pattern
if [[ "$COMMAND" == *"$pattern"* ]]; then
# 如果匹配到了危险命令,就把拦截日志输出到 stderr
# 注意这里用 >&2,不污染 stdout
echo "BLOCKED: $pattern" >&2

# cat <<EOF 的意思是:
# 从下一行开始,直到遇到 EOF 为止,中间的所有内容都原样输出
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "拦截危险命令模式: $pattern"
}
}
EOF

# 退出码 2:表示有意阻止。系统将 stderr 的内容作为错误原因反馈给 Claude。
exit 2
fi
# 继续检查下一个危险模式
done

# 如果 for 循环全部跑完,都没有匹配到危险命令,说明这条命令暂时没有命中危险规则
# 所以输出 allow JSON,告诉 Claude Code: “允许这次工具调用继续执行”
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
exit 0

5.6.3 PreToolUse: 敏感文件保护

第二道防线专注于保护敏感文件免受意外修改。该 Hook 的匹配器配置为“Write|Edit”,确保仅在发生文件写入或编辑操作时触发,从而在源头阻断风险。

请看以下代码示例。 protect-files.sh

#!/bin/bash
# .claude/hooks/protect-files.sh

set -e
INPUT=$(cat)

FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')

if [ -z "$FILE_PATH" ]; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
exit 0
fi

FILENAME=$(basename "$FILE_PATH")

# 受保护的文件名模式
PROTECTED_FILES=(
".env"
".env.local"
".env.production"
"credentials.json"
"secrets.yaml"
"secrets.json"
"id_rsa"
"id_ed25519"
)

# 受保护的扩展名
PROTECTED_EXTENSIONS=("pem" "key" "p12" "pfx")

# 受保护的目录
PROTECTED_DIRS=(".git/" ".ssh/" "node_modules/")

# 检查目录
for dir in "${PROTECTED_DIRS[@]}"; do
if [[ "$FILE_PATH" == *"$dir"* ]]; then
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "不允许修改受保护目录中的文件:$dir"
}
}
EOF
exit 2
fi
done

# 检查文件名
for name in "${PROTECTED_FILES[@]}"; do
if [[ "$FILENAME" == "$name" ]]; then
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "不允许修改敏感文件:$name"
}
}
EOF
exit 2
fi
done

# 检查扩展名
EXT="${FILENAME##*.}"
for ext in "${PROTECTED_EXTENSIONS[@]}"; do
if [[ "$EXT" == "$ext" ]]; then
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "不允许修改密钥类文件:*.$ext"
}
}
EOF
exit 2
fi
done

echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'
exit 0

5.6.4 PostToolUse: 全量操作审计

第三道防线是全量操作审计。通过配置 matcher:”*”,该工具能够捕获并记录所有的工具调用。在合规性要求严格的企业环境中,这是不可或缺的机制,它清晰地回答了“Claude 在什么时间对什么目标执行了什么操作”这一核心审计问题。

请看以下代码示例。audit-log.sh

#!/bin/bash
# .claude/hooks/audit-log.sh

INPUT=$(cat)

LOG_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/logs"
mkdir -p "$LOG_DIR"

LOG_FILE="$LOG_DIR/audit-$(date +%Y-%m-%d).log"

TIMESTAMP=$(date -Iseconds)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')

echo "[$TIMESTAMP] $TOOL_NAME: $TOOL_INPUT" >> "$LOG_FILE"

echo '{}'
exit 0

5.7 工程实战二: 代码质量自动化

安全防护旨在 “防患于未然” ,而代码质量自动化则致力于 “确保卓越交付” 。本节的实战将深入展示 PostToolUse 与 Stop Hook 的协同工作机制。

5.7.1 PostToolUse: 自动格式化

每次 Claude 写入文件后,系统将自动触发格式化工具。该 Hook 的精妙之处在于实现了“关注点分离”:Claude 不需要感知项目具体采用何种格式化规范,只需要专注于代码逻辑编写,格式化过程将在后台无感完成。请看以下代码示例。

#!/bin/bash
# .claude/hooks/auto-format.sh

set -e
INPUT=$(cat)

FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')

if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
echo '{}'
exit 0
fi

EXTENSION="${FILE_PATH##*.}"

case "$EXTENSION" in
js|jsx|ts|tsx|json|md|css|scss|html)
if command -v npx &> /dev/null; then
npx prettier --write "$FILE_PATH" 2>/dev/null
echo '{"hookSpecificOutput":{"additionalContext":"已用 Prettier 格式化"}}'
fi
;;
py)
if command -v black &> /dev/null; then
black "$FILE_PATH" 2>/dev/null
echo '{"hookSpecificOutput":{"additionalContext":"已用 Black 格式化"}}'
fi
;;
go)
if command -v gofmt &> /dev/null; then
gofmt -w "$FILE_PATH" 2>/dev/null
echo '{"hookSpecificOutput":{"additionalContext":"已用 gofmt 格式化"}}'
fi
;;
*)
echo '{}'
;;
esac

exit 0

脚本中引人了 command -v 进行环境检查。如果检测到格式化工具未安装,Hook 将静默跳过而非抛出错误。这体现了“优雅降级”的设计原则:Hook 自身的异常不应阻塞核心工作流的正常运行。

5.7.2 PostToolUse: Lint 反馈循环

自动格式化确保了代码的“美观”,而 Lint 检查则保障了代码的“正确”。本小节介绍的示例的核心在于利用 additionalContext 将 Lint 检查结果反馈给 Claude,从而构建起“修改→检查→反馈→修复”的自动化闭环。请看以下代码示例。

#!/bin/bash
# .claude/hooks/lint-check.sh

set -e
INPUT=$(cat)

FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')

if [[ "$FILE_PATH" == *.js || "$FILE_PATH" == *.ts || "$FILE_PATH" == *.jsx || "$FILE_PATH" == *.tsx ]]; then
LINT_RESULT=$(npx eslint "$FILE_PATH" 2>&1) || true

if [ $? -ne 0 ]; then
ESCAPED=$(echo "$LINT_RESULT" | head -30 | jq -Rs '.')
echo "{\"hookSpecificOutput\":{\"additionalContext\":\"ESLint 发现问题:\n${ESCAPED}\"}}"
else
echo '{"hookSpecificOutput":{"additionalContext":"ESLint 检查通过"}}'
fi
else
echo '{}'
fi

exit 0

当 Claude 完成 JavaScript/TypeScript 文件的修改后,如果触发 Lint 错误,脚本会将错误详情注入 additionalContext。Claude 读取到该上下文后,将自动分析并修复问题。整个流程不需要人工干预,实现了从代码生成到质量达标的全自动选代。

5.7.3 Stop Hook: 测试质量门控

Stop Hook 是质量保证的一道防线:当 Claude 宣称任务完成时,自动触发测试套件。如果测试失败,系统将阻止会话结束并强制要求继续修复。请看以下代码示例。

#!/bin/bash
# .claude/hooks/run-tests.sh

INPUT=$(cat)

# 【关键机制】防止无限循环:检查 stop_hook_active 标志
# 如果该标志为 true,说明已经重试过一次,本次必须放行以避免死锁
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
if [ "$STOP_ACTIVE" = "true" ]; then
exit 0 # 终止拦截,允许 Claude 停止
fi

# 切换到项目目录
if [ -n "$CLAUDE_PROJECT_DIR" ]; then
cd "$CLAUDE_PROJECT_DIR"
fi

# 检测项目类型并运行测试
TEST_PASSED=true
TEST_RESULT=""

if [ -f "package.json" ] && grep -q '"test"' package.json; then
TEST_RESULT=$(npm test 2>&1) || TEST_PASSED=false
elif [ -f "pyproject.toml" ] || [ -f "pytest.ini" ]; then
TEST_RESULT=$(pytest 2>&1) || TEST_PASSED=false
elif [ -f "go.mod" ]; then
TEST_RESULT=$(go test ./... 2>&1) || TEST_PASSED=false
else
echo '{"hookSpecificOutput":{"additionalContext":"未检测到测试框架"}}'
exit 0
fi

if [ "$TEST_PASSED" = true ]; then
echo '{"hookSpecificOutput":{"additionalContext":"所有测试通过"}}'
else
# 截取前 50 行错误日志并转义
TEST_ESCAPED=$(echo "$TEST_RESULT" | head -50 | jq -Rs '.')

# 返回 block 决策,强制 Claude 继续工作
cat <<EOF
{
"decision": "block",
"reason": "测试失败,请修复后再停止",
"hookSpecificOutput": {
"additionalContext": $TEST_ESCAPED
}
}
EOF
fi

exit 0

脚本中的 stop_hook_active 字段是防止系统陷人“死循环”的关键所在,其逻辑类似于递归函数的终止条件。当 Stop Hook 执行失败时,若系统尝试修复并再次触发该 Hook,stop_hook_active 将被置为 true。

随后,脚本检测到该标志位便会选择放行,从而退出循环,正如递归函数必须设定终止条件,Stop Hook 也必须具备明确的退出机制。如果缺失这一检查,Claude 将陷人“修复一测试失败→再修复”的死循环,无法自行脱困。

5.8 子智能体 Hooks: 精准的上下文管理

在第3章中,我们了解到子智能体通过隔离上下文来实现任务委派。Hooks 系统为此提供了两种专属事件,SubagentStart 和 SubagentStop。然而,更为关键的是第三种机制是直接在子智能体的 Frontmatter 中定义 Hooks

5.8.1 全局与 Frontmatter: 精度问题

假设你拥有一个名为 db-reader 的子智能体,专门用于执行 SQL 查询。若需要审查其执行的每一条 Bash 命令以防范 SQL 注入风险,在全局 settings.json 中配置 Hook 并非最佳方案。因为全部配置会无差别地拦截所有 Bash 命令,涵盖编译代码、运行测试或安装依赖等与数据库无关的操作。这不仅浪费系统性能,还极易引发误拦截。

更优的解决方案是直接在子智能体的 Frontmatter 中定义 Hook。请看以下代码示例。

---
name: db-reader
description: 只读数据库分析工具
tools: Read, Grep, Glob, Bash

hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./.claude/hooks/check-sql-injection.sh"

Stop:
- hooks:
- type: prompt
prompt: "检查查询结果是否包含 PII(如姓名、邮箱、手机号)。若包含,请回复 ok: false 并要求进行脱敏处理。"
---

你是一名数据库分析专家。仅执行 SELECT 查询,严禁执行任何修改数据的 SQL 语句。

采用 Frontmatter 定义 Hook 的核心优势在于生命周期的紧密绑定:Hook 随子智能体的启动而激活,并在任务完成后自动清理。此外,配置与子智能体定义集成于同一文件中,可随 md 文件一同分发,用户不需要额外修改全局 settings.json,极大地降低了配置复杂度。

5.8.2 SubagentStart: 自动注入上下文

SubagentStart Hook 的典型应用场景是在子智能体启动时动态注入团队规范。例如,每当启动 code-reviewer 子智能体时,系统可自动注入团队的编码标准。请看以下代码示例。

{
"hooks": {
"SubagentStart": [
{
"matcher": "code-reviewer",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"SubagentStart\",\"additionalContext\":\"团队编码规范:使用 camelCase 命名,行长上限 100 个字符,公共 API 必须包含 JSDoc 注释\"}}'"
}
]
}
]
}
}

5.8.3 SubagentStop: 验证输出质量

SubagentStop Hook 可用于验证子智能体的工作成果是否达标。通过结合 agent_transcript_path 读取子智能体的完整交互记录,系统能够执行细粒度的质量验收。请看以下代码示例。

#!/bin/bash
# verify-review-quality.sh

INPUT=$(cat)
AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_type')
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active')

# 仅验证 code-reviewer 子智能体
if [ "$AGENT_TYPE" != "code-reviewer" ]; then
exit 0
fi

# 防止死循环(若当前已是 Stop Hook 触发阶段,则跳过)
if [ "$STOP_ACTIVE" = "true" ]; then
exit 0
fi

TRANSCRIPT=$(echo "$INPUT" | jq -r '.agent_transcript_path')

if [ -f "$TRANSCRIPT" ]; then
HAS_ISSUES=$(grep -c "issue\|问题\|bug" "$TRANSCRIPT" || true)

HAS_SUGGESTIONS=$(grep -c "suggest\|建议\|recommend" "$TRANSCRIPT" || true)

if [ "$HAS_ISSUES" -gt 0 ] && [ "$HAS_SUGGESTIONS" -eq 0 ]; then
echo '{"decision":"block","reason":"发现了问题但未提供修复建议,请补充每个问题的改进方案"}'
exit 0
fi
fi

exit 0

5.8.1 小节到 5.8.3 小节介绍的这3层防护机制各司其职,构建了完整的质量保障闭环。

  • FrontmatterHook:负责内部自检,确保子智能体自问“我的输出是否完整?”。

  • SubagentStart Hook:负责外部注入,在启动时赋予其必要的上下文(“给它必要的背景信息”)。

  • SubagentStop Hook:负责外部验收,在结束时严格核查“它的工作成果是否达标?”。

5.9 异步 Hooks: 后台执行不阻塞

默认情况下,Hook 脚本的执行是同步阻塞的,Claude 会暂停当前工作流,直至脚本执行完毕并返回结果。对于快速的模式匹配检查,这几毫秒的延迟通常可以忽略不计;然而,对于运行测试套件、调用外部 API 或发送通知等耗时操作,同步阻塞会显著拖慢 Claude 的响应速度,影响用户体验。

为此,2026年年初发布的 Claude Code 引入了异步 Hooks 支持。

{
"type": "command",
"command": "./.claude/hooks/run-tests-background.sh",
"async": true,
"timeout": 300
}

配置 "async": true 后,Hook 将在后台非阻塞运行。Claude 不需要等待其完成即可立即继续后续工作。当异步 Hooks 执行完毕后,其输出结果将在下一个对话轮次中自动传递给 Claude,供其参考或处理。

异步 Hooks 的两个关键限制决定了其适用边界。

  • 类型限制:仅 command 类型的 Hook 支持异步执行。prompt 和 agent 类型的 Hook 必须同步运行。

  • 拦截能力限制:异步 Hooks 无法阻止当前操作。由于主流程在 Hook 启动的瞬间就已经继续执行,异步 Hooks 失去了在操作发生前进行干预的时机。

因此,异步 Hooks 适用于日志记录、异步通知、后台数据验证、非关键性质量审计等“事后处理”任务,不适用于需要实时阻断的安全检查(如 SQL 注入防御、敏感信息过滤)。

5.10 环境变量与调试

5.10.1 Hooks 可用的环境变量

下表列出了 Hooks 可用的环境变量,涵盖了变量的作用域及其核心用途。

环境变量 作用域 核心用途
CLAUDE_PROJECT_DIR 所有 Hook 获取当前项目的根目录绝对路径
CLAUDE_SESSION_ID 所有 Hook 当前会话的唯一标识符
CLAUDE_TOOL_NAME 所有 Hook 触发当前 Hook 的工具名称
CLAUDE_FILE_PATH 所有 Hook 当前操作涉及的文件绝对路径(若适用)
CLAUDE_ENV_FILE 仅 SessionStart 环境变量持久化文件的路径
CLAUDE_NOTIFICATION 仅 Notification 包含具体的通知消息内容
CLAUDE_CODE_REMOTE 所有 Hook 布尔值标识,指示是否在远程 Web 环境中运行
CLAUDE_PLUGIN_ROOT 仅 Plugin Hook 插件安装的根目录路径

5.10.2 调试“三板斧”

调试 Claude Hook 脚本的3种核心方法如下。

第一种,将调试信息输出至 stderr。

由于 stdout 专用于输出 JSON 决策结果,所有调试信息必须重定向至 stderr。

echo "DEBUG: Checking file $FILE_PATH" >&2      # 调试信息
echo '{"decision": "allow"}' # JSON 决策

第二种,手动测试 Hook 脚本。

通过构造模拟输入直接验证脚本逻辑。

echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | ./.claude/hooks/block-dangerous.sh
echo "Exit code: $?"

第三种,使用 claude –debug 查看完整的 Hook 执行细节。

调试模式将显示匹配的 Hook 列表、各脚本的执行耗时和返回结果。

5.10.3 常见陷阱

在使用 Hooks 的过程中,有两个经常被忽视的问题。

问题一:若 Shell 配置文件(如 ~/.zshrc~/.bashrc)中包含无条件的 echo 语句(如用于输出欢迎信息),这些输出会污染标准输出(stdout),从而导致 JSON 解析失败。解决方法是使用 [[ $- == *i* ]] 条件判断将这些 echo 语句包裹起来,确保它们仅在交互式 Shell 中执行。

问题二:直接编辑 settings.json 后,Hook 往往不会立即生效。这是因为 Claude Code 仅在启动时捕获配置快照,运行期间对文件的修改不会自动同步。若需要生效,用户需要在 /hooks 菜单中确认变更,或重启当前会话。

5.11 工程设计方法论

面对具体的自动化需求,设计 Hook 方案需要明确以下 3 个核心维度。

  • 拦截时机(事件选择):操作前拦截,选用 PreToolUseUserPromptSubmit;操作后反馈,选用 PostToolUse;完成时检查,选用 StopSubagentStop;生命周期管理,选用 SessionStartSessionEnd

  • 判断方式(类型选择):规则明确(如模式匹配、文件检查),选用 command 类型;需要语义判断但输入充分,选用 prompt 类型;需要深度代码分析,选用 agent 类型。

  • 配置作用域(位置选择):团队通用规范,配置于 .claude/settings.json;个人偏好设置,配置于 ~/.claude/settings.json;子智能体专属检查,配置于 Frontmatter。

设计过程通常遵循“三步走”策略。

第一步,首先配置基于 PostToolUse 事件且匹配器为 matcher:"*" 的审计日志 Hook,以此观察 Claude 的实际工具调用模式,并积累数日的真实运行数据。

第二步,基于审计数据识别高风险操作模式,进而设计针对性的 PreToolUse 拦截规则。

第三步,逐步收紧拦截规则,同时始终保留日志记录功能,确保在发生误拦截时能够快速定位问题根源。


Hooks 是团队级的基础设施,而非个人实验玩具。

.claude/settings.json 中配置的 Hook 将对所有克隆该仓库的成员生效。若成员因不明原因被意外拦截,将严重阻碍工作流并引发挫败感。因此,务必遵循以下准则:

  • 在提交 Hook 配置前,必须与团队充分讨论并达成共识。
  • 每个拦截规则都必须附带清晰的原因说明,告知用户被拦截的具体原因。
  • 利用审计日志实时监控 Hook 的触发频率,以便及时发现并修正误拦截情况。

本章小结

Hooks 填补了 CLAUDE.md 和 Skills 无法覆盖的空白:它能在任务执行的关键节点,以系统级方式强制执行团队约定。作为 Claude Code 扩展机制中唯一运行于系统执行层而非认知层的组件,Hooks 不依赖 Claude 的理解能力或 Prompt 的引导,而是通过脚本逻辑直接拦截工具调用。

17 种事件类型覆盖了从会话启动到结束、从主对话到子智能体的完整流程。

3 种处理器类型(包括 commandpromptagent)形成了“确定性递减、灵活性递增”的能力阶梯。选型原则为:首先选用 command 类型,其次是 prompt 类型,最后考虑 agent 类型。

支持在子智能体的 Frontmatter 中配置内联 Hook,实现比全局配置更精准的管控。

引入异步 Hooks,可以有效解决长时间操作导致的阻塞问题。

从工程方法论的角度审视,Hooks 的设计严格遵循“先观测,后管控”的演进原则:首先利用 PostToolUse 事件记录审计日志,深入分析真实的工具调用模式与行为特征;然后基于积累的数据洞察,针对性地设计 PreToolUse 拦截规则,确保策略的准确性与必要性。内置 stop_hook_active 标志位,有效防止 Hook 逻辑自身触发无限递归调用。规定退出码 2 代表“有意阻止”,将其与系统错误明确区分,体现了系统在异常处理与状态定义上的深思熟虑。

Hooks 的核心价值不在于“扩展能力边界”(让 Claude 做更多),而在于“夯实执行底座”(让 Claude 做的每一件事都更可靠)。它将那些“理应发生却常被遗忘”的关键检查(如敏感文件保护、代码格式化规范、测试验收流程)从依赖自觉的“软约束”,升级为不可绕过的系统级“硬约束”。开篇案例中提到的惨痛教训,仅需要一个简单的 PreToolUse Hook 即可从根本上避免。