×−+
Hooks 与 Rules第 10 课 · Codex 教程
第 10 课 · HOOKS & RULES

Hooks 与 Rules:
把规矩写成机关

在 AGENTS.md 里写"别动 .env",是叮嘱:Codex 大多数时候会照做,但没人能保证它每次都记得。Hooks 和 Rules 是机关:到了固定的时刻、碰到特定的命令,就按你写好的规则办,不看模型当时怎么想。这一课给 order-admin 装上两个 hook 和两条规则。

约 40 分钟 11 节 · 5 个动手练习 章末测验 形式参考 luongnv89/claude-howto · 事实依据 OpenAI 官方文档
同一个任务,点上面切换看看
入门先用起来 原理弄懂为什么 实战动手练 深入进阶与避坑

时间紧就先看"入门"和"实战":知道写在哪、怎么信任,照着第 8 节的三个文件改一改就能用。"原理"讲每个时刻能做什么决定,决定你写的 hook 会不会"写了等于没写"。

01入门

叮嘱和机关

AGENTS.md、Hooks、Rules 都能"管住"Codex,只是管法不一样。先用三个比方把它们分清。

AGENTS.md 像贴在工位上的守则每次开对话它都读一遍,照着做。但守不守、怎么理解,最后靠它自己(第 5 课)。
Hook 像流水线上的自动检查站到了固定的时刻(改文件之前、改完之后、一轮要结束时……)就运行你写的脚本。脚本说拦就拦,说放就放。
Rule 像门卫手里的名单只看 Codex 要运行的命令,按命令开头对名单:有的直接放行,有的先问你,有的一律不许。

各管一摊

管什么谁来执行典型用法
AGENTS.md做事的习惯和约定模型读了之后自己照做测试怎么跑、代码风格、哪些文件别碰
Hook一次对话里的 12 个固定时刻你写的脚本,或者一个 MCP 工具改完自动格式化、拦下对 .env 的修改、收工前检查测试
Rule 实验Codex 要运行的命令Codex 按命令前缀比对git push 先问、rm -rf 禁止、某条常用命令免批准

官方举的 hook 用法:把对话记进你自己的日志系统;扫描提示词,拦下不小心粘贴进去的 API key;自动总结对话,存成长期记忆;一轮结束时跑一遍自定义检查;在某个目录下补充特定的提示。

Hooks 默认就开着,2026 年 5 月 14 日正式可用(GA)。Rules 还是实验功能,写法以后可能会变。两者在桌面 App、命令行、IDE 扩展里用的是同一份配置。

是护栏,不是保险箱

官方对 hooks 的定位是 "a useful guardrail, not a complete enforcement boundary":有用的护栏,但不是完整的强制边界。有些专门的工具调用会绕开 hook,联网搜索这类托管工具 hook 也看不到;Rules 只按命令前缀比对,换个写法就可能对不上。用它们挡住常见的手滑,安全底线还是权限档位(第 3 课)和 Git 检查点(第 4 课)。

官方的搭配建议

AGENTS.md 写规矩,再配上能强制执行的东西:pre-commit hook、linter、类型检查。这样同一个错误就不容易犯第二次。Codex 的 Hooks 和 Rules,就是把"强制执行"搬进了 Codex 自己的工作流程。

02原理

Hook 会在哪些时刻响

一次对话从开始到结束,有 12 个时刻可以挂 hook(官方叫"事件")。点每个时刻,看它什么时候响、能管什么。

事件名里的 Session(常译作"会话")可以理解为一个对话从打开到关闭,本课统一叫"对话"。虚线框里的三个时刻最常用:PreToolUse 能在动手前拦下,PermissionRequest 能替你回答批准,PostToolUse 只能事后处理。

一张表看全 12 个事件
事件什么时候响

matcher:只在某些情况下响

每个 hook 可以带一个 matcher,它是一个正则表达式,用来筛选"这次要不要运行"。不同事件筛的东西不一样:工具相关的事件筛工具名,SessionStart 筛启动方式,压缩相关的筛触发方式。写 "*"、写空字符串或者干脆不写,表示全部都算。

matcher 写法意思
Bash跑命令的时候
Edit|Write改文件的时候。改文件的工具叫 apply_patch,它也认 Edit、Write 这两个名字
^apply_patch$同样是改文件,^ 和 $ 表示整个名字必须一模一样
mcp__github__.*GitHub 这个 MCP 服务器的所有工具(工具名格式是 mcp__服务器__工具,第 9 课)
startup|resumeSessionStart:新开对话或恢复旧对话时
manual|autoPreCompact、PostCompact:手动压缩或自动压缩时

文档只说 matcher 是正则表达式,没说不加 ^…$ 时算"包含就行"还是"整个名字一样"。第 5 节的沙盘按"包含"处理;想要精确匹配,就加上 ^…$。

UserPromptSubmit、Stop、Interrupt 不看 matcher,写了也会被忽略,每次都运行。

03入门

写在哪、要先信任

hook 写在 hooks.json 里,或者写进 config.toml 的 [hooks] 表。写好还不算数:没审查、没信任的 hook 不会运行。

四个常用位置

位置管谁什么时候加载
~/.codex/hooks.json
~/.codex/config.toml 的 [hooks]
你自己,所有项目总是加载
<项目>/.codex/hooks.json
<项目>/.codex/config.toml 的 [hooks]
这个项目,可以提交给团队项目被信任时才加载
插件里的 hooks/hooks.json装了这个插件的人插件启用后(第 11 课)
管理员下发(requirements.toml 等)整个组织按策略加载,自动信任

几处都写了,全部加载:优先级高的配置层不会覆盖低的(这和第 7 课的普通配置项不一样)。同一个事件有好几个 hook 匹配时,它们会同时启动,一个 hook 没法阻止另一个开始。同一层里既有 hooks.json 又有 [hooks],Codex 会合并它们并在启动时警告:一层只用一种写法。

三层结构

1
事件在哪个时刻响,比如 PostToolUse。
2
matcher 组这次算不算匹配,比如 "matcher": "Edit|Write"。
3
处理器(handler)匹配了就运行的东西,一个组里可以有好几个。
同一个 hook 的两种写法
.codex/hooks.json
  • type:只支持 command(运行一条命令)和 mcp_tool(调用一个已经连上的 MCP 工具)。prompt、agent 两种会被读进来,但直接跳过。
  • timeout:单位是秒,不写默认 600 秒。SessionEnd 和 Interrupt 例外:默认 1 秒,最多 3 秒。
  • statusMessage:可选,hook 运行时显示的一句话。
  • async:设成 true 就在后台跑,Codex 不等它(第 4 节讲它的限制)。
  • 命令在对话的工作目录里运行。项目里的 hook 最好从 Git 仓库根目录找脚本(上面的 git rev-parse --show-toplevel),别写 .codex/hooks/… 这种相对路径:Codex 可能是从子目录启动的。

审查并信任

除了管理员下发的,每个 hook 都要你先审查、信任那一段具体的定义才会运行。Codex 把信任记在这段定义当前的哈希(相当于指纹)上:新加的、改过的 hook 都会被标成"待审查",信任之前一律跳过。

注意,信任记在 hook 的定义上。第 8 节的写法把真正的逻辑放在 protect_env.py 这类脚本文件里,文档没说只改脚本、不改定义时会不会触发重新审查。所以脚本本身也要走代码审查,别指望 Codex 替你盯着。

桌面 AppApp 里有审查、信任 hook 的流程(2026 年 5 月的更新加入),设置里也有 Hooks 页。入口和按钮文字以你的 App 为准。
命令行输入 /hooks:查看 hook 从哪来、审查新的或改过的 hook、信任,或者停用某一个。启动时如果有 hook 待审查,Codex 会提示你打开 /hooks。
管理员下发的来自系统、MDM、云端或 requirements.toml 的 hook 标为"托管",按策略自动信任,你也没法在 hook 列表里停用它们。
项目没信任,项目里的 hook 一个都不加载

项目 .codex/ 这一层(配置、hooks、rules)只在你信任这个项目时才加载;你个人 ~/.codex 里的照常加载。项目信任是第 3 课的内容。

Hooks 默认开着。要整体关掉,在 config.toml 里写 [features] 下面 hooks = false(功能开关见第 7 课)。

04原理

hook 怎么回答 Codex

Codex 把这次事件的详情写成一段 JSON,从标准输入(stdin)交给你的脚本。脚本用退出码和标准输出(stdout)回答。每个事件听得懂的回答不一样,答错了往往等于没答。

它收到什么

PreToolUse 收到的 stdin(示意)

每个事件都有 session_id、transcript_path、cwd、hook_event_name、model 这几个公共字段,再加上自己的字段。transcript_path 指向对话记录,但官方说记录的格式不稳定,别让脚本依赖它。

它怎么回答

退出码 0、什么都不输出算成功,Codex 照常往下走。大多数"没问题就放行"的 hook 就这么写。
退出码 2,理由写到 stderrPreToolUse、UserPromptSubmit 里是"拦下";PostToolUse 里是"把结果换成这段反馈";Stop、SubagentStop 里是"接着干"。
stdout 输出 JSON按这个事件支持的字段处理,见下表。
stdout 输出纯文本SessionStart、SubagentStart、UserPromptSubmit 会把它当成额外的上下文;PreToolUse、PermissionRequest、PostToolUse 等会忽略;Stop、SubagentStop、Interrupt 认为它无效。
事件能做的决定怎么写
PreToolUse拦下;改写后放行;补一句上下文permissionDecision: "deny";"allow" + updatedInput;additionalContext
PermissionRequest放行、拒绝,或不表态(照常弹窗)decision.behavior: "allow" / "deny";几个 hook 里有一个 deny 就拒绝
PostToolUse撤不回;把工具结果换成你的反馈decision: "block" + reason,或 continue: false
UserPromptSubmit拦下这条消息;补一句上下文decision: "block" + reason
Stop、SubagentStop接着干decision: "block" + reason(reason 变成新的提示词)
PreCompact、PostCompact不压缩了 / 压缩完就停continue: false
SessionStart、SubagentStart补充上下文纯文本,或 additionalContext
Interrupt、SessionEnd只能记录、清理输出影响不了 Codex
写错了会被当成失败,而且照常放行

PreToolUse 里返回 permissionDecision: "ask"、continue: false 这些还不支持的字段,Codex 会把这次 hook 标成失败、报出错误,然后照常执行工具调用。想"先问我再跑",用 Rules 的 prompt(第 6 节)。同样,后台运行(async: true)的 hook 什么都拦不住:它不能拦、不能批准、不能改写,也不能让 Codex 接着干。

练一练:猜结果

每种情况下,Codex 最后会怎么做?

05实战

钩子沙盘

选一个时刻、这次发生的事,写 matcher,再选 hook 的回答,看 Codex 最后怎么做。默认摆好的是"PreToolUse 拦下对 .env 的修改"。

1 · 事件
2 · 这次发生了什么
3 · matcher
4 · hook 的回答

沙盘假设 hook 已经审查、信任过(没信任的根本不会运行);JSON 里的 ID 和路径是示意。可以试试:PermissionRequest 配"改 .env";Stop 配 decision: "block";任何能拦的回答,再打开 async。

06入门

Rules:命令的门禁名单 实验

Rules 只看 Codex 要运行的命令:按命令开头对名单,决定放行、先问你,还是直接拦下。官方的说法是"控制 Codex 能在沙箱外面运行哪些命令"。

实验功能

Rules 目前标着 Experimental(实验),写法可能会变。用之前看一眼官方文档有没有更新。

写在哪、什么时候生效

  • 在某个配置层旁边建一个 rules/ 文件夹,里面放 .rules 文件。最常见的是你个人的 ~/.codex/rules/default.rules。
  • 项目里的 <项目>/.codex/rules/ 只在项目被信任时加载,和项目里的 hooks 一样。
  • Codex 在启动时扫描这些文件,所以改完要重启 Codex。
  • 在命令行里把一条命令加进"允许清单"时,Codex 会把它写进 ~/.codex/rules/default.rules。批准请求时它也可能主动建议一条规则(Smart approvals,默认开启),接受前看清楚前缀写的是什么。
  • 管理员可以在 requirements.toml 里强制加规则,而且只能是 prompt 或 forbidden,和你的规则合在一起算。

一条规则长什么样

.codex/rules/order-admin.rules(节选)
decision命中后
allow在沙箱外直接运行,不问你。不写 decision 时默认就是它
prompt每次匹配都先问
forbidden不弹窗,直接拦下
  • 最严格的赢:几条规则同时命中,按 forbidden > prompt > allow 取最严的那个。
  • 按参数逐个比对:Codex 把命令看成一串参数(git、push、origin、main),pattern 必须是它开头的那几个。某个位置写成列表表示"任选其一":["git", ["add", "commit"]] 同时管 git add 和 git commit。
  • justification:可选的理由,Codex 可能在批准或拒绝时显示出来。用 forbidden 时,官方建议顺便写上"该用什么代替"。
  • match / not_match:写在规则里的小测试。Codex 加载规则时会拿这些例子验一遍,写错了早发现。
  • 文件用 Starlark 语言写,长得像 Python,但规则引擎运行它时不会碰你的文件系统。

和权限档位是什么关系

沙箱是围墙(第 3 课),Rules 是围墙上的几扇小门。官方的建议是:遇到个别例外,写一条规则,往往比整体放宽权限更合适。另外两点值得记住:

  • prompt 规则对沙箱里本来就能跑的命令也管用:官方就是用它让网络白名单里的 curl 也得先过审批。
  • 在 Approve for me 档位(交给自动审查代你判断,设置里叫 Auto-review)下,prompt 规则命中的命令会先交给自动审查,而不是弹给你。默认的 Ask for approval 档位(每次越界都问你)下,才是弹给你。
用命令行测一测(命令行)

codex execpolicy check --pretty --rules .codex/rules/order-admin.rules -- git push origin main
它输出一段 JSON:最严格的决定是什么、命中了哪些规则、各自的 justification。可以写多个 --rules 把几个文件合起来测。execpolicy 命令目前还是预览版。

07实战

规则匹配器

三条示例规则(比第 8 节的 order-admin.rules 多一条 allow),输入一条命令,看它被怎么拆、命中哪条、最后怎么处理。规则可以开关,decision 可以改。

先补一个细节。有些命令会被包成一整段 shell 脚本发出,比如 bash -lc "git add . && rm -rf /",一段里藏着好几条命令。Codex 对 bash、zsh、sh 的 -c / -lc 这样处理:

  • 全是普通的词,只用 &&、||、;、| 连起来:拆成几条分别判断,最严格的赢。所以就算 git add 被 allow,后面藏的 rm -rf / 也会让整条命令放不过去。
  • 用了重定向(> >> <)、变量或替换($FOO、$(…))、赋值(FOO=bar)、通配符(* ?)、控制流(if、for):不拆,整段当成 ["bash", "-lc", "…"] 这一条去比对。
示例规则(在第 8 节的文件基础上多加了一条 allow)
Codex 要运行的命令
$

示意:真实的拆分用 tree-sitter 解析 shell 语法,这里用简化规则模拟;判断顺序和"最严格的赢"与官方文档一致。

08实战

演练:给 order-admin 装上

让 Codex 写好两个 hook(改完自动格式化、不许碰 .env)和两条规则(git push 要问、rm -rf 禁止),再一个个触发。中途有两处要你拿主意:写 .codex 要批准,新 hook 要信任。

画面为教学示意:文件行数、Codex 说的话、App 里信任 hook 的卡片样式都是虚构的;hook 和规则的行为与官方文档一致。

三个文件长这样

09实战

练一练:这件事交给谁管

7 个要求,各选一个最合适的地方:直接在对话里说、写进 AGENTS.md、写成 Hook,还是写成 Rule。

10深入

进阶与避坑

写 hook 和规则的人迟早会碰到的问题,按需展开。

旧教程对照
  1. "先在 [features] 里写 codex_hooks = true 才能用 hooks":现在默认开启,开关的键名叫 hooks;codex_hooks 还认,但已经是废弃的别名 已废弃。
  2. "Claude Code 的 hook 配置直接搬过来就能用":两边的 hook 确实很像(插件 hook 甚至会设 CLAUDE_PLUGIN_ROOT 兼容旧脚本),但 prompt、agent 类型在 Codex 里会被跳过,permissionDecision: "ask" 也不支持(算失败并照常执行)。从 Claude Code 导入后,官方也提醒要复查 hooks,因为"行为可能不同"(专题 D)。
  3. 2026 年 5 月以前的文章:Hooks 在 5 月 14 日才正式可用,之前写的事件列表和字段可能不全,以本课和官方文档为准。
Windows 上 hook 命令怎么写?
给处理器加一个 commandWindows(TOML 里也可以写 command_windows),它只在 Windows 上替换 command。比如 macOS 上用 python3,Windows 上换成 py -3。管理员下发的 hook 则用 windows_managed_dir 指定脚本目录。Windows 的其他细节见专题 B。
退出码 2 的坑
退出码 2 在 hook 里有特殊含义:拦下、替换结果或者接着干。如果你的脚本里调用的某个工具碰巧以 2 退出,而你又把它的退出码原样传了出去,Codex 就会当成你在拦截。脚本里要自己处理好退出码:没问题就明确 exit 0。
hook 输出太长会怎样?
给模型看的 hook 输出,每条默认约 2,500 token。超出的部分,Codex 会把全文存到临时目录的 hook_outputs/<对话 ID>/ 下,只给模型一段"开头 + 结尾"的预览和文件路径,官方叫它 spilling(溢出到磁盘)。返回 additionalContext 的 hook 可以用 additionalContextLimit 调这个阈值,设成 0 表示不限,但一个 hook 就可能吃光上下文。因为可能写到磁盘上,别在 hook 输出里放密钥。
能让 hook 直接调一个 MCP 工具吗?
可以,处理器写 "type": "mcp_tool",再填 server、tool 和 input。input 里可以用 ${tool_input.file_path} 这样的占位符引用事件里的字段。它用现成的 MCP 连接,不会去启动或重连服务器;工具出错、服务器不在,都不会拦下操作;它总是同步运行;SessionEnd 不支持这种 hook。
后台 hook(async)什么时候用?
只做记录、通知、不需要管控的活,比如把每次命令写进日志。后台 hook 跑完后,它补充的内容会在下一个安全点交给模型:这一轮还在进行,就等当前请求和工具调用结束;没在进行,就等你下次发消息,跑完本身不会开启新的一轮。每个对话最多同时跑 8 个后台 hook,多出来的排队;对话结束时没跑完的会被取消。SessionEnd 永远同步运行。
Stop hook 会不会让它停不下来?
Stop 收到的输入里有 stop_hook_active,表示这一轮是不是已经被 Stop hook 要求"接着干"过。脚本可以先看这个字段再决定要不要再次 block,免得一直接着干。另外,任何一个匹配的 Stop hook 返回 continue: false,都会压过其他 hook 的"接着干"。
规则写多宽合适?
官方建议前缀写得越精确越好:用 ["pnpm", "run", "lint"] 这样的,别用 ["python"]、["curl"] 这种放行一整个程序的。官方的说法是,宽的规则往往会把本该守住的那道边界整个抹掉。Codex 在批准时建议的规则也一样,接受前看清前缀。
规则能挡住所有危险命令吗?
挡不住。它是字面比对:["rm", ["-rf", "-fr"]] 对不上 rm -r -f;命令里一有变量、重定向,整段就不拆,你写的 rm 规则也看不到里面的 rm。另外,Rules 只比对要运行的命令。Codex 平时用编辑工具(apply_patch)改文件,那不是命令,规则对不上;用 shell 命令改也可能换各种写法。所以"别改 .env"交给同时匹配 Bash 和 apply_patch 的 PreToolUse hook 更稳。重要的东西,靠权限档位和 Git 检查点兜底。
公司想统一管 hook 和规则
管理员可以在 requirements.toml 里直接写 [hooks],用 managed_dir(macOS、Linux)和 windows_managed_dir 指定脚本目录,脚本本身要用 MDM 等工具另外分发。allow_managed_hooks_only = true 会跳过个人、项目、插件的 hook,只留托管的;再固定 [features].hooks = true,个人就关不掉。规则则写在 [rules] 的 prefix_rules 里,只能是 prompt 或 forbidden。
插件里带的 hook
插件默认从自己根目录的 hooks/hooks.json 读 hook,也可以在清单文件里另指路径。插件 hook 运行时能拿到 PLUGIN_ROOT(插件目录)和 PLUGIN_DATA(可写的数据目录)两个环境变量,为了兼容还会设 CLAUDE_PLUGIN_ROOT、CLAUDE_PLUGIN_DATA。装了、启用了插件,不等于信任了它的 hook,同样要审查。第 11 课细讲。
自动化脚本里不想每次都信任(命令行)
一次性的自动化,如果 hook 来源已经在 Codex 之外审核过,可以加 --dangerously-bypass-hook-trust,这一次运行不要求保存过的信任。名字里带 dangerously 是有原因的,日常别用。codex exec 还有 --ignore-rules 跳过个人和项目的规则文件,第 14 课讲。
11检验

小测验

8 道题,每题选完会立刻看到解析。