一、什么是 Hook
本文中的 Hook 特指 Agent Host 的生命周期扩展点,不是 Git Hook、React Hook 或普通回调函数。Agent Host 是承载并调度 Agent 的 CLI、IDE 或桌面运行时,以下简称 Host。当会话、用户输入、工具调用、上下文压缩或结束等事件发生时,Host 自动调用已注册的 handler,并根据退出码或结构化结果继续、阻断、补充上下文或改变后续流程。
Hook 的价值来自两个特征:
- 自动触发:不依赖模型记得调用;
- 确定介入:在明确的生命周期节点执行可复查逻辑。
最常见的实现是 command Hook,也就是运行本地脚本。但 Hook 不等于 shell command。Claude Code 还支持 HTTP、MCP Tool、Prompt 和 Agent handler;Codex 截至本文更新时间只执行 command handler。
Skill 告诉 Agent 一类任务应该怎样完成;Hook 保证某个生命周期节点发生时,指定检查或动作会自动执行。
二、为什么需要 Hook
Prompt 和 Skill 可以要求 Agent“修改后格式化”“结束前跑测试”“不要执行危险命令”,但模型推理不是机械调度器。上下文压缩、工具路径变化、任务中断或模型判断偏差,都可能让这类要求没有被执行。
Hook 把“希望 Agent 记得做”改成“事件发生就运行”。典型场景包括:
PreToolUse在工具执行前检查危险命令或受保护路径;PostToolUse在文件修改后运行格式化、静态检查或审计;Stop在一次回答准备结束时检查测试、证据或输出契约;SessionStart加载动态环境信息或少量会话上下文;PreCompact、PostCompact在上下文压缩前后保存或恢复关键信息;- 通知类事件把等待输入、任务完成或错误状态转发到桌面或外部服务。
Hook 不适合承载开放式长流程。一个需要持续理解需求、探索多条线索、调用大量工具并根据结果调整方向的任务,仍应由 Skill 或 Agent 组织;高频 Hook 应短小、可预测、可超时和可诊断。
三、Hook 与其他能力的职责边界
| 需求 | 更适合的组件 | 原因 |
|---|---|---|
| 定义一类任务的步骤、判断和验收 | Skill | 需要模型理解并按上下文决策 |
| 在固定生命周期节点自动检查或处理 | Hook | 事件驱动,不依赖模型主动调用 |
| 提供可重复执行的确定逻辑 | script / CLI | 逻辑本体可以独立运行和测试 |
| 连接数据库、SaaS 或实时外部系统 | MCP Server / App | 需要工具协议、鉴权与结构化输入输出 |
| 隔离上下文完成语义审查或独立子任务 | Agent / Subagent | 需要独立推理和工具使用 |
| 封装、安装和版本化一组能力 | Plugin | 负责分发,不负责定义运行语义 |
| 在合并或部署链路做最终强制门禁 | CI / pre-commit / 服务端策略 | 脱离单次 Agent 会话,覆盖更多入口 |
Hook 经常调用普通脚本,两者不是替代关系。脚本负责“如何检查”,Hook 负责“什么时候自动检查”。Plugin 可以打包 Hook,但不改变 Hook 的事件协议;分发与目录结构详见 Agent Plugin 完全指南。
Hook 也不应成为唯一安全边界。Host 的 Sandbox、权限规则和审批流程负责基础隔离,CI 或服务端策略负责最终门禁;Hook 更适合在 Agent 循环中提供早期反馈和额外约束。
四、Hook 的运行模型
Agent Hook 通常嵌在三层生命周期里:会话包含多个回合,每个回合又可能包含多次工具调用。
图中最重要的结论是:执行前事件可以阻止尚未发生的动作,执行后事件只能处理结果,不能撤销已经产生的副作用。 Stop 也不是“整项任务完成”的同义词,它表示 Agent 当前准备结束一次响应;阻断 Stop 通常意味着要求 Agent 继续工作。
一个 Hook 配置通常有三层:
- Event:选择生命周期节点,例如
PreToolUse; - Matcher group:过滤这次事件是否相关,例如只匹配
Bash; - Handler:实际运行的命令、HTTP 请求、MCP Tool 或模型判断。
事件触发后,Host 把结构化上下文交给 handler。command Hook 通常从 stdin 读取 JSON,通过退出码、stderr 和可选的 stdout JSON 返回结果。不同 Host、不同事件对结果的解释并不相同。
五、配置来源与作用域
Hook 放在哪里,决定它对哪些项目和会话生效。用户级配置适合个人习惯,项目级配置适合团队规则,Plugin 适合可安装分发,managed 配置适合组织政策。本文中的 managed 指由组织或设备管理员下发并控制的策略层,不是普通项目配置。
Claude Code
| 位置 | 作用域 | 是否适合共享 |
|---|---|---|
~/.claude/settings.json | 当前用户的所有项目 | 否 |
.claude/settings.json | 当前项目 | 是 |
.claude/settings.local.json | 当前项目的本机设置 | 否 |
| Managed policy settings | 组织或设备 | 由管理员控制 |
Plugin 的 hooks/hooks.json | Plugin 启用期间 | 是 |
| Skill / Agent frontmatter | 组件激活期间 | 是 |
Skill 或 Agent frontmatter 中的 Hook 只在组件激活期间存在,结束后自动清理。Agent 中的 Stop Hook 会按 Subagent 生命周期处理。完整位置和作用域见 Claude Code Hooks reference。
Codex
Codex 支持独立 hooks.json,也支持与配置层相邻的 config.toml 内联 [hooks]:
~/.codex/hooks.json;~/.codex/config.toml;<repo>/.codex/hooks.json;<repo>/.codex/config.toml;requirements.toml等 managed 配置;- 已启用 Plugin 的
hooks/hooks.json或 manifest 声明。
项目级 .codex/ 配置只在项目被信任时加载。多个来源的 Hook 会累加,而不是由高优先级层覆盖低优先级层;同一层同时使用 hooks.json 与内联 [hooks] 时,Codex 会合并两者并给出警告。详见 Codex Hooks。
无论在哪一端,多个匹配 handler 都可能并行启动。不要用配置顺序表达依赖关系,也不要假设前一个 Hook 能阻止后一个 Hook 开始执行。
六、生命周期事件怎么选
事件名看起来相似,但“发生在动作之前还是之后”决定了它能否阻断、能拿到哪些数据,以及运行频率。
两端共有的主干事件
| 事件 | 触发时机 | 常见用途 | 关键边界 |
|---|---|---|---|
SessionStart | 会话启动、恢复或压缩后恢复 | 加载动态上下文、准备环境 | 不适合长时初始化 |
UserPromptSubmit | 用户输入交给模型前 | 敏感信息检查、补充上下文 | 可能直接阻断用户输入 |
PreToolUse | 工具调用执行前 | 危险操作拦截、输入改写 | 只覆盖 Host 明确暴露的工具路径 |
PermissionRequest | Host 即将请求用户审批时 | 自动允许或拒绝特定审批 | 没有审批就不会触发 |
PostToolUse | 工具已完成后 | 格式化、审计、反馈结果 | 无法撤销副作用 |
SubagentStart | 子 Agent 启动时 | 注入角色上下文 | 通常不能阻止启动 |
SubagentStop | 子 Agent 准备结束时 | 验收、要求继续 | 必须避免循环 |
PreCompact | 上下文压缩前 | 保存状态、阻止不安全压缩 | 高频大输出会反向增加成本 |
PostCompact | 上下文压缩后 | 恢复关键提示、记录摘要 | 已无法改变之前的压缩过程 |
Stop | 当前响应准备结束时 | 测试、证据和输出契约校验 | 阻断通常表示继续工作 |
Claude Code 的扩展事件
截至 2026-07-17,Claude Code 的公开 reference 列出 30 个事件。除上表主干外,还包括:
| 类别 | 事件 |
|---|---|
| 初始化与会话 | Setup、InstructionsLoaded、SessionEnd |
| 用户输入与显示 | UserPromptExpansion、MessageDisplay |
| 工具与权限 | PermissionDenied、PostToolUseFailure、PostToolBatch |
| Agent 与任务 | TaskCreated、TaskCompleted、StopFailure、TeammateIdle |
| 环境与配置 | Notification、ConfigChange、CwdChanged、FileChanged |
| Worktree 与 MCP | WorktreeCreate、WorktreeRemove、Elicitation、ElicitationResult |
完整事件输入、matcher 对象和决策字段以 Claude Code Hook lifecycle 为准。StopFailure、PostToolUseFailure 与 PostToolUse 的失败语义不同,不应合并成一个模糊的“结束事件”。
Codex 当前事件范围
Codex 当前公开支持上表中的 10 个主干事件。PreToolUse、PermissionRequest 和 PostToolUse 明确覆盖 Bash、apply_patch 及 MCP 工具,但对 shell 的拦截仍不完整,也不覆盖 WebSearch 等其他工具路径。因此 Codex 官方把 PreToolUse 定位为 guardrail,而不是完整 enforcement boundary。
七、Handler 类型与选型
Claude Code 的五类 handler
| 类型 | 执行方式 | 适合场景 | 主要限制 |
|---|---|---|---|
command | 本地命令,stdin/stdout/exit code 通信 | 快速校验、格式化、环境脚本 | 以当前用户权限运行 |
http | 向 URL POST 事件 JSON | 团队审计服务、云函数 | 非 2xx 和超时默认不阻断 |
mcp_tool | 调用已连接 MCP Server 的工具 | 复用现有内部工具 | 不负责建立连接或认证 |
prompt | 单次模型判断,返回 ok/reason | 输入数据足够的语义判断 | 有模型成本,结果不确定 |
agent | 启动可使用工具的验证 Agent | 需要读文件、搜索或运行检查 | 实验能力,生产优先 command |
Claude Code 官方把 agent Hook 标为 experimental;需要确定门禁时,优先使用可单测的 command Hook。prompt 和 agent 把模型判断引入生命周期,不应再被描述为纯确定性逻辑。详见 Hook handler fields。
command Hook 有两种执行形式:
- 配置
args时使用 exec form,不经过 shell,路径和参数边界更清楚; - 省略
args时使用 shell form,支持变量、管道和重定向,但同时扩大命令注入与转义风险。
Claude Code 的工具事件还可以在单个 handler 上配置 if,使用权限规则语法按工具参数做二次过滤。它适合在启动脚本前减少无关匹配,但解析失败时会选择运行 handler,因此不能替代权限系统或 handler 内部校验。一个 if 只接受一条规则,不支持用 &&、|| 或数组拼接多条条件。
Codex 的当前限制
Codex 当前只运行 type: "command"。prompt、agent 和 async 配置即使能被解析,也会被跳过;异步 Hook 尚未支持。跨端 Hook 应把共享逻辑放在独立脚本里,再为每个 Host 单独维护配置和结果适配层。
八、输入、输出与阻断语义
每个 command Hook 都从 stdin 收到一个 JSON 对象。常见字段包括:
| 字段 | 含义 | 稳定性注意 |
|---|---|---|
session_id | 当前会话标识 | 不应当作永久业务 ID |
cwd | 当前工作目录 | 不保证等于仓库根目录 |
hook_event_name | 当前事件名 | 可用于共享脚本分派 |
tool_name | 工具事件中的工具名 | 只在相关事件出现 |
tool_input | 本次工具参数 | 结构随工具变化 |
tool_response | 工具执行结果 | 只在执行后事件出现 |
turn_id | 当前回合标识 | 两端字段覆盖范围不同 |
transcript_path | 会话记录路径 | 格式不是稳定 Hook 协议 |
不要让一个脚本假设所有事件都有相同字段。解析输入时应使用类型检查和缺省值,并把每个事件的 fixture 独立保存。
退出码
可移植的最小约定是:
exit 0且无输出:Hook 成功,但不表达额外决定;exit 0且输出合法 JSON:Host 按当前事件的 schema 解释;exit 2且向stderr写原因:在支持阻断的事件中表达拒绝或要求继续。
exit 2 的效果由事件决定:PreToolUse 可以阻止工具调用,UserPromptSubmit 可以拒绝提示词,Stop 则要求 Agent 继续。对 PostToolUse 而言,工具已经执行,所谓 block 只能改变反馈或后续模型处理。
两端在这里还有一处关键差异:Codex 的 PostToolUse 返回 decision: "block" 时,会用 Hook 反馈替换原工具结果再交给模型;Claude Code 的同名 decision 只把 reason 附加在原结果旁,只有 updatedToolOutput 才会替换模型看到的工具输出。两者都不能回滚工具已经造成的副作用。
Claude Code 中,除特殊事件外,exit 1 等其他非零值通常只是非阻断错误;要阻断不能沿用普通 Unix 的“任何非零都失败”直觉。Codex 的发布文档只对部分事件明确 exit 2 行为,未写明的事件应使用其专属 JSON schema,不做跨事件推断。
JSON 输出
富决策通常放在事件专属对象里:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Blocked by repository policy."
}
}几个容易混淆的字段:
systemMessage通常是显示给用户的警告;additionalContext才是交给模型的额外上下文;continue: false与decision: "block"的优先级和效果取决于事件;updatedInput只能在明确支持的工具前事件中使用;- handler 若输出 JSON,stdout 不应再夹杂欢迎语、调试日志或其他文本。
事件名相同,不代表 Codex 与 Claude Code 的返回字段完全相同。事件不同,更不能复制同一份 decision JSON。
九、写一个最小的 PreToolUse Hook
下面的例子在 Bash 执行前拦截少量明显危险片段。脚本逻辑两端共用,配置路径分别适配 Claude Code 与 Codex。示例对畸形输入和校验器异常选择 fail-closed:向 stderr 写原因并退出 2,避免普通异常变成非阻断错误。
目录结构:
<repo>/
├── .agent-hooks/
│ └── block_dangerous.py
├── .claude/settings.json
└── .codex/hooks.json脚本从 stdin 读取事件 JSON。安全分支命中时向 stderr 写原因并退出 2;没有意见时静默退出 0:
#!/usr/bin/env python3
import json
import sys
MAX_EVENT_CHARS = 1_000_000
MAX_COMMAND_CHARS = 200_000
def block(reason: str) -> int:
print(f"Blocked: {reason}", file=sys.stderr)
return 2
def check() -> int:
raw = sys.stdin.read(MAX_EVENT_CHARS + 1)
if len(raw) > MAX_EVENT_CHARS:
return block("hook input is too large.")
try:
event = json.loads(raw)
except json.JSONDecodeError:
return block("hook input is not valid JSON.")
if not isinstance(event, dict):
return block("hook input must be a JSON object.")
if event.get("tool_name") != "Bash":
return block("unexpected tool event.")
tool_input = event.get("tool_input")
if not isinstance(tool_input, dict):
return block("tool_input must be an object.")
command = tool_input.get("command")
if not isinstance(command, str):
return block("Bash command must be a string.")
if len(command) > MAX_COMMAND_CHARS:
return block("Bash command is too large to validate.")
blocked_fragments = ("rm -rf", "rm -fr", "git reset --hard")
if any(fragment in command for fragment in blocked_fragments):
return block("destructive command requires manual review.")
return 0
def main() -> int:
try:
return check()
except Exception:
return block("hook validator failed unexpectedly.")
if __name__ == "__main__":
raise SystemExit(main())Claude Code 的 POSIX 项目配置把 /bin/sh 作为固定 executable,并通过独立参数传入项目根目录。wrapper 不拼接 tool_input,同时把脚本缺失、解释器缺失和校验器异常统一转换为 exit 2:
{
"hooks": {
"PreToolUse": [
{
"matcher": "^Bash$",
"hooks": [
{
"type": "command",
"command": "/bin/sh",
"args": [
"-c",
"script=\"$1/.agent-hooks/block_dangerous.py\"; [ -r \"$script\" ] || { printf '%s\\n' 'Blocked: hook script is missing.' >&2; exit 2; }; python=$(command -v python3) || { printf '%s\\n' 'Blocked: python3 is unavailable.' >&2; exit 2; }; \"$python\" \"$script\"; status=$?; if [ \"$status\" -eq 0 ] || [ \"$status\" -eq 2 ]; then exit \"$status\"; fi; printf '%s\\n' 'Blocked: hook validator failed unexpectedly.' >&2; exit 2",
"hook-pretool",
"${CLAUDE_PROJECT_DIR}"
],
"timeout": 5,
"statusMessage": "Checking Bash command"
}
]
}
]
}
}普通的非门禁 Hook 可以直接用 command: "python3" 和脚本 args,减少 shell 层;这里保留 wrapper 是为了明确安全校验的失败策略。Windows 应改用 PowerShell wrapper。
Codex 的项目配置从 Git 根目录解析脚本,避免从子目录启动时路径失效:
{
"hooks": {
"PreToolUse": [
{
"matcher": "^Bash$",
"hooks": [
{
"type": "command",
"command": "root=$(git rev-parse --show-toplevel 2>/dev/null) || { printf '%s\\n' 'Blocked: cannot resolve repository root.' >&2; exit 2; }; script=\"$root/.agent-hooks/block_dangerous.py\"; [ -r \"$script\" ] || { printf '%s\\n' 'Blocked: hook script is missing.' >&2; exit 2; }; /usr/bin/python3 \"$script\"; status=$?; if [ \"$status\" -eq 0 ] || [ \"$status\" -eq 2 ]; then exit \"$status\"; fi; printf '%s\\n' 'Blocked: hook validator failed unexpectedly.' >&2; exit 2",
"timeout": 5,
"statusMessage": "Checking Bash command"
}
]
}
]
}
}这份 Codex 配置面向 macOS / Linux 的 Git 仓库,并假设解释器路径为 /usr/bin/python3。根目录解析、脚本缺失或校验器异常都会转换成 exit 2;非 Git 项目应改用经过审核的绝对路径或稳定 wrapper,Windows 则应配置 commandWindows 并替换解释器路径。
先脱离 Host 测试脚本:
printf '%s\n' '{"tool_name":"Bash","tool_input":{"command":"rm -rf build"}}' \
| python3 .agent-hooks/block_dangerous.py
echo $? # 预期为 2这个例子提供的是输入校验和阻断协议骨架,不是完整 shell 安全解析器。字符串检查可以被转义、变量、子 shell 或等价命令绕过;强安全策略仍应使用 Host 权限规则、Sandbox、managed policy 和 CI,并让 handler 对完整输入做结构化校验。
十、工作目录、路径与持久数据
Hook 经常因为“配置存在但脚本路径失效”而报错。路径设计应明确区分项目源码、Plugin 安装目录和可写数据目录。
Claude Code
${CLAUDE_PROJECT_DIR}:项目根目录;${CLAUDE_PLUGIN_ROOT}:当前 Plugin 根目录;${CLAUDE_PLUGIN_DATA}:Plugin 的持久数据目录。
Codex
PLUGIN_ROOT:当前安装的 Plugin 根目录;PLUGIN_DATA:Plugin 的可写持久数据目录;CLAUDE_PLUGIN_ROOT、CLAUDE_PLUGIN_DATA:为现有 Plugin Hook 提供的兼容别名。
Codex 命令在会话 cwd 中运行,项目 Hook 不应假设当前目录永远是仓库根。Plugin 的缓存或安装路径可能随更新变化,持久数据应写入 PLUGIN_DATA,不要写进安装目录。双端缓存与稳定 wrapper 案例见 Codex 与 Claude Code 的 Skill Plugin Hook 机制。
transcript_path 适合诊断和有限读取,不适合当成稳定数据库接口。Codex 明确说明 transcript 格式可能变化;停止事件已经提供 last_assistant_message 时,应优先使用事件字段,避免读取尚未刷新的文件。
十一、加载、生效与信任
Hook 文件存在不等于它已经运行。排查前先区分四个状态:已发现、已启用、已信任、已匹配。
Codex 的 Hook trust
Codex 会要求用户审核非 managed command Hook,并把信任绑定到当前 Hook 配置定义 的 hash。配置定义变化后,Hook 会重新进入待审核状态;未信任的 Hook 会被跳过。
公开文档只承诺 hash 绑定到 Hook definition,没有说明它会覆盖被命令引用的脚本字节、解释器或依赖。因此不能把 trust 当成脚本完整性校验:脚本仅修改内容时未必触发重新审核,仍要单独审查脚本、依赖和更新 diff。
在 CLI 中使用 /hooks 查看来源、审核新定义、信任或禁用非 managed Hook。项目不受信任时,项目 .codex/ Hook 不会加载;managed Hook 由组织策略信任,不能从用户浏览器禁用。安装或启用 Plugin 也不会自动信任其中的 Hook。
一次性自动化只有在 Codex 外部已经完成来源审核时,才应考虑 --dangerously-bypass-hook-trust。这个参数不是日常开发的便利开关。
Claude Code 的加载状态
Claude Code 的 /hooks 是只读浏览器,可查看事件、matcher、handler 和来源。设置文件通常由文件监听器自动重载;未出现时应检查 JSON、路径和配置层,必要时重启会话。
Claude Code 可用 disableAllHooks: true 临时关闭非 managed Hook;Codex 可在 config.toml 中设置:
[features]
hooks = falseClaude Code 当前公开 Hooks reference 没有描述 Codex 式的逐定义 hash trust 流程,因此不能把两端的“项目信任”“Plugin 安装”和“Hook 审核”视为同一机制。
十二、安全模型
Command Hook 会自动执行代码。Anthropic 明确说明它以当前系统用户权限运行;Codex 对非 managed Hook 增加定义审核,但这不等于脚本获得了沙箱隔离。评审 Hook 应按评审可执行代码的标准进行。
| 风险 | 常见来源 | 缓解方式 |
|---|---|---|
| 命令注入 | 把 tool_input 拼进 shell | 优先 exec form;参数分离;不使用 eval |
| 路径穿越 | 接受 ../、符号链接或绝对路径 | 规范化路径并确认仍位于允许根目录 |
| 敏感数据泄漏 | 记录 prompt、token、.env 或 transcript | 默认最小日志;字段白名单;脱敏与访问控制 |
| 权限扩大 | Hook 自动返回 allow | 安全分支外静默 exit 0,保留正常审批流程 |
| 伪安全边界 | 只拦某个工具名或字符串 | 与权限规则、Sandbox、CI 和服务端策略组合 |
| 供应链执行 | 从未知 Plugin 或仓库加载 Hook | 审核来源、脚本、依赖、更新 diff 和信任状态 |
| 并发副作用 | 多个 handler 写同一文件或状态 | 原子写入、文件锁、唯一 key、幂等设计 |
| 失败模式不清 | HTTP 超时、脚本异常后默认继续 | 明确 fail-open / fail-closed,并覆盖测试 |
fail-open 表示 Hook 失败时继续原流程,fail-closed 表示失败时阻断。安全敏感的前置校验通常倾向 fail-closed,但必须接受误阻断并准备恢复路径;通知、遥测等旁路任务通常倾向 fail-open。具体能否阻断仍由事件 schema 决定,不能只靠返回任意非零退出码。
输入 JSON 来自 Host,但其中可能包含用户文本、工具参数、文件路径和外部系统内容,不能视为可信 shell 片段。至少应做到:
- 校验字段类型、枚举和长度;
- 给 shell 变量加引号,优先使用绝对路径;
- 拒绝越界路径与敏感文件;
- 对网络目标和环境变量使用白名单;
- 不把密钥放进配置、命令行或普通日志;
- 为脚本和依赖锁定可信来源。
Claude Code 的 HTTP Hook 还有一个容易误判的边界:非 2xx、连接失败和超时都是非阻断错误。要拒绝动作,服务必须返回 2xx 和正确的 decision JSON;不能把 403 或 500 当成可靠门禁。
十三、性能与可靠性
Hook 处在 Agent 的热路径上。PreToolUse 每次工具调用前都可能运行,延迟会被调用次数放大;Stop 可能在每次响应结束时运行,而不是只在整项任务完成时运行。
按频率分层
| 层级 | 适合内容 | 不适合内容 |
|---|---|---|
| 每次工具调用 | 纯本地规则、路径检查、轻量格式化 | 网络请求、完整测试、模型审查 |
| 每个回合结束 | 增量测试、工作区扫描、输出契约校验 | 无上限的递归审查 |
| 会话开始或压缩后 | 少量动态上下文、环境准备 | 把大仓库资料全部塞进上下文 |
| CI / 提交阶段 | 完整测试、安全扫描、最终政策门禁 | 需要即时反馈的微小检查 |
可靠的 Hook 应具备:
- 显式超时:不要依赖 Host 的长默认值;
- 幂等:同一事件重复触发不会破坏状态;
- 并发安全:多个 handler 同时运行也不会互相覆盖;
- 输出有界:只返回 Agent 下一步需要的信息;
- 失败可见:保留事件名、脚本版本、退出码和脱敏错误;
- 可独立运行:脚本能接收 fixture,在 Host 外单测。
Claude Code 支持 async: true 的后台 command Hook,但异步结果不能阻断已经继续的动作,每次触发也会产生独立后台进程。asyncRewake: true 可以在后台 Hook 以退出码 2 结束时重新唤醒 Claude 处理失败,但仍不能撤销先前动作。Codex 当前不支持异步 Hook。需要跨端一致行为时,应按同步 Hook 设计,或者把异步观察逻辑放到外部队列与服务中。
多个 Hook 同时改写同一个 updatedInput 会产生时序竞争。无论 Host 当前如何聚合结果,都不应依赖“配置在后面的 Hook 最后生效”;同一工具输入最多保留一个改写者,其他 Hook 只观察或拒绝。
十四、Codex 与 Claude Code 的实现差异
| 维度 | Codex | Claude Code |
|---|---|---|
| 当前公开事件 | 10 个主干事件 | 30 个事件,覆盖更多配置、任务、显示和 MCP 生命周期 |
| 当前可执行 handler | command | command、http、mcp_tool、prompt、agent |
| Agent Hook | 解析但跳过 | 支持,标记 experimental |
| Async command Hook | 解析但跳过 | 支持;asyncRewake 可在失败后唤醒,但不能阻断当前动作 |
| 用户配置 | ~/.codex/hooks.json 或 config.toml | ~/.claude/settings.json |
| 项目配置 | <repo>/.codex/hooks.json 或 config.toml | .claude/settings.json / .local.json |
| Plugin 默认路径 | hooks/hooks.json | hooks/hooks.json |
| 组件内 Hook | 当前公开文档未定义通用 Skill/Agent frontmatter Hook | Skill / Agent frontmatter 可声明 |
| Matcher | 正则字符串;部分事件支持别名 | 精确名、列表或 JavaScript 正则;工具事件还有 if |
| Hook 审核 | 非 managed 定义按 hash 审核与信任 | 公开 reference 未描述同类逐定义 hash 流程 |
/hooks | 查看、信任和禁用非 managed Hook | 只读查看来源与配置 |
| Plugin 根变量 | PLUGIN_ROOT / PLUGIN_DATA,另有 Claude 兼容别名 | CLAUDE_PLUGIN_ROOT / CLAUDE_PLUGIN_DATA |
| 工具前拦截边界 | 当前 shell 与工具覆盖不完整 | 只拦真实工具调用;文件直接附加等路径可能绕过工具事件 |
两端都采用相似的“事件 → matcher → handler”结构,但这只是配置形状相近,不代表协议兼容。跨端复用时建议拆成三层:
共享业务脚本
├── 只接受稳定、最小的内部输入
├── 输出自定义中间结果
└── 由端侧 adapter 转换为各自 Hook JSON
Codex adapter Claude Code adapter
事件字段归一化 事件字段归一化
Codex decision schema Claude decision schema
Codex 路径与 trust Claude 路径与 settings scope仅仅同时提供 CLAUDE_PLUGIN_ROOT 环境变量,不代表 Claude Code Hook 可以原样复制到 Codex。导入后必须重新核对事件名、handler、matcher、退出码和输出字段。
十五、常见设计模式
调用前阻断
PreToolUse 运行快速、确定的策略检查。只在明确危险时拒绝;安全分支保持沉默,让 Host 的正常权限流程继续。
适合:
- 受保护路径;
- 明确禁止的命令类别;
- MCP 写工具的参数范围;
- 密钥或敏感数据外发检查。
调用后处理
PostToolUse 适合格式化、记录和反馈,但不能回滚工具副作用。需要保证文件最终符合规范时,应在 Stop、pre-commit 或 CI 再做一次聚合检查,覆盖通过 Bash 等其他路径产生的修改。
结束前门禁
Stop 可以检查报告字段、测试状态或证据完整性,并把缺失项反馈给 Agent 继续处理。Codex 与 Claude Code 当前都提供布尔字段 stop_hook_active:第一次准备结束时为 false,已因 Stop Hook 继续执行后为 true。
最保守的跨端策略只允许一次 continuation:
if event.get("stop_hook_active") is True:
return 0
if not acceptance_passes():
print("Required checks are still missing.", file=sys.stderr)
return 2
return 0需要多次重试时,应使用以 session_id / turn_id 为 key 的外部计数器,并设置明确上限和收敛条件。Claude Code 会在连续阻断 8 次后强制结束;Codex 当前公开文档没有承诺相同上限,不能依赖 Host 自动打破循环。
会话与压缩上下文
静态项目规则应放在 AGENTS.md、CLAUDE.md 或 Skill 中。SessionStart、PostCompact 更适合注入动态信息,例如当前分支、最近提交、外部任务状态或压缩后必须恢复的小段约束。
审计与通知
审计 Hook 只记录完成追踪所需的最小字段。通知、遥测和长测试适合异步或外部服务,但不应伪装成同步安全门禁;HTTP、网络或队列不可用时,要提前定义是否继续。
分层检查
一种稳定组合是:
PreToolUse做轻量本地阻断;PostToolUse做增量格式化和记录;Stop做一次回合级聚合校验;- pre-commit / CI 做最终完整测试和安全扫描。
这样可以兼顾即时反馈和最终覆盖,不让每次工具调用都承担完整测试成本。
十六、测试、可观测性与分层排错
Hook 应先作为普通程序测试,再接入 Host。推荐保存每类事件的最小 JSON fixture,并覆盖允许、拒绝、字段缺失、异常输入、超时和重复执行。
测试顺序
- 用 fixture 直接运行 handler,核对 stdout、stderr 和退出码;
- 用无副作用事件验证配置能被发现和匹配;
- 在
/hooks中确认来源、事件、matcher、启用和信任状态; - 使用真实但可回滚的工具调用做集成测试;
- 验证并发、超时、重试、Stop continuation 和失败模式;
- 最后再接入团队配置、Plugin 或 managed policy。
Claude Code 可使用:
claude --debug-file /tmp/claude-hooks.log
tail -f /tmp/claude-hooks.log更细的 matcher 日志可设置 CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose。Codex 应先用 /hooks 确认“已发现、已启用、已信任”,再用 /debug-config 检查配置层;需要详细运行日志时可设置 RUST_LOG=debug 并指定 log_dir。
分层排错表
| 现象 | 优先检查 | 常见原因 |
|---|---|---|
/hooks 中完全没有 | 配置路径、JSON/TOML、当前项目 | 写错 settings 路径、语法错误、项目不受信任 |
| 能看到但没有运行 | event、matcher、if、工具名大小写 | 事件选错、matcher 被忽略或不匹配 |
| Codex 显示待审核 | Hook 定义 hash、Plugin 更新 | 配置或命令变化后需要重新 trust |
command not found | cwd、绝对路径、依赖、权限 | 从子目录启动、脚本不可执行、PATH 不同 |
| JSON validation failed | stdout 内容、shell profile | 调试文本或欢迎语污染 JSON |
| 阻断没有生效 | 事件是否可阻断、退出码和字段 | 使用 exit 1、Post 事件已产生副作用、schema 写错 |
| 延迟明显 | 事件频率、网络、超时 | 在 PreToolUse 运行重测试或模型判断 |
| Stop 一直继续 | stop_hook_active、收敛条件 | 每次都返回 block,没有终止分支 |
| 偶发覆盖或重复 | 并发、幂等、共享文件 | 多个 handler 无序写同一资源 |
| 更新后路径失效 | Plugin 根目录、缓存、wrapper | 硬编码版本化安装路径 |
十七、常见反模式
| 反模式 | 问题 | 更合适的做法 |
|---|---|---|
| 把 Hook 写成长篇业务工作流 | 高频阻塞,难以恢复和测试 | 用 Skill / Agent 组织流程,Hook 只做门禁 |
用 PostToolUse 阻止副作用 | 动作已经发生 | 在 PreToolUse 拦截,Post 只反馈或修复 |
用 exit 1 期望统一阻断 | 多数事件不会按预期阻断 | 使用事件文档规定的 exit 2 或专属 JSON |
安全分支外总是返回 allow | 可能绕过正常确认 | 没有意见时静默 exit 0 |
多个 Hook 改同一 updatedInput | 并行完成顺序不确定 | 保留单一改写者 |
| 在每次工具调用运行完整测试 | 延迟被调用次数放大 | 增量检查下沉,完整检查放 Stop / CI |
| 异步 Hook 返回阻断决定 | 当前动作已经继续 | 异步只做观察、通知和延后反馈 |
| 把 transcript 当稳定接口 | 格式和写入时机可能变化 | 优先使用当前事件字段 |
| 硬编码 Plugin 缓存版本路径 | 升级或旧会话后失效 | 使用根变量、数据目录或稳定 wrapper |
| 默认记录全部输入输出 | 泄漏 prompt、token 和代码 | 字段白名单、脱敏和有界保留 |
| 把 Hook 当唯一安全边界 | 工具和入口覆盖不完整 | 与权限、Sandbox、CI 和服务端策略组合 |
| 不处理 Stop 重入 | Agent 无法结束或撞到上限 | 检查 active 标志和明确收敛条件 |
十八、核心结论
- Hook 是 Host 管理的生命周期扩展点,不等于 shell command,也不是通用跨端协议。
- Event 决定时机,matcher 决定范围,handler 决定动作,事件 schema 决定结果如何被解释。
PreToolUse可以阻止尚未执行的支持路径;PostToolUse不能撤销既有副作用;Stopblock 通常表示继续工作。- Codex 与 Claude Code 的配置形状相近,但事件、handler、matcher、trust 和返回协议均有差异。
- 安全 Hook 必须短小、幂等、并发安全、显式超时、输入受控,并能脱离 Host 单独测试。
- Hook 是 Agent 循环中的 guardrail 和反馈层;Sandbox、权限规则、managed policy 与 CI 才构成更完整的执行边界。
二十、参考链接
OpenAI / Codex
- Codex Hooks
- Codex advanced configuration
- Build Codex plugins
- Import from another agent
- Codex Hooks source
Anthropic / Claude Code
- Hooks reference
- Automate actions with hooks
- Claude Code settings
- Claude Code plugins reference
- Bash command validator example