Codex CLI · v0.117.0 实测验证

Hooks 完整指南
5 个生命周期
一次写清楚

不是泛泛介绍。这份指南结合源码、schema 与真实实验,把 hooks 怎么开、怎么配、怎么调,以及 Stop 到底是什么、为什么能无限续跑,一次讲透。

事件数 5
适用版本 0.117.0+
测试平台 macOS
核对时间 2026-04
~/.codex/hooks.json
// 开关:config.toml
[features]
codex_hooks = true

// 规则:hooks.json
{
"hooks": {
"Stop": [
{
"hooks": [{
"type": "command",
"command": "python3 ~/.codex/stop.py"
}]
}
]
}
}

$ codex --enable codex_hooks
✓ Hooks enabled · SessionStart fired

先看这 10 条,再往下读

如果你只有几分钟,这 10 条是整份指南最值得记住的。

📌
事件集合是固定的
hooks 不是自由发明的事件系统。你不能创造 TaskDoneReviewFinished,只有 5 个固定事件。
📂
开关和规则分两个文件
开关写在 config.toml,规则写在 hooks.json,很多人第一眼会搞错。
⚠️
command 现在是字符串
旧示例用 argv 数组写法已过时。当前版本 command 必须是单个 shell 字符串,这是最容易踩的坑。
🔄
Stop 是 turn 结束,不是会话退出
很多人误解 Stop 是"整个终端关闭"。它实际是 Codex 准备结束当前这一轮 agent turn 时触发。
♾️
Stop 能让 Codex 无限续跑
在 Stop hook 里返回 decision: "block",Codex 就会被推回去继续干。这很危险,会不断消耗 token。
🔀
多个 hooks 是并发执行的
同一事件下命中的多个 hook 不是串行的,而是并发跑。写共享日志文件时要自己处理并发。

5 个生命周期事件

从会话开始到 turn 结束,Codex 在 5 个关键节点派发事件,每个都可以挂载自定义脚本。

🚀
SessionStart
thread 级
会话启动或恢复时触发。适合注入环境说明、仓库规范、约束上下文。
💬
UserPromptSubmit
turn 级
用户 prompt 真正提交前触发。可拦截敏感词、注入规则、阻止提交。
🛡️
PreToolUse
turn 级
工具执行前触发(当前主要对 Bash 生效)。可阻止危险命令执行。
📤
PostToolUse
turn 级
工具执行后触发(当前主要对 Bash 生效)。可追加上下文、清洗输出。
⏹️
Stop
turn 级
Codex 准备结束当前 turn 时触发。可阻止停止,让模型继续执行。
事件名 支持 matcher 可拦截 可注入上下文 最低版本
SessionStart continue: false additionalContext v0.114.0
UserPromptSubmit 忽略 decision: block 纯文本 stdout v0.116.0
PreToolUse 是(正则) decision: block 暂不支持 v0.117.0
PostToolUse 是(正则) 切断后续 additionalContext v0.117.0
Stop 忽略 decision: block 通过 reason v0.114.0
一次 Agent Turn 的生命周期
🚀
Session
Start
💬
User
Prompt
🛡️
Pre
Tool
📤
Post
Tool
⏹️
Stop

先开关,再写规则

hooks 需要先在 config.toml 打开功能开关,然后在单独的 hooks.json 里写规则。

第一步:打开开关(config.toml)
这是最稳定的方式,全局永久生效,不需要每次启动时手动指定。
第二步:写规则(hooks.json)
全局放 ~/.codex/hooks.json,项目级放 <repo>/.codex/hooks.json。两者可以同时生效。
TOML ~/.codex/config.toml
# 打开 hooks 功能开关
[features]
codex_hooks = true

# 可选:临时启动时启用
# codex --enable codex_hooks
JSON ~/.codex/hooks.json — 最小可用结构
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 ~/.codex/hooks/stop_demo.py",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
⚠️
项目级 hooks 需要 trust
如果项目不受信任,<repo>/.codex/hooks.json 可能不会被加载。发现项目级 hook 没反应,先检查 trust 设置。
ℹ️
非 git 目录也可以有项目级 hooks
如果找不到 .git 等标记,加载器会把当前 cwd 当作 project root。但子目录不会自动继承父目录的 hooks。

hook 是怎么运行的

理解执行模型,才能正确写脚本、正确读输入、正确返回输出。

🐚
通过 shell 启动
Unix/macOS 上优先用 $SHELL -lc,不可用时回退 /bin/sh -lc。所以 command 写成 shell 字符串是合理的。
📥
输入通过 stdin 传递
Codex 把原始 JSON payload 写到 stdin,用 json.load(sys.stdin) 读取最稳妥,不推荐按行读取。
⏱️
默认超时 600 秒
默认 timeout 是 600s,最小 1s,可以用 timeout 或别名 timeoutSec 字段控制。
🔀
多个 hooks 并发执行
同一事件下多个匹配 hook 是并发的,不是串行的。共享日志文件要自行加锁或拆开写。
Python 读取 stdin payload 的标准写法
import json
import sys

# 推荐写法:整体读取
payload = json.load(sys.stdin)

# 或者:先读再解析
# raw = sys.stdin.read()
# payload = json.loads(raw)

# hook 不只做副作用,还可以通过 stdout 控制 Codex 行为
# 只做通知时,返回空 JSON 最稳妥
print("{}")

每个事件逐一拆解

结合源码和实测结果,把每个事件的触发时机、能做什么、不能做什么写清楚。

🚀
SessionStart
thread 级 · v0.114.0+

在会话启动或恢复时触发,发生在你发第一条用户消息之前。source 字段当前除文档说的 startupresume 外,schema 里还能看到 clear

  • 通过 hookSpecificOutput.additionalContext 注入上下文
  • 纯文本 stdout 也会被当作 additional context
  • continue: false 可以中止后续处理
  • 支持 matcher(正则匹配)
Python session_context.py · 注入环境上下文
#!/usr/bin/env python3
import json

print(json.dumps({
    "hookSpecificOutput": {
        "additionalContext": (
            "You are running on macOS. Prefer Chinese replies. "
            "Do not assume old hook examples are correct; "
            "current command format is a shell string."
        )
    }
}))
💬
UserPromptSubmit
turn 级 · v0.116.0+

在用户 prompt 真正提交给模型前触发——不是在输入框里打字时触发,而是真正提交的那一刻。不支持 matcher,注入的 context 会进入 developer instructions 侧。

  • 纯文本 stdout 变成 additional context
  • decision: "block" 可以拦截提交
  • 退出码 2 加 stderr 也可以阻止提交
  • 不支持 matcher(会被忽略)
Python sanitize_prompt.py · 阻止密钥外发
#!/usr/bin/env python3
import json
import re
import sys

payload = json.load(sys.stdin)
text = json.dumps(payload, ensure_ascii=False)

patterns = [
    r"sk-[A-Za-z0-9]+",       # OpenAI key
    r"AKIA[0-9A-Z]{16}",     # AWS key
]

for pattern in patterns:
    if re.search(pattern, text):
        print(json.dumps({
            "decision": "block",
            "reason": "Prompt appears to contain a secret. Submission was blocked."
        }))
        raise SystemExit(0)

print("{}")
🛡️
PreToolUse
turn 级 · v0.117.0+ · 当前主要对 Bash 生效

名字听起来像"所有工具执行前都能挂",但当前 schema 里 tool_name 实际限定为 Bash。不要假设它覆盖全部工具。支持 matcher 正则匹配。

  • decision: "block" 可以阻止 Bash 执行
  • 退出码 2 加 stderr 也可以阻止
  • hookSpecificOutput.permissionDecision: "deny" 可用
  • allow / ask / updatedInput 暂不支持
  • additionalContext 暂不支持
  • continue: false / stopReason 暂不支持
Python pre_bash_guard.py · 阻止危险命令
#!/usr/bin/env python3
import json
import sys

payload = json.load(sys.stdin)
serialized = json.dumps(payload, ensure_ascii=False)

dangerous = [
    "rm -rf /",
    "mkfs",
]

for item in dangerous:
    if item in serialized:
        print(json.dumps({
            "decision": "block",
            "reason": f"Blocked dangerous command pattern: {item}"
        }))
        raise SystemExit(0)

print("{}")
📤
PostToolUse
turn 级 · v0.117.0+ · 当前主要对 Bash 生效

工具执行后触发。此时工具已经跑完了,decision: "block" 不是"阻止刚才的执行",而是把一个"反馈/替代结果"送回后续处理流程。

  • 可以追加 additionalContext
  • continue: false 可以切断后续处理
  • 退出码 2 加 stderr 变成反馈
  • updatedMCPToolOutput 当前未支持

Stop 是怎么让 Codex 无限续跑的

这是整份指南最核心的部分。Stop 不是"会话结束",也不是"扫描固定文本",而是一个可以被反复拦截的运行时生命周期钩子。

♾️ 无限续跑的工作原理
1
Codex 准备结束当前 turn

模型认为任务完成,运行时进入"准备停止"阶段

2
触发 Stop hook

运行时主动派发 Stop 事件,不是扫描模型输出的固定文本

3
hook 返回 decision: "block"

脚本输出 JSON,告诉 Codex "还不能停"

4
reason 被注入为隐藏提示

格式为 <hook_prompt ...>,作为隐藏用户消息送回

5
模型继续干,仍是同一个 turn_id

stop_hook_active 字段从 false 变成 true,可以用来判断是否已续跑过一次

6
再次准备停止 → 再次触发 Stop

如果 hook 继续 block,就会无限循环。如果不 block,turn 正常结束。

🔑
用 stop_hook_active 做安全续跑
第一次 Stop 时 stop_hook_active 是 false,续跑后变成 true。检查这个字段,就能做"只续跑一次"的安全版本。
💀
无限续跑非常危险
会不断消耗 token,容易造成无限循环,日志和状态文件持续增长,通常只能靠手动中断。
Python 安全版:只续跑一次
#!/usr/bin/env python3
import json
import sys

payload = json.load(sys.stdin)

# 已经续跑过一次,不再拦截
if payload.get("stop_hook_active"):
    print("{}")
else:
    print(json.dumps({
        "decision": "block",
        "reason": "Do one more check pass."
    }))
Python 通知版:每轮完成后弹 macOS 通知
#!/usr/bin/env python3
import json
import subprocess
import sys

payload = json.load(sys.stdin)
message = payload.get("last_assistant_message", "") or "Codex finished."
message = " ".join(message.split())[:120]

apple_script = (
    f'display notification {json.dumps(message)} '
    f'with title {json.dumps("Codex Stop")}'
)
subprocess.run(
    ["/usr/bin/osascript", "-e", apple_script],
    check=False,
    stdout=subprocess.DEVNULL,
    stderr=subprocess.DEVNULL,
)
print("{}")  # 不 block,只做通知

5 种常见用途,直接拿去用

根据你的实际需求选择合适的方案,比照着改一改就能跑。

🔔
每轮完成后弹通知
不需要 hooks,内建的 notify 更简单直接。在 config.toml 里加一行。
→ config.toml notify
🔁
模型完成后补一次检查
用 Stop 事件 + stop_hook_active 判断,实现"只续跑一次"的安全模式。
→ Stop + stop_hook_active
🔒
阻止危险 Bash 命令
用 PreToolUse 在命令执行前检查,发现危险模式立即阻断。
→ PreToolUse + decision: block
🚫
阻止密钥等敏感信息外发
用 UserPromptSubmit 正则扫描 prompt 内容,命中后 block 提交。
→ UserPromptSubmit + 正则拦截
📋
自动注入项目规范
用 SessionStart 或 UserPromptSubmit 给每次对话自动追加仓库规则。
→ SessionStart additionalContext
Python debug_dump.py · 调试时先用这个看 payload 长什么样
#!/usr/bin/env python3
# 完全不知道 stdin 里是什么时,先跑这个把 payload 落盘
import sys
from pathlib import Path

raw = sys.stdin.read()
Path("/tmp/codex-hook-dump.json").write_text(raw, encoding="utf-8")

print("{}")

最容易踩的 6 个坑

这些错误在网上旧帖子、旧示例里非常常见,现在花一分钟记住,省去之后的排查时间。

1
command 写成数组而不是字符串

旧示例:["bash", "-c", "echo hello"]。当前版本要求是单个 shell 字符串:"echo hello"。这是第一大坑。

2
把 Stop 理解成"整个终端会话关闭"

Stop 是 turn 级 事件,是"Codex 准备结束当前这一轮 agent turn",不是整个 shell 退出,也不是"用户半天没说话"。

3
在 config.toml 里写 hooks 规则

开关写在 config.toml[features] 节,但 hooks 具体规则必须写在 hooks.json

4
以为 PreToolUse / PostToolUse 能拦住所有工具

当前版本里,这两个事件实际上主要针对 Bash。不要默认以为它已经覆盖全部工具生态。

5
在 UserPromptSubmit 和 Stop 里用 matcher

这两个事件会完全忽略 matcher 字段,写了也没用。只有 SessionStart、PreToolUse、PostToolUse 支持 matcher。

6
schema 里有的字段不代表已经实现

当前 asyncpromptagentallowaskupdatedInputupdatedMCPToolOutput 虽然在枚举里出现,但当前版本未支持。

hook 没反应?按这个顺序查

按序逐项排查,通常前 4 步就能找到问题。

确认 codex --version 版本足够新
config.toml 是否开了 codex_hooks = true
改的是 hooks.json,不是 config.toml
command 是否写成了当前版本支持的字符串
脚本有执行权限,或命令里指定了解释器
脚本能否正确从 stdin 读取 JSON
期望用 matcher 的事件是否其实不支持
PreToolUse 不能拦住非 Bash 工具
项目级 hooks 是否因 trust 问题未加载
hook 是否超时(默认 600s)
Stop hook 是否返回了非法 JSON
看 rollout 日志确认 turn_id 是否被续跑
Shell 查看 rollout 日志确认 Stop 续跑
# 查看当天的 rollout 日志
ls ~/.codex/sessions/$(date +%Y/%m/%d)/*.jsonl

# 确认 turn_id 是否被续跑(同一 turn_id 出现多次)
# 以及是否出现了隐藏的 <hook_prompt ...> 内容
cat ~/.codex/sessions/YYYY/MM/DD/rollout-<id>.jsonl | grep turn_id
📄
Codex CLI Hooks 完整指南(脱敏版)
Codex-CLI-Hooks-Guide-Redacted.md · Markdown 文档
↓ 下载文档