死磕clude code-Hooks 入门:从「相信 AI 守规矩」到「强制 AI 守规矩」

TL;DR: Hooks 是 Claude Code 的确定性控制层。模型负责推理,Hooks 负责在固定事件上记录、拦截、提醒和验证。它不是第二个 AI,而是纯脚本的规则引擎。
为什么需要 Hooks
CLAUDE.md 可以写"不要修改 .env 文件",但提示词约束本质上是非确定性的——模型可能遵守,也可能在上下文压力下忽略。Hooks 解决的是这个问题:用确定性脚本替代非确定性指令。

三个 Hooks 的独特价值:
系统架构
Hook 系统的核心设计是:模型做决策 → 触发生命周期事件 → 确定性脚本响应。模型不知道 Hook 的存在,也不参与 Hook 的执行。

┌──────────────────────────────────────────────────────────────┐
│ Claude Code 运行时 │
│ │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ │
│ │ 用户输入 │───→│ 模型推理 │───→│ 工具调用 │ │
│ └─────────┘ └──────────┘ └────┬────┘ │
│ │ │
│ ┌─────────────┼──────────────┐ │
│ │ │ │ │
│ PreToolUse PostToolUse PostToolUse │
│ 检查/阻断 记录/验证 Failure │
│ │ │ 错误处理 │
│ │ │ │ │
│ exit 0: 放行 ┌────┴────┐ │ │
│ exit 2: 阻断 │ 执行结果 │ │ │
│ │ └────┬────┘ │ │
│ ▼ ▼ ▼ │
│ [工具实际执行] [Hook 后处理] [Hook 后处理] │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 其他生命周期事件 │ │
│ │ Notification · Stop · SubagentStart/Stop │ │
│ │ InstructionsLoaded · PreCompact │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
关键设计决策:Hook 的执行结果通过退出码传递给 Claude Code 运行时,而不是注入到模型推理过程中。这意味着 Hook 不会"改变 AI 的想法",而是在物理层面控制工具调用的放行或阻断。
完整事件列表
每个事件对应一个触发时机和一组适用场景。理解触发时机是正确使用 Hook 的前提。
|
事件
|
触发时机
|
能否阻断
|
典型用途
|
| — | — | — | — |
| PreToolUse |
工具执行前
|
能(exit 2)
|
安全门禁、文件保护、命令过滤
|
| PostToolUse |
工具成功执行后
|
不能
|
自动格式化、变更记录、验证提醒
|
| PostToolUseFailure |
工具执行失败后
|
不能
|
错误收集、失败分析、重试建议
|
| Notification |
需要权限或空闲时
|
不能
|
审计日志、通知转发
|
| SubagentStart |
子代理启动时
|
不能
|
注入安全规则、输出格式约束
|
| SubagentStop |
子代理结束时
|
不能
|
收集结论、审计轨迹、结果摘要
|
| Stop |
主会话结束时
|
不能
|
会话总结、验证报告、审计记录
|
| InstructionsLoaded |
CLAUDE.md 和 rules 加载后
|
不能
|
动态上下文注入、环境检测
|
| PreCompact |
上下文压缩前
|
不能
|
保留关键信息的提示注入
|
PreToolUse 是唯一能阻断工具执行的事件。其他事件都是"观察者"——它们能记录和提醒,但不能阻止工具调用。这个设计是有意的:阻断是一种高风险操作,只应该在工具执行前的安全门禁中使用。
Hook 类型
Hook 有两种类型:command 和 prompt。
Command 类型
Shell 脚本。从 stdin 读取工具输入(JSON 格式),通过退出码传递决策,通过 stdout 传递消息。
stdin → JSON 格式的工具输入
stdout → 传递给 Claude 的消息(可选)
stderr → 日志输出(用户不可见)
exit 0 → 放行(或不阻断的通知)
exit 2 → 阻断(仅 PreToolUse 有效)
Command Hook 的确定性是整个系统的基础。同样的 stdin 输入,同样的退出码。没有"可能阻断",只有"阻断或不阻断"。
Prompt 类型
直接向 Claude 注入文本上下文。不执行脚本,不读 stdin,不做决策。纯粹的"额外提示"。
{
"type": "prompt",
"prompt": "每次修改文件后,提醒运行相关测试。"
}
Prompt Hook 适合注入静态规则——那些不需要根据工具输入动态判断的指令。它的优势是零执行成本,劣势是无法做条件判断。
类型选择决策
|
条件
|
用 Command
|
用 Prompt
|
| — | — | — |
|
需要读取工具输入
|
是
|
–
|
|
需要条件判断
|
是
|
–
|
|
需要阻断
|
是
|
–
|
|
需要执行外部命令
|
是
|
–
|
|
只需要注入静态文本
|
–
|
是
|
|
执行成本敏感
|
–
|
是(零成本)
|
真实配置
以下是一个包含多种 Hook 的 settings.json 配置:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/block-sensitive-files.sh"
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/block-dangerous-commands.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/log-file-changes.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/session-summary.sh"
}
]
}
],
"SubagentStart": [
{
"hooks": [
{
"type": "prompt",
"prompt": "你是一个受限执行环境中的子代理。不要修改 .env、证书或生产配置文件。所有发现必须附带文件路径和行号。"
}
]
}
]
}
}
配置解读:
-
• matcher 字段:正则表达式,匹配工具名称。
"Edit|Write"匹配 Edit 或 Write 工具。"Bash"匹配 Bash 工具。省略 matcher 则匹配所有工具。 -
• hooks 数组:同一事件可以有多个 Hook,按顺序执行。PreToolUse 中任意一个 Hook 返回 exit 2 就会阻断。
-
• command 路径:建议使用
.claude/hooks/目录统一管理,路径相对于项目根目录。 -
• 多层配置:Hook 可以在全局
settings.json和项目级.claude/settings.json中同时存在,项目级优先。
三类 Hook 的工程定位
记录型 Hook
定位:观察者。记录工具调用、参数和结果,不影响执行流程。
适合场景:
-
• 审计日志:记录每次文件修改和命令执行
-
• 变更追踪:维护会话内的修改文件列表
-
• 行为分析:统计 Claude Code 使用了哪些工具、频率如何
不适合:安全门禁(无法阻断)、上下文注入(不操作模型输入)
提示型 Hook
定位:上下文增强。在不消耗用户消息的前提下注入额外指令。
适合场景:
-
• 提醒规则:“修改后端代码后运行 API 测试”
-
• 注入约束:“只读审查,不要修改文件”
-
• 环境信息:“当前分支是 release/v2.3,不要合并到 main”
不适合:条件判断(无法根据输入动态决定)、阻断操作(无法阻止工具执行)
阻断型 Hook
定位:安全门禁。在工具执行前拦截高风险操作。
适合场景:
-
• 文件保护:禁止修改
.env、证书、生产配置 -
• 命令过滤:拦截
rm -rf、git push --force、生产数据库写入 -
• 路径约束:限制 Claude Code 只能操作指定目录
不适合:业务逻辑判断(Hook 应该保持简单)、频繁触发的检查(影响开发体验)
类型选择矩阵
我需要做什么?
│
├─ 记录发生了什么 → 记录型(PostToolUse / Stop)
│
├─ 给 Claude 额外指令 → 提示型(prompt Hook)
│
├─ 阻止危险操作 → 阻断型(PreToolUse, exit 2)
│
└─ 既要记录又要阻止 → 两个 Hook:PreToolUse 阻断 + PostToolUse 记录
Hook 执行流程
以一次 Edit 工具调用为例,展示完整的 Hook 执行流程:

用户:"修改 auth.ts 的登录逻辑"
Claude Code 推理 → 决定调用 Edit 工具修改 auth.ts
│
├─ [1] PreToolUse 触发
│ ├─ block-sensitive-files.sh 运行
│ │ ├─ stdin: {"tool_name":"Edit","tool_input":{"file_path":"/src/auth.ts",...}}
│ │ ├─ 检查文件路径是否匹配敏感模式
│ │ ├─ auth.ts 不匹配 → exit 0(放行)
│ │ └─ stdout: 空
│ │
│ └─ 所有 PreToolUse Hook 放行 → 继续执行
│
├─ [2] Edit 工具执行
│ └─ 文件修改成功
│
├─ [3] PostToolUse 触发
│ ├─ log-file-changes.sh 运行
│ │ ├─ stdin: {"tool_name":"Edit","tool_input":{"file_path":"/src/auth.ts"},"result":"success"}
│ │ ├─ 记录变更到审计日志
│ │ └─ exit 0
│ │
│ └─ 所有 PostToolUse Hook 完成
│
└─ [4] Claude Code 向用户展示修改结果
--- 如果 Edit 工具执行失败 ---
├─ [3'] PostToolUseFailure 触发
│ └─ 错误日志记录
│
--- 如果 PreToolUse 阻断 ---
├─ [1'] PreToolUse 返回 exit 2
│ ├─ 工具调用被取消
│ └─ stdout 中的消息展示给 Claude("BLOCK: ...")
└─ Claude 根据阻断消息调整行为
退出码的完整语义:
|
退出码
|
含义
|
适用事件
|
| — | — | — |
|
0
|
放行 / 正常完成
|
所有
|
|
2
|
阻断工具执行
|
仅 PreToolUse
|
|
其他
|
异常,等同于放行
|
所有
|
设计意图:异常时不阻断。Hook 脚本出错(语法错误、运行时异常)不应该让 Claude Code 不可用。这和 fail-open 的安全模型一致——Hook 是附加控制层,不是核心依赖。
Hook 与 Permission System 的关系
Claude Code 有两层控制:Permission System 和 Hooks。

用户请求 → Claude 推理 → 工具调用决策
│
┌──────┴──────┐
│ Permission │ 第一层:用户显式授权
│ System │ (允许/拒绝特定工具)
└──────┬──────┘
│ 通过
┌──────┴──────┐
│ Hooks │ 第二层:规则引擎
│ (PreToolUse)│ (确定性脚本检查)
└──────┬──────┘
│ 通过
┌──────┴──────┐
│ 工具执行 │ 实际执行
└─────────────┘
两者的职责边界:
-
• Permission System:管理"这个工具能不能用"。粗粒度的开/关控制。用户驱动。
-
• Hooks:管理"这次调用安不安全"。细粒度的条件检查。规则驱动。
Permission System 说"Bash 可以用",Hooks 说"Bash 可以用,但不能执行 rm -rf"。两者互补,不替代。
Matcher 配置详解
Matcher 是 PreToolUse 和 PostToolUse 的路由机制。它决定一个 Hook 只对哪些工具生效。
{
"matcher": "Edit|Write",
"hooks": [...]
}
Matcher 的值是正则表达式,匹配工具名称:
|
Matcher
|
匹配的工具
|
| — | — |
| "Edit" |
Edit
|
| "Edit|Write" |
Edit 或 Write
|
| "Bash" |
Bash
|
| ".*" |
所有工具
|
| "mcp__.*" |
所有 MCP 工具
|
|
省略
|
所有工具
|
常见的 Matcher 配置策略:
// 策略 1:只监控文件写入工具
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/check-file-writes.sh" }]
}
// 策略 2:只监控命令执行工具
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/check-commands.sh" }]
}
// 策略 3:监控所有 MCP 工具调用
{
"matcher": "mcp__.*",
"hooks": [{ "type": "command", "command": "bash .claude/hooks/audit-mcp-calls.sh" }]
}
// 策略 4:全局审计(所有工具)
{
"hooks": [{ "type": "command", "command": "bash .claude/hooks/audit-all.sh" }]
}
Hook 的 stdin 数据格式
Command Hook 通过 stdin 接收 JSON 格式的工具调用信息。理解这个格式是编写 Hook 脚本的基础。
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/src/auth.ts",
"old_string": "const token = req.headers.authorization;",
"new_string": "const token = req.headers.authorization?.split(' ')[1];"
}
}
Bash 工具的输入格式:
{
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf node_modules && npm install",
"description": "Clean install dependencies"
}
}
Hook 脚本中读取 stdin 的标准写法:
#!/bin/bash
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
注意 // empty 的用法:当字段不存在时返回空字符串而不是 “null”。这在编写通用 Hook 时很重要——不是所有工具都有 file_path,也不是所有工具都有 command。
Hook 配置的层级
Hook 可以在三个层级配置,优先级从高到低:
|
层级
|
文件路径
|
作用域
|
典型用途
|
| — | — | — | — |
|
项目级
| .claude/settings.json |
当前项目
|
项目特定的文件保护和命令过滤
|
|
用户级
| ~/.claude/settings.json |
所有项目
|
全局审计日志、个人偏好
|
|
企业级
|
管理员统一配置
|
团队所有成员
|
安全合规、强制策略
|
配置合并规则:
-
• 同一事件的所有层级 Hook 都会执行,不覆盖
-
• 执行顺序:企业级 → 用户级 → 项目级
-
• PreToolUse 阻断:任一层级的 Hook 返回 exit 2 都会阻断
这个设计允许团队设置全局安全策略(企业级),同时不阻止个人或项目添加额外的 Hook。
失败案例:未测试的 Hook 阻断了所有工具调用
经过
一个 4 人前端团队决定为项目添加 PreToolUse Hook,阻止对 package.json 的意外修改。开发者在周五下午写了以下 Hook:
#!/bin/bash
# .claude/hooks/block-package-json.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
if [[ "$FILE_PATH" == *"package.json"* ]]; then
echo "BLOCK: package.json 修改需要团队 review"
exit 2
fi
看起来没问题。但周一早上,团队成员开始报告 Claude Code 几乎不可用:
-
• 任何涉及文件的操作都被阻断
-
• 即使是读取
package.json也被拦截 -
• Claude Code 无法完成任何文件编辑任务
根因
Hook 脚本有两个 bug:
修复
分两步修复:
第一步:立即恢复。禁用 Hook(从 settings.json 中移除或注释),让团队恢复工作。
第二步:正确实现。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/block-package-json.sh"
}
]
}
]
}
}
#!/bin/bash
# .claude/hooks/block-package-json.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# 精确匹配根目录的 package.json
if [[ "$FILE_PATH" == "*/package.json" ]]; then
echo "BLOCK: 根目录 package.json 修改需要团队 review。使用 npm 命令管理依赖。"
exit 2
fi
exit 0
改动要点:
-
• 添加
"matcher": "Edit|Write"确保 Hook 只对写入工具生效 -
• 路径匹配改为
*/package.json,只匹配根目录的 package.json -
• 显式
exit 0放行不匹配的情况 -
• 阻断消息包含具体的替代操作建议
教训
Hook 部署策略
推荐的渐进式部署流程:
第 1 周:记录模式
├─ 所有 Hook 以 exit 0 运行
├─ 只记录日志(stderr 或文件)
├─ 观察触发频率和匹配准确性
└─ 收集误判和遗漏样本
第 2 周:提醒模式
├─ 对匹配的调用输出提示信息(stdout)
├─ 仍然 exit 0(不阻断)
├─ 用户可以看到 Hook 的判断结果
└─ 根据反馈调整匹配规则
第 3 周起:阻断模式
├─ 对确认的高风险操作返回 exit 2
├─ 保留日志和提示信息
├─ 定期审查阻断记录
└─ 持续优化匹配规则
这个策略的核心逻辑是:Hook 的误判成本很高。误阻断会直接破坏开发体验,误放行会留下安全漏洞。先用观察模式积累数据,再用提醒模式验证可读性,最后才用阻断模式上线。
Hook 审计模板
每个上线的 Hook 应该有对应的文档记录。以下是一个审计模板:
## Hook 审计记录
### 基本信息
- 名称: block-sensitive-files.sh
- 事件: PreToolUse
- Matcher: Edit|Write
- 类型: command(阻断)
### 输入
- 格式: JSON(stdin)
- 关键字段: tool_input.file_path
### 输出
- exit 0: 文件路径不在敏感列表中,放行
- exit 2: 文件路径匹配敏感模式,阻断
- stdout: 阻断原因说明
### 匹配规则
- .env / .env.*
- *.pem / *.key
- infra/prod/**
- migrations/**
### 禁用方法
从 .claude/settings.json 的 hooks.PreToolUse 数组中移除此条目
### 上线日期
2025-03-15
### 上次审查
2025-04-01 - 确认无误判
这个模板确保每个 Hook 的行为、触发条件、输出和禁用方法都有据可查。当 Hook 出现问题时,任何人都能快速理解和处理。
交叉参考
-
• 23 PreToolUse 防护[1]:PreToolUse Hook 的完整实现指南,包含文件保护和命令过滤的真实脚本
-
• 24 PostToolUse / Stop 验证[2]:工具执行后的自动验证和会话结束时的结果记录
-
• 25 Subagent Hooks[3]:给子代理注入上下文和收集结果的 Hook 配置
-
• 26 Hook 设计原则[4]:小、确定、可解释、可回滚的四条原则和工程实践
Hook 与 Rules 和 CLAUDE.md 的决策矩阵
三种机制都能约束 Claude Code 的行为,但工程定位完全不同。选错机制会导致约束无效或维护困难。
决策维度 Hook Rules (.md) CLAUDE.md
──────────────────────────────────────────────────────────────────────
执行确定性 100% 确定 非确定(提示词) 非确定(提示词)
上下文消耗 零 低 高
可阻断工具调用 是(PreToolUse) 否 否
可审计 是(退出码+日志) 否 否
条件判断能力 是(脚本逻辑) 否 否
运行时成本 有(进程开销) 无 无
适合约束类型 硬性规则 软性指导 项目上下文
修改后生效 立即 下次对话加载 下次对话加载
选择流程:
要约束的行为是什么?
│
├─ 绝对不能违反的规则(禁止修改 .env、禁止 rm -rf)
│ → Hook(PreToolUse,exit 2 阻断)
│ 理由:提示词约束不可靠,必须用确定性脚本
│
├─ 建议性指导(修改后端代码要运行测试、PR review 关注安全)
│ → Rules(.claude/rules/ 目录下的 .md 文件)
│ 理由:不是硬性约束,作为提示词注入更合适
│
├─ 项目级上下文(项目架构说明、技术栈、代码约定)
│ → CLAUDE.md
│ 理由:这是上下文信息,不是约束。模型需要这些信息做出正确决策
│
└─ 多层叠加(重要约束需要多层防护)
例如:禁止修改 workflow 文件
Layer 1:CLAUDE.md 中声明"Don't modify .github/workflows/"
Layer 2:Rules 中强调"Workflow files are protected"
Layer 3:Hook 阻断(PreToolUse,匹配 .github/workflows/ 路径)
只有 Layer 3 是可靠的,Layer 1-2 是提示词级的软约束
常见错误:把所有约束都写在 CLAUDE.md 里。一个 2000 行的 CLAUDE.md 里有 100 行是"不要做 X"、“不要做 Y”。这些应该分流——硬性约束用 Hook,软性指导用 Rules,CLAUDE.md 只保留上下文信息。
Hook 脚本系统设计分析
理解 Hook 脚本的输入、输出和环境变量设计,是编写可靠 Hook 的基础。
输入系统
stdin(标准输入):
格式:JSON
内容:工具调用的完整信息
PreToolUse 输入示例:
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/src/auth.ts",
"old_string": "...",
"new_string": "..."
}
}
PostToolUse 输入示例(额外包含执行结果):
{
"tool_name": "Edit",
"tool_input": { ... },
"tool_result": {
"status": "success",
"output": "File edited successfully"
}
}
Bash 工具的特殊字段:
{
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run tests",
"timeout": 120000
}
}
输出系统
stdout(标准输出):
- 内容会被传递给 Claude 作为上下文消息
- PreToolUse 的 stdout 在 exit 2 时作为阻断原因展示
- PostToolUse 的 stdout 作为补充信息注入对话
- 保持简短:建议 ≤ 200 字符
- 不要输出 JSON 到 stdout——那是给模型看的信息,不是日志
stderr(标准错误):
- 内容写入 Hook 日志,用户不可见
- 适合记录调试信息、审计日志
- 不会影响 Claude 的推理过程
退出码:
0 → 正常完成/放行
2 → 阻断(仅 PreToolUse 有效)
其他 → 异常,等同于放行(fail-open)
环境变量
Hook 脚本可用的关键环境变量:
CLAUDE_PROJECT_DIR 项目根目录的绝对路径
用途:构建相对于项目的文件路径
CLAUDE_SESSION_ID 当前会话的唯一标识
用途:关联同一会话的多次 Hook 调用
HOME 用户主目录
用途:访问全局配置或日志目录
PATH 系统 PATH
用途:确保脚本能找到需要的命令
注意:环境变量中不包含 API key 或 secrets。
Hook 脚本不应该尝试读取 Claude Code 的内部状态。
完整的每种事件类型配置示例
Notification Hook
Notification 事件在 Claude Code 需要用户权限确认或会话空闲时触发。
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/forward-notification.sh"
}
]
}
]
}
}
#!/bin/bash
# .claude/hooks/forward-notification.sh
# 将通知转发到团队的 Slack 频道(用于监控 AI 行为)
INPUT=$(cat)
MESSAGE=$(echo "$INPUT" | jq -r '.message // "Claude Code notification"')
# 发送到 Slack webhook(URL 存储在环境变量中)
if [ -n "$SLACK_WEBHOOK_URL" ]; then
curl -s -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{\"text\": \"[Claude Code] $MESSAGE\"}" > /dev/null 2>&1
fi
exit 0 # Notification 永远不阻断
InstructionsLoaded Hook
InstructionsLoaded 在 CLAUDE.md 和 rules 加载完成后触发。适合注入动态上下文。
{
"hooks": {
"InstructionsLoaded": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/inject-runtime-context.sh"
}
]
}
]
}
}
#!/bin/bash
# .claude/hooks/inject-runtime-context.sh
# 注入运行时环境信息,让 Claude 了解当前状态
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
HAS_STASH=$(git stash list 2>/dev/null | head -1 | wc -l)
UNCOMMITTED=$(git status --porcelain 2>/dev/null | wc -l)
echo "运行时上下文:"
echo "- 当前分支: $BRANCH"
echo "- 未提交的变更: $UNCOMMITTED 个文件"
if [ "$HAS_STASH" -gt 0 ]; then
echo "- 注意:有 git stash 存在"
fi
if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then
echo "⚠️ 警告:当前在主分支上操作。建议创建 feature 分支。"
fi
exit 0
PreCompact Hook
PreCompact 在上下文压缩前触发。适合注入"必须保留"的关键信息提示。
{
"hooks": {
"PreCompact": [
{
"hooks": [
{
"type": "prompt",
"prompt": "压缩上下文时,请保留以下关键信息:(1) 当前任务的最终目标 (2) 已经完成的步骤和结果 (3) 还需要完成的具体步骤 (4) 任何已经发现但未解决的错误。不要保留中间探索过程的细节。"
}
]
}
]
}
}
SubagentStart / SubagentStop Hook
{
"hooks": {
"SubagentStart": [
{
"hooks": [
{
"type": "prompt",
"prompt": "你是受限执行环境中的子代理。不要修改 .env、证书或生产配置文件。所有发现必须附带文件路径和行号。如果发现安全问题,立即停止并报告。"
}
]
}
],
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/collect-subagent-result.sh"
}
]
}
]
}
}
#!/bin/bash
# .claude/hooks/collect-subagent-result.sh
# 收集子代理执行结果,写入审计日志
INPUT=$(cat)
AGENT_NAME=$(echo "$INPUT" | jq -r '.agent_name // "unknown"')
RESULT=$(echo "$INPUT" | jq -r '.result // "no result"')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
LOG_ENTRY="[$TIMESTAMP] Agent: $AGENT_NAME | Result: $(echo "$RESULT" | head -c 500)"
# 写入审计日志
echo "$LOG_ENTRY" >> "${CLAUDE_PROJECT_DIR}/.claude/agent-audit.log"
exit 0
Hook 执行生命周期
完整的 Hook 执行生命周期,从注册到触发到结果处理:
注册阶段:
settings.json 加载 → 解析 hooks 配置
→ 验证 command 路径是否存在(不存在则跳过并警告)
→ 验证 prompt 是否非空
→ 注册到对应事件的监听列表
触发阶段:
Claude Code 运行时事件发生
→ 按事件类型查找匹配的 Hook 组
→ 如果有 matcher,用正则匹配工具名称
→ 按注册顺序执行匹配的 Hook
执行阶段(Command 类型):
1. fork 子进程
2. 通过 stdin 传入 JSON 数据
3. 设置环境变量(CLAUDE_PROJECT_DIR 等)
4. 等待执行完成(受 timeout 约束)
5. 收集 stdout、stderr 和退出码
6. 处理超时(超时视为异常,等同于放行)
结果处理阶段:
PreToolUse:
exit 0 → 放行,继续下一个 Hook 或执行工具
exit 2 → 阻断,取消工具调用,stdout 作为阻断原因
其他/超时 → 等同于放行(fail-open)
stdout → 注入为 Claude 的上下文信息
PostToolUse / PostToolUseFailure:
退出码不影响工具执行(已经完成)
stdout → 注入为 Claude 的上下文信息
stderr → 写入日志
Stop / Notification / SubagentStop:
退出码无实际效果(观察者角色)
stdout → 注入为上下文(如果会话还在继续)
事件触发时序图
会话生命周期中的 Hook 触发时序:
InstructionsLoaded ──────────────────────────────────────────
│ CLAUDE.md 和 rules 加载完成后触发
│ 时机:会话开始时,只触发一次
▼
用户输入 ─── Claude 推理 ─── 决定调用工具
│
PreToolUse ────────────────────
│ 检查/阻断
├─ exit 2 → 阻断,回到 Claude 推理
└─ exit 0 → 放行
│
工具执行(实际调用)
│
┌─────────┴─────────┐
│ │
成功执行 执行失败
│ │
PostToolUse PostToolUseFailure
记录/验证 错误处理/日志
│ │
└─────────┬─────────┘
│
回到 Claude 推理(可能继续调用工具)
│
[重复上述循环]
│
┌──────────────────────────────────┤
│ │
SubagentStart Notification
│ 子代理启动时触发 │ 需要权限确认或空闲时触发
│ 注入安全规则 │ 审计日志、通知转发
▼ ▼
[子代理内部循环] [继续等待或用户响应]
│
SubagentStop
│ 子代理结束时触发
│ 收集结果、审计轨迹
▼
│
用户结束会话
│
Stop ─────────────────────────
│ 会话结束时触发
│ 会话总结、验证报告、审计记录
▼
PreCompact ────────────────────
│ 上下文压缩前触发
│ 注入"必须保留"的关键信息提示
▼
[压缩后继续会话]
权衡
Hook 是代码,不是魔法。它会失败、阻塞、误判。越靠近阻断逻辑,越要保持小而确定。一个 200 行的 Hook 脚本比没有 Hook 更危险——因为它给了你虚假的安全感,同时引入了不确定性。
Hook 解决不了所有安全问题。它能阻止 Claude Code 执行 rm -rf /,但阻止不了模型生成一段看起来正确但实际有逻辑漏洞的代码。安全是一个分层系统,Hook 是其中一层,不是全部。
交叉参考
-
• 23 PreToolUse 防护[1]:PreToolUse Hook 的完整实现指南,包含文件保护和命令过滤的真实脚本
-
• 24 PostToolUse / Stop 验证[2]:工具执行后的自动验证和会话结束时的结果记录
-
• 25 Subagent Hooks[3]:给子代理注入上下文和收集结果的 Hook 配置
-
• 26 Hook 设计原则[4]:小、确定、可解释、可回滚的四条原则和工程实践
引用链接
[1] 23 PreToolUse 防护: ./23-pretooluse-guardrails.md[2] 24 PostToolUse / Stop 验证: ./24-posttooluse-stop-verification.md[3] 25 Subagent Hooks: ./25-subagent-hooks.md[4] 26 Hook 设计原则: ./26-hook-design-principles.md

更多推荐
所有评论(0)