一、before Hook详解
before Hook 在事件执行之前触发,是 Claude Code Hooks 系统中最重要的控制节点。它的核心价值在于能够在事件发生前进行验证、检查和审批,从而实现对工作流的精确控制。
1.1 执行时序
before Hook 的执行时机位于"用户触发事件"和"事件实际执行"之间。当一个事件被触发时,系统会先检查是否存在对应的 before Hook,如果存在则先执行 Hook 脚本,根据 Hook 的退出码决定是否继续执行后续事件。
执行流程:
用户操作 → 触发事件 → 检查before Hook → 执行Hook脚本
→ 退出码为0 → 继续执行事件
→ 退出码非0 → 阻断事件,返回错误信息
1.2 核心特性
- 阻断能力:before Hook 最强大的特性——当 Hook 脚本返回非零退出码时,系统会立即终止事件的执行,并将 Hook 的输出信息返回给用户。这类似于 Git Hooks 中的 pre-commit Hook,可以在提交前进行代码检查,检查不通过则阻止提交。
- 环境修改:before Hook 可以修改环境变量或输入参数,影响后续事件的执行行为。例如在命令执行前自动加载特定环境配置。
- 输入验证:可以对用户输入进行校验,确保参数合法、文件存在、权限充分等前置条件满足。
- 审批流程:可以集成外部审批系统,在关键操作(如生产环境部署)执行前等待人工审批。
1.3 典型应用场景
代码质量门禁
在 git commit 前运行 linter 和测试,代码质量不达标则阻止提交。
权限检查
在执行敏感操作前验证用户权限,防止未授权的操作。
环境准备
在任务执行前自动拉取最新代码、安装依赖、检查磁盘空间。
配置校验
验证配置文件格式是否正确、必要配置项是否完整。
1.4 超时和失败处理
before Hook 支持超时机制。如果 Hook 脚本在指定时间内未完成执行,系统会将其视为超时失败,并按照非零退出码的方式处理——即阻断事件执行。默认超时时间可以通过配置调整。超时处理确保 before Hook 不会因挂起而无限期阻塞工作流。
注意:before Hook 的退出码判断采用 Shell 标准约定——0 表示成功,非 0 表示失败。在 Shell 脚本中务必使用 exit 0 表示检查通过,exit 1(或其他非零值)表示检查不通过。
二、after Hook详解
after Hook 在事件执行之后触发,它的设计定位是"观察者模式"——可以获取事件的执行结果,但不能干预事件本身的执行流程。这与 before Hook 形成鲜明对比,两者互补构成了完整的 Hook 体系。
2.1 执行时序
after Hook 在事件执行完毕后立即触发,无论事件执行成功还是失败。系统会将事件的退出状态通过 $STATUS 环境变量传递给 after Hook 脚本,供后续处理逻辑使用。
执行流程:
用户操作 → 触发事件 → 执行事件 → 事件完成 →
→ 检查after Hook → 执行Hook脚本
→ Hook执行完毕(无论退出码如何)
→ 返回结果给用户
2.2 核心特性
- 无阻断能力:after Hook 的退出码不影响原始事件的执行结果。即使 after Hook 返回非零退出码,原始事件的结果仍然会正常返回给用户。这是 after 和 before 最本质的区别。
- 获取执行状态:通过
$STATUS 环境变量获取事件执行的状态码,0 表示成功,非 0 表示失败。这使得 after Hook 可以根据事件结果执行不同的后续处理逻辑。
- 可靠性:即使原始事件执行失败,after Hook 仍然会被触发执行。这确保了日志记录、清理操作、通知发送等后续处理不会被遗漏。
- 异步友好:after Hook 适合执行不需要阻塞用户响应的任务,如数据上报、指标收集等。
2.3 典型应用场景
日志记录
记录每次操作的详细信息、执行时间、结果状态,便于审计追踪。
通知发送
操作完成后发送 Slack/邮件/钉钉通知,告知相关人员操作结果。
清理回收
释放临时资源、删除临时文件、关闭数据库连接等清理工作。
指标采集
统计操作频率、成功率、耗时分布等运维指标数据。
提示:在 after Hook 中可以利用 $STATUS 实现条件分支逻辑。例如仅在事件执行失败时发送告警通知,或在成功时触发后续自动化流程。
三、before vs after对比
理解 before 和 after 两种 Hook 类型的差异,是正确设计 Hook 体系的基础。下面通过对比表格进行详细分析。
3.1 核心对比
| 对比维度 |
before Hook |
after Hook |
| 执行时机 |
事件执行之前 |
事件执行之后 |
| 能否阻断事件 |
能(非零退出码可阻断) |
不能(事件已被执行完毕) |
| 适用场景 |
验证、检查、审批、环境准备 |
通知、日志、清理、度量 |
| 返回值要求 |
必须正确返回退出码(0通过/非0阻断) |
退出码不影响主流程(仅用于日志记录) |
| 异常处理方式 |
超时或失败时自动阻断事件 |
超时或失败时记录错误,不影响用户 |
| 能否修改输入 |
可以修改环境变量和输入参数 |
不能(事件已执行完毕) |
| 典型用例 |
代码检查、权限验证、配置校验 |
日志记录、消息通知、资源清理 |
| 执行顺序 |
多个 before Hook 按序串联执行 |
多个 after Hook 按序串联执行 |
3.2 何时选择 before
- 需要对操作进行前置条件检查时——例如检查磁盘空间是否充足、网络是否可达
- 需要实现安全控制时——例如只有特定角色的用户才能执行生产环境操作
- 需要准备运行环境时——例如自动切换 Node.js 版本、加载 VPN
- 需要实现审批流程时——例如重大变更需要经外部系统确认后才能继续
3.3 何时选择 after
- 需要记录操作历史时——无论操作成功失败,都需要留下审计记录
- 需要发送操作通知时——例如 CI/CD 部署完成后的状态通知
- 需要清理临时资源时——例如删除构建过程中产生的临时文件
- 需要收集度量数据时——例如统计操作耗时、成功率等性能指标
核心原则:如果你想让某件事"阻止"事件发生,使用 before Hook;如果你想让某件事"响应"事件发生,使用 after Hook。一个控制流程,一个观察流程。
四、混合使用策略
在实际项目中,before 和 after Hook 常常配合使用,形成完整的"检查-执行-记录"闭环。合理的混合使用策略能够最大化 Hooks 系统的价值。
4.1 同一事件的 before 和 after 搭配
最常见的模式是为同一个事件同时配置 before 和 after Hook,形成完整的生命周期管理:
配置示例(settings.json):
{
"hooks": {
"beforeCommand": [
"check-env.sh",
"validate-input.sh"
],
"afterCommand": [
"log-command.sh",
"send-notification.sh"
]
}
}
上述配置中,每次执行命令前会先运行 check-env.sh 和 validate-input.sh 进行前置检查,命令执行完成后会运行 log-command.sh 和 send-notification.sh 进行后续处理。
4.2 多个 before Hook 的串联执行
当配置了多个 before Hook 时,系统会按照配置顺序依次执行。串联执行的链路中,任何一个 Hook 返回非零退出码都会立即阻断后续所有 Hook 和主事件的执行。
多个 before Hook 执行逻辑:
beforeHook-1 执行 → 退出码为0 → 继续
→ beforeHook-2 执行 → 退出码为0 → 继续
→ beforeHook-3 执行 → 退出码非0 → 阻断!
✗ 不执行后续 beforeHook
✗ 不执行主事件
→ 返回错误信息给用户
这种串联机制使得我们可以将不同职责的检查逻辑分离到独立的 Hook 脚本中,每个脚本只关注一件事情,保持关注点单一。例如将"权限检查"和"参数校验"分为两个脚本,各自独立维护。
4.3 多个 after Hook 的串联执行
多个 after Hook 同样按照配置顺序依次执行。与 before Hook 不同的是,after Hook 的退出码不会影响后续 after Hook 的执行——即使前面的 after Hook 失败了,后续的 after Hook 仍然会继续执行。
多个 after Hook 执行逻辑:
主事件执行完毕 → afterHook-1 执行 → 退出码非0
→ afterHook-2 执行 → 退出码为0
→ afterHook-3 执行 → 退出码非0
(所有 after Hook 都会执行完毕,相互不影响)
4.4 before 阻断后的 after 行为
一个重要的设计问题是:当 before Hook 阻断了事件执行,已配置的 after Hook 是否还会执行?答案是:不会。因为 before Hook 阻断意味着事件"从未被执行",自然也就不存在"事件执行之后"的状态。after Hook 仅在事件实际执行完成后才会触发。
重要:如果需要在 before Hook 阻断时仍然执行某些清理或日志逻辑,不应依赖 after Hook,而应在 before Hook 脚本自身中添加相应的错误处理分支。
五、Hook状态码和返回值
正确理解和使用 Hook 的退出状态码,是编写可靠 Hook 脚本的基础。Hook 系统遵循 Shell 标准的退出码约定,并在 before 和 after 两种场景中有不同的语义。
5.1 状态码约定
| 退出码 |
Shell 语义 |
before Hook 含义 |
after Hook 含义 |
| 0 |
成功 |
检查通过,允许事件继续执行 |
记录日志:Hook 执行成功 |
| 1 |
通用错误 |
检查失败,阻断事件执行 |
记录日志:Hook 执行失败 |
| 2 |
误用/参数错误 |
阻断事件,提示参数错误 |
记录错误日志,不影响主流程 |
| 126 |
命令不可执行 |
阻断事件,报告权限问题 |
记录错误日志 |
| 127 |
命令未找到 |
阻断事件,报告依赖缺失 |
记录错误日志 |
| 128+n |
信号终止(信号 n) |
阻断事件,报告被信号终止 |
记录错误日志 |
| 130 |
Ctrl+C 终止 |
阻断事件,报告用户中断 |
记录日志:Hook 被用户中断 |
5.2 Shell 脚本退出码示例
#!/bin/bash
# before Hook 示例:检查磁盘空间
THRESHOLD=90
USAGE=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
if [ "$USAGE" -gt "$THRESHOLD" ]; then
echo "错误:磁盘使用率已达 ${USAGE}%,超过阈值 ${THRESHOLD}%"
exit 1 # 阻断事件执行
fi
echo "磁盘空间检查通过,当前使用率 ${USAGE}%"
exit 0 # 允许事件继续执行
#!/bin/bash
# after Hook 示例:记录命令执行结果
STATUS=${STATUS:-$?}
EVENT_NAME=${EVENT_NAME:-"unknown"}
TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
if [ "$STATUS" -eq 0 ]; then
echo "[$TIMESTAMP] 事件 '$EVENT_NAME' 执行成功"
# 可以在此处触发后续流程
else
echo "[$TIMESTAMP] 事件 '$EVENT_NAME' 执行失败,状态码: $STATUS"
# 可以在此处发送告警通知
fi
exit 0 # after Hook 即使检测到失败也应返回 0,避免产生额外噪声
5.3 $STATUS 环境变量
$STATUS 是 after Hook 中特有的环境变量,用于传递主事件的退出状态码。在 Shell 脚本中可以通过以下方式获取:
- 直接引用:
$STATUS 变量由 Hook 系统自动注入到执行环境中
- 备用方案:部分场景下
$? 也可以获取到退出码,但建议优先使用 $STATUS 以确保可靠性
- 状态码语义:0 表示成功,非 0 表示失败,具体数值对应不同的错误类型
最佳实践:
- before Hook 中始终使用明确的退出码(0 或 1),并输出可读的错误信息
- after Hook 中即使检测到错误,也建议返回 0 退出码,避免产生额外的错误日志
- 在 after Hook 中通过
$STATUS 判断主事件状态时,注意处理变量未定义的情况
- 不要在 after Hook 中执行长时间运行的任务,以免影响整体响应速度