Codex 生命周期 Hooks 实战指南:用钩子系统掌控 AI 编程的每一个关键节点

引言

Codex CLI 作为 OpenAI 开源的终端 AI 编程助手,除了提供基础的对话式编程能力外,还内置了一套强大的生命周期钩子系统(Lifecycle Hooks)。这套系统允许你在 AI Agent 执行任务的关键节点插入自定义逻辑,实现对工具调用、权限决策、会话生命周期的精细控制。

本文将从钩子事件类型、配置方式、实战示例三个维度,带你全面掌握 Codex Hooks 的使用方法。

Hooks 是什么

Hook 本质上是一个外部命令或脚本,Codex 在特定的生命周期事件发生时,会调用你配置的 Hook 程序,通过 stdin 传入 JSON 格式的上下文信息,并根据 Hook 的退出码和 stdout 输出来决定后续行为。

Hook 的能力包括:

  • 拦截和修改工具调用参数
  • 阻止危险操作
  • 注入额外的上下文信息给 AI 模型
  • 在会话/回合开始时执行自定义逻辑
  • 在工具执行后进行审计和记录

Hook 事件类型全览

Codex 支持 11 种 Hook 事件,覆盖从会话启动到结束的完整生命周期:

| 事件名称 | 触发时机 | 关键能力 |
|---------|---------|---------|
| PreToolUse | 工具执行之前 | 拦截/修改工具参数,阻止执行 |
| PermissionRequest | 权限请求时 | 自动化权限决策 |
| PostToolUse | 工具执行之后 | 审计、记录、注入上下文 |
| PreCompact | 上下文压缩之前 | 判断是否允许压缩 |
| PostCompact | 上下文压缩之后 | 压缩后处理 |
| SessionStart | 会话启动时 | 初始化、环境检查 |
| SessionEnd | 会话结束时 | 清理、通知 |
| UserPromptSubmit | 用户提交提示词时 | 提示词预处理、过滤 |
| SubagentStart | 子 Agent 启动时 | 子任务上下文准备 |
| SubagentStop | 子 Agent 停止时 | 子任务结果处理 |
| Stop | 回合结束时 | 回合级别收尾工作 |

配置 Hooks

配置文件位置

Hook 配置通过 JSON 文件定义,放在以下位置之一:

  • 用户级:~/.codex/hooks.json
  • 项目级:<项目根>/.codex/hooks.json
  • 插件内:随插件分发

基本配置结构

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "^Bash$",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.codex/hooks/security_check.py",
            "timeout_sec": 10
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.codex/hooks/on_session_start.sh"
          }
        ]
      }
    ]
  }
}

配置项说明

  • matcher:正则表达式,用于匹配工具名称。空字符串表示匹配所有
  • type:Hook 处理器类型,目前支持 "command"(执行外部命令)
  • command:要执行的命令,可以是脚本或可执行文件
  • timeout_sec:超时时间(秒),超时视为 Hook 失败
  • async:是否异步执行(不阻塞主流程),默认为 false

一个 Matcher Group 可以包含多个 Hook,它们会按顺序依次执行

Hook 的 stdin 输入格式

当 Hook 被触发时,Codex 会通过 stdin 传入一个 JSON 对象。以 PreToolUse 为例:

{
  "session_id": "thread_abc123",
  "turn_id": "turn_xyz789",
  "cwd": "/home/user/my-project",
  "hook_event_name": "PreToolUse",
  "model": "gpt-5",
  "permission_mode": "default",
  "tool_name": "Bash",
  "tool_input": {
    "command": "rm -rf /important-data"
  },
  "tool_use_id": "tool_use_001"
}

Hook 脚本可以解析这个 JSON,根据上下文件信息做出决策。

Hook 的返回值约定

Hook 通过退出码stdout 输出来表达其决定:

退出码约定

| 退出码 | 含义 |
|-------|------|
| 0 | 成功,允许操作继续 |
| 2 | 阻止当前操作(仅 PreToolUse) |
| 其他非零 | Hook 执行失败 |

stdout 输出格式(PreToolUse)

当需要阻止操作时,返回 JSON:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "禁止删除重要数据目录"
  }
}

当需要修改工具输入时:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": {
      "command": "ls -la /important-data"
    }
  }
}

当需要注入额外上下文时:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "additionalContext": "该目录包含生产数据,请谨慎操作"
  }
}

如果 stdout 输出为空或不是合法 JSON,Hook 视为成功通过(不阻止)。

实战示例

示例一:PreToolUse——阻止危险命令

创建一个安全审计 Hook,拦截 rm -rfDROP TABLE 等危险操作:

#!/usr/bin/env python3
import json
import sys

DANGEROUS_PATTERNS = [
    "rm -rf /",
    "rm -rf --no-preserve-root",
    "DROP TABLE",
    "DROP DATABASE",
    "shutdown",
    "reboot",
    "format C:",
    ":(){ :|:& };:",  # fork bomb
]

def main():
    input_data = json.load(sys.stdin)
    tool_name = input_data.get("tool_name", "")
    command = input_data.get("tool_input", {}).get("command", "")

    for pattern in DANGEROUS_PATTERNS:
        if pattern.lower() in command.lower():
            print(json.dumps({
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": f"检测到危险操作: 匹配模式 '{pattern}'"
                }
            }))
            return

    # 安全检查通过,允许执行
    sys.exit(0)

if __name__ == "__main__":
    main()

配置 hook:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "^Bash$",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.codex/hooks/security_check.py",
            "timeout_sec": 10
          }
        ]
      }
    ]
  }
}

当 Codex 尝试执行 rm -rf /tmp/cache 时,Hook 会检测到 rm -rf 模式并阻止操作,同时向用户反馈阻止原因。

示例二:SessionStart——会话初始化

在会话启动时自动加载项目环境变量、检查依赖:

#!/bin/bash
# ~/.codex/hooks/on_session_start.sh

input=$(cat)
session_id=$(echo "$input" | jq -r '.session_id')
cwd=$(echo "$input" | jq -r '.cwd')

echo "会话 $session_id 已启动,工作目录: $cwd" >> ~/.codex/session.log

# 检查 Node.js 依赖是否安装
if [ -f "$cwd/package.json" ] && [ ! -d "$cwd/node_modules" ]; then
    echo '{"additionalContext": "项目依赖未安装,建议先执行 npm install"}'
fi

exit 0

配置:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "~/.codex/hooks/on_session_start.sh"
          }
        ]
      }
    ]
  }
}

示例三:PostToolUse——审计日志

记录每次工具调用的详细信息,用于审计:

#!/usr/bin/env python3
import json
import sys
from datetime import datetime

def main():
    input_data = json.load(sys.stdin)

    log_entry = {
        "timestamp": datetime.now().isoformat(),
        "session_id": input_data.get("session_id"),
        "turn_id": input_data.get("turn_id"),
        "tool_name": input_data.get("tool_name"),
        "tool_input": input_data.get("tool_input"),
        "tool_response": input_data.get("tool_response"),
    }

    # 追加到审计日志
    with open(f"{__file__}/../../../audit.log", "a") as f:
        f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")

    sys.exit(0)

if __name__ == "__main__":
    main()

示例四:组合使用——完整的 CI/CD 安全检查流

在一个生产项目中,你可以组合多个 Hook 事件,形成完整的安全网:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "~/.codex/hooks/check_env.sh" }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "^Bash$",
        "hooks": [
          { "type": "command", "command": "python3 ~/.codex/hooks/safety_check.py" }
        ]
      },
      {
        "matcher": "^(GitHub|GitLab|Bitbucket)",
        "hooks": [
          { "type": "command", "command": "python3 ~/.codex/hooks/secret_scan.py" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "~/.codex/hooks/audit_logger.sh" }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "~/.codex/hooks/notify_complete.sh" }
        ]
      }
    ]
  }
}

Hook 的层级与来源

Hook 可以从多个来源加载,不同来源的 Hook 可以共存:

| 来源 | 说明 | 配置路径 |
|------|------|---------|
| User | 用户个人配置 | ~/.codex/hooks.json |
| Project | 项目级配置 | <project>/.codex/hooks.json |
| Plugin | 插件内置 | 插件包内 |
| Managed Config | 企业统一管理 | 远程配置下发 |
| System | 系统级 | 由管理员控制 |

多个来源的 Hook 会合并执行。管理员还可以通过设置 allow_managed_hooks_only = true 来禁用用户和项目级的 Hook,确保企业安全策略不被绕过。

调试 Hooks

手动测试 Hook 脚本

你可以直接向 Hook 脚本传入模拟 JSON 来测试其行为:

echo '{
  "session_id": "test_session",
  "turn_id": "test_turn",
  "cwd": "'$PWD'",
  "hook_event_name": "PreToolUse",
  "model": "gpt-5",
  "permission_mode": "default",
  "tool_name": "Bash",
  "tool_input": {"command": "rm -rf /tmp/test"},
  "tool_use_id": "test_001"
}' | python3 ~/.codex/hooks/security_check.py
echo "Exit code: $?"

查看 Hook 执行事件

在 Codex TUI 中,Hook 的执行状态(Running、Completed、Failed、Blocked)会以事件的形式展示在界面上,你可以直观地看到每个 Hook 的执行结果。

注意事项

性能影响:同步 Hook(async: false)会阻塞主流程,超时时间不宜设置过长,建议 5-10 秒

幂等性:Hook 可能被多次触发(如重试场景),应设计为幂等的

错误处理:Hook 返回非零退出码(除 2 外)视为失败但不阻止操作,这是一种"安全失败"策略

权限:Hook 脚本需要有可执行权限(Linux/macOS: chmod +x

环境隔离:Hook 运行在独立的子进程中,不会污染 Codex 的环境变量

总结

Codex 的 Hooks 系统为 AI 编程助手提供了一层可编程的"中间件",让你能够在 AI 执行任务的每个关键节点插入自定义逻辑。无论是安全审计、环境初始化、还是自动化流程编排,Hooks 都能帮你实现。

合理使用 Hooks,可以让你的 Codex 工作流更加安全、高效和可控。从简单的命令拦截到复杂的多阶段流程编排,Hooks 的灵活性足以应对各种场景。

如果你还没有尝试过,不妨从最简单的 SessionStart Hook 开始——在每次会话启动时自动加载项目环境,体验一下钩子系统带来的便利。