MarkZ

一、什么是 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 加载动态环境信息或少量会话上下文;
  • PreCompactPostCompact 在上下文压缩前后保存或恢复关键信息;
  • 通知类事件把等待输入、任务完成或错误状态转发到桌面或外部服务。

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 通常嵌在三层生命周期里:会话包含多个回合,每个回合又可能包含多次工具调用。

Agent Hook 生命周期与事件处理流程

图中最重要的结论是:执行前事件可以阻止尚未发生的动作,执行后事件只能处理结果,不能撤销已经产生的副作用。 Stop 也不是“整项任务完成”的同义词,它表示 Agent 当前准备结束一次响应;阻断 Stop 通常意味着要求 Agent 继续工作。

一个 Hook 配置通常有三层:

  1. Event:选择生命周期节点,例如 PreToolUse
  2. Matcher group:过滤这次事件是否相关,例如只匹配 Bash
  3. 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.jsonPlugin 启用期间
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 明确暴露的工具路径
PermissionRequestHost 即将请求用户审批时自动允许或拒绝特定审批没有审批就不会触发
PostToolUse工具已完成后格式化、审计、反馈结果无法撤销副作用
SubagentStart子 Agent 启动时注入角色上下文通常不能阻止启动
SubagentStop子 Agent 准备结束时验收、要求继续必须避免循环
PreCompact上下文压缩前保存状态、阻止不安全压缩高频大输出会反向增加成本
PostCompact上下文压缩后恢复关键提示、记录摘要已无法改变之前的压缩过程
Stop当前响应准备结束时测试、证据和输出契约校验阻断通常表示继续工作

Claude Code 的扩展事件

截至 2026-07-17,Claude Code 的公开 reference 列出 30 个事件。除上表主干外,还包括:

类别事件
初始化与会话SetupInstructionsLoadedSessionEnd
用户输入与显示UserPromptExpansionMessageDisplay
工具与权限PermissionDeniedPostToolUseFailurePostToolBatch
Agent 与任务TaskCreatedTaskCompletedStopFailureTeammateIdle
环境与配置NotificationConfigChangeCwdChangedFileChanged
Worktree 与 MCPWorktreeCreateWorktreeRemoveElicitationElicitationResult

完整事件输入、matcher 对象和决策字段以 Claude Code Hook lifecycle 为准。StopFailurePostToolUseFailurePostToolUse 的失败语义不同,不应合并成一个模糊的“结束事件”。

Codex 当前事件范围

Codex 当前公开支持上表中的 10 个主干事件。PreToolUsePermissionRequestPostToolUse 明确覆盖 Bashapply_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。promptagent 把模型判断引入生命周期,不应再被描述为纯确定性逻辑。详见 Hook handler fields

command Hook 有两种执行形式:

  • 配置 args 时使用 exec form,不经过 shell,路径和参数边界更清楚;
  • 省略 args 时使用 shell form,支持变量、管道和重定向,但同时扩大命令注入与转义风险。

Claude Code 的工具事件还可以在单个 handler 上配置 if,使用权限规则语法按工具参数做二次过滤。它适合在启动脚本前减少无关匹配,但解析失败时会选择运行 handler,因此不能替代权限系统或 handler 内部校验。一个 if 只接受一条规则,不支持用 &&|| 或数组拼接多条条件。

Codex 的当前限制

Codex 当前只运行 type: "command"promptagentasync 配置即使能被解析,也会被跳过;异步 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: falsedecision: "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_ROOTCLAUDE_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 = false

Claude 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;不能把 403500 当成可靠门禁。

十三、性能与可靠性

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 的实现差异

维度CodexClaude Code
当前公开事件10 个主干事件30 个事件,覆盖更多配置、任务、显示和 MCP 生命周期
当前可执行 handlercommandcommandhttpmcp_toolpromptagent
Agent Hook解析但跳过支持,标记 experimental
Async command Hook解析但跳过支持;asyncRewake 可在失败后唤醒,但不能阻断当前动作
用户配置~/.codex/hooks.jsonconfig.toml~/.claude/settings.json
项目配置<repo>/.codex/hooks.jsonconfig.toml.claude/settings.json / .local.json
Plugin 默认路径hooks/hooks.jsonhooks/hooks.json
组件内 Hook当前公开文档未定义通用 Skill/Agent frontmatter HookSkill / 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.mdCLAUDE.md 或 Skill 中。SessionStartPostCompact 更适合注入动态信息,例如当前分支、最近提交、外部任务状态或压缩后必须恢复的小段约束。

审计与通知

审计 Hook 只记录完成追踪所需的最小字段。通知、遥测和长测试适合异步或外部服务,但不应伪装成同步安全门禁;HTTP、网络或队列不可用时,要提前定义是否继续。

分层检查

一种稳定组合是:

  1. PreToolUse 做轻量本地阻断;
  2. PostToolUse 做增量格式化和记录;
  3. Stop 做一次回合级聚合校验;
  4. pre-commit / CI 做最终完整测试和安全扫描。

这样可以兼顾即时反馈和最终覆盖,不让每次工具调用都承担完整测试成本。

十六、测试、可观测性与分层排错

Hook 应先作为普通程序测试,再接入 Host。推荐保存每类事件的最小 JSON fixture,并覆盖允许、拒绝、字段缺失、异常输入、超时和重复执行。

测试顺序

  1. 用 fixture 直接运行 handler,核对 stdout、stderr 和退出码;
  2. 用无副作用事件验证配置能被发现和匹配;
  3. /hooks 中确认来源、事件、matcher、启用和信任状态;
  4. 使用真实但可回滚的工具调用做集成测试;
  5. 验证并发、超时、重试、Stop continuation 和失败模式;
  6. 最后再接入团队配置、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 foundcwd、绝对路径、依赖、权限从子目录启动、脚本不可执行、PATH 不同
JSON validation failedstdout 内容、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 不能撤销既有副作用;Stop block 通常表示继续工作。
  • Codex 与 Claude Code 的配置形状相近,但事件、handler、matcher、trust 和返回协议均有差异。
  • 安全 Hook 必须短小、幂等、并发安全、显式超时、输入受控,并能脱离 Host 单独测试。
  • Hook 是 Agent 循环中的 guardrail 和反馈层;Sandbox、权限规则、managed policy 与 CI 才构成更完整的执行边界。

二十、参考链接

OpenAI / Codex

Anthropic / Claude Code


相关笔记