Codex从零开始制定规则与钩子机制完整实战指南
先还原一段我上个月真实发生的对话。我跟同事在排查一个CI老挂的问题:
就是这一句话点醒了我。回去配置了一个PostToolUse钩子,从那以后Codex每改完一个文件,格式化就会自动执行,我再也没手敲过,也再也没有被CI打回来过。这篇文章,我会把规则和钩子这两个概念,从「是什么」一直讲到「怎么配、怎么调」。
看完这篇文章,你将掌握:
- 一句话说清规则和钩子各自负责什么,以及为什么它们能实现AGENTS.md做不到的「确定性」
- 规则(
.rules文件/prefix_rule)如何编写,让某条命令「永远放行」或「直接禁用」,以及怎么用codex execpolicy check先测试再使用 - 钩子配置在哪些文件中,生命周期里有哪些「时机」(
PreToolUse/PostToolUse/Stop/SessionStart等)可以挂载 - Codex钩子特有的信任机制——为什么新钩子默认不执行,你需要在
/hooks里先「过目签字」 - 钩子与Codex的通信方式(stdin的JSON、退出码、stdout的JSON),以及和Claude Code几个反直觉的差异
- 两个可以直接照搬的真实案例:改完后自动格式化、拦截危险命令;外加钩子不触发时的排查方法
01 先分清:规则管「闸门」,钩子管「扳机」
开篇先把这两者的分工明确下来,因为名称听起来都比较抽象,新手最容易混淆。
规则(Rules)解决的是「这条命令到底能不能在沙箱外运行」——它是一道闸门,盯着你想要执行的命令,按照你写死的规则判断「放行 / 先询问 / 直接禁止」。
钩子(Hooks)解决的是「工作流程走到某个节点时,自动替我运行一段脚本」——它像一个扳机,挂在生命周期的固定时机上,事件一来就会触发,跟Codex自己想不想执行没有关系。
类比:小区门口的两样东西——门禁卡和自动感应灯。
门禁系统里存着一张「谁能进、谁要登记、谁直接拦」的列表,这就是规则:每次有人刷卡,它对照列表判断。而楼道里那盏「人一走过就亮」的感应灯,就是钩子:它不关心你是谁、要去哪,只要「有人经过」这个事件发生,灯就一定会亮。一个负责判断准入,一个负责自动响应——两件事,不要混为一谈。
那么它们和你已经会写的AGENTS.md(项目说明书,第11篇讲过)有什么区别?区别在于一个根本点:
简单来说:
- AGENTS.md里写「改完记得格式化」「别碰生产库」——这是你拜托Codex去做的事,它大概率会照做,但每一步都在「自己判断」,可能会漏、可能会忘。
- 配成钩子的格式化、配成规则的禁令——只要那个条件触发,动作一定执行(或一定被拦截),跟Codex想不想、记不记得没有任何关系。
这就是开头那段对话的核心:「改完跑格式化」写在AGENTS.md里只是一个请求,三次可能漏一次;配成钩子则是保证,一次都不会漏。
下面列出几个你大概率会遇到、值得「上保证」的场景,先感受一下:
- 「
gh pr view这条命令我天天用,别每次都弹审批问我」——用规则给它开一个「永远放行」的通道 - 「
rm -rf、git push --force这种,谁来都给我硬拦截」——用规则或钩子确定地拦住,不靠提示 - 「每次改完文件,自动跑格式化 / lint」——用钩子,别再手敲,也别指望它自觉
02 规则(Rules):为单条命令立规矩
先说规则解决的痛点。第15篇讲过,沙箱是「按圈划分」的粗边界——圈里随便操作,出圈就问。但有时候你需要的是按命令的精细控制:「gh pr view哪怕出圈也直接放行,别烦我」「grep一律禁用,逼我自己用rg」。这种需求,扩大整个沙箱太粗暴,写一条规则才是对症下药。
类比:给保安一张「特批 / 拉黑」名单。沙箱是保安的默认守则(「陌生人一律先登记」),规则是你额外塞给他的两张小纸条:一张写「这几个老熟人放进来,别拦」,一张写「这几个人来了直接轰走」。保安照着名单办,比每次都临时问你省事,也比「把大门拆了」安全。
它真实会用在哪:
- 团队里
gh、make test这类高频只读命令,开一个白名单通道,省掉每天几十次审批点击 - 把
grep、find这类有更好替代品的命令标记为禁用,强制团队走rg、fd - 公司层面把
rm -rf /、改~/.ssh这类高危前缀直接设为forbidden,焊死团队红线
规则文件长什么样、放哪
规则写在.rules文件里,这个文件需要放在某个生效配置层旁边的rules/文件夹下,最常用的是用户级~/.codex/rules/default.rules。语法看起来像Python,实际上是Starlark(一种「能安全执行、不会乱碰你文件系统」的配置语言)。
一条最简单的规则长这样——「gh pr view出沙箱前先问我一声」:
# 运行带 `gh pr view` 前缀的命令前,先弹审批问我。
prefix_rule(
# 要匹配的命令前缀(按参数逐个写)。
pattern = ["gh", "pr", "view"],
# 匹配到时的动作:allow(直接放行)/ prompt(先问)/ forbidden(直接拦死)。
decision = "prompt",
# 可选:写清这条规则为什么存在,审批弹窗里可能会显示。
justification = "查看 PR 允许,但要我点头",
# 可选的「内联单元测试」:举例哪些命令该命中、哪些不该,加载时 Codex 会替你验证。
match = [
"gh pr view 7888",
"gh pr view --repo openai/codex",
],
not_match = [
# 不命中:pattern 必须是「精确前缀」,这条把 view 挪后面了。
"gh pr --repo openai/codex view 7888",
],
)
prefix_rule里几个字段,挑最需要了解的说:
| 字段 | 作用 | 备注 |
|---|---|---|
| pattern(必填) | 要匹配的命令前缀,按参数逐个列出 | 元素可以是字面量 "pr",也可以是「或」["view", "list"] |
| decision(默认allow) | 命中后怎么办 | allow / prompt / forbidden,最严的赢 |
| justification(可选) | 这条规则的理由 | 审批 / 拒绝时可能会显示给你 |
| match / not_match(可选) | 举例验证规则是否正确 | 加载时 Codex 替你跑,提前发现写错 |
decision三选一,优先级官方写死——最严的赢(forbidden > prompt > allow):
出沙箱直接跑,不问
写完.rules别忘了重启Codex才能生效。还有一个你自然会碰到的点:当你在TUI里手动把某条命令「加进允许列表」时,Codex会自动帮你写进~/.codex/rules/default.rules——所以这个文件往往不是你纯手写的,而是用着用着自己长出来的。
一个救命的安全设计:复合命令会被拆开判
这条特别重要,第15篇点过,这里展开。遇到git add . && rm -rf /这种把多条命令塞进一行的,Codex在安全的前提下会用tree-sitter把它拆成单条逐个判断,再取「最严的结果」:
["bash", "-lc", "git add . && rm -rf /"]
会被拆成两条单独评估:
["git", "add", "."]
["rm", "-rf", "/"]
关键就在这里:哪怕你allow了git add,那个rm -rf /也会被单独拦下,整条命令不会被「夹带过关」。这堵死了「把危险命令藏在安全命令后面偷渡」的路径。
但要留个心眼——只有「干净的线性命令链」才会被拆。如果脚本里用了重定向(>)、变量替换($(...))、环境变量赋值(FOO=bar)、通配符(*)这些高级特性,Codex不会去拆它,而是把整段当成一条bash -lc "<整个脚本>"来判断。所以别指望规则能看穿任意复杂shell——它在「安全可拆」时帮你细查,「拆不动」时保守对待。
改完规则,先用execpolicy check验一把
别上来就实战。改完规则文件,用codex execpolicy check拿一条命令试试它会怎么判:
codex execpolicy check --pretty --rules ~/.codex/rules/default.rules -- gh pr view 7888 --json title,body,comments
它会输出一段JSON,告诉你最严的判决是什么、命中了哪几条规则(连justification一起带出来)。我自己写forbidden规则时养成了一个习惯:先用execpolicy check把「该拦的拦住、不该拦的别误伤」验一遍,再重启Codex。有次我图省事直接上,结果一条写宽了的forbidden把团队常用的git log也连带拦了,被同事吐槽半天——execpolicy check跑一下本可以提前发现。
03 钩子(Hooks):有哪些「时机」能挂
规则讲完,转到钩子。钩子不是随便什么时候都能挂,它必须挂在Codex工作流程里特定的「时机」上——官方叫做事件(event)。想用好钩子,第一步就是认清「我想让这事在什么时候发生」。
回想第02篇讲的「袋里循环」——Codex干活是「想 → 做 → 看」转圈。这些事件,正好分布在这个循环的前前后后。先画张图,看几个最常用的事件卡在哪儿:

这张图把「想 → 做 → 看」摊开了:一进会话是SessionStart,你说话是UserPromptSubmit,然后进入「要不要用工具」的循环——每次动工具,前面有PreToolUse、后面有PostToolUse,这一轮答完就是Stop。你想让动作在哪一步发生,就挂对应的那个事件。
Codex支持的事件不止这几个(还有压缩前后的PreCompact/PostCompact、子袋里起停的SubagentStart/SubagentStop、审批请求时的PermissionRequest等),但对新手来说,先把下面这四个吃透,能覆盖九成场景:
| 事件 | 什么时候触发 | 最典型的用法 |
|---|---|---|
| PreToolUse | 某个工具执行前 | 拦截危险命令、改写命令(能阻止操作) |
| PostToolUse | 某个工具产出结果之后 | 改完文件自动格式化 / 跑 lint |
| Stop | Codex 答完这一轮 | 让它「再多跑一趟」(比如把没过的测试再修一遍) |
| SessionStart | 会话开始 / 恢复时 | 往上下文里注入项目状态(如最近的提交) |
第五个值得知道的是PermissionRequest(将要弹审批弹窗时触发),能自动放行或拒绝——配合第02节讲的规则一起用,比单独靠规则更灵活,尤其适合「高频命令免审批」这种场景。
记这张表有个窍门:看名字里的Pre和Post——Pre是「之前」,所以只有它能在动作发生前拦住;Post是「之后」,工具都跑完了,它撤不回已经产生的副作用,只能「事后补一刀」(格式化、记日志、把结果退回让模型重看)。

这张图把上面四个事件摊在Codex一次会话的生命周期上:会话开始 / 工具调用前 / 工具调用后 / 会话结束自上而下串成主轴,每个固定点旁边各挂一个hook脚本——流程一走到那个点,挂着的脚本就被「到点自动跑」拉起来,跟Codex记不记得没关系。这就是钩子「扳机」的本质:你只管挂上,开火由时机决定。
这里有个易错点得专门说:和Claude Code不同,Codex这边Stop事件里decision: "block"不是「拒绝这一轮」,而是反过来——它告诉Codex「别停,再来一轮」,并把你给的reason当成下一条用户提示。PostToolUse的decision: "block"也不撤销已经跑完的命令,而是把反馈塞回去让模型接着往下想。Codex的block在这两个事件上更像「续一轮 / 退回重看」,不是「否决」,别按Claude Code的直觉理解。
04 钩子配在哪、matcher怎么把它收窄
知道了能挂哪些事件,来看怎么写、写哪。
钩子放在哪几个文件
Codex找钩子的地方有两种形态:独立的hooks.json,或config.toml里内联的[hooks]表。官方说最常用的四个位置是:
| 配置文件 | 生效范围 | 能共享给团队吗 |
|---|---|---|
| ~/.codex/hooks.json | 你所有项目 | 否,只在你这台机器 |
| ~/.codex/config.toml | 你所有项目 | 否 |
| 仅当前项目 | 是,可提交进 git | |
| 仅当前项目 | 是 |
逻辑和第15篇讲配置时一样:全队都该有的钩子(比如「改完一律格式化」)写进项目的.codex/提交进git;只是你自己想要的写进~/.codex/。两个细节记一下:
- 多个来源的钩子会全部加载、一起跑——高优先级的配置层不会替换低优先级的钩子,是叠加关系。
- 同一层里别同时写
hooks.json和内联[hooks],Codex会合并但会在启动时警告你,官方建议一层只用一种写法。 - 项目级钩子只在这个
.codex/层被「信任」时才加载(信任是什么,下一节讲);不信任的项目里,Codex仍会加载你的用户级和系统级钩子。
钩子配置长什么样
先看一个最小的完整例子——「每次Codex用Bash跑完命令,触发一个脚本」,写进项目根的.codex/hooks.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/post_tool_use.py\"",
"timeout": 30,
"statusMessage": "Reviewing Bash output"
}
]
}
]
}
}
别被嵌套吓到,它就三层,对着拆一下你立刻懂:
"PostToolUse"——挂哪个事件(这里:工具产出结果后)。"matcher": "Bash"——收窄到哪个工具才触发(这里:只在Bash之后)。- 里层的
hooks数组——真正要跑的动作:"type": "command"表示跑一条shell命令,"command"就是那条命令。
这里有几个Codex特有的、最容易想当然写错的默认值,挑出来钉死:
timeout的单位是「秒」,不是毫秒;省略不写时,默认是600秒。- 目前只有
type: "command"的处理器真会跑;prompt和agent类型「能被解析但会被跳过」;async: true的也会被跳过(异步钩子还没支持)。 - 同一事件上有多个匹配钩子时,它们是并发启动的——你的拦截钩子不会阻止另一个同事件匹配钩子同时跑。这是Codex和Claude Code的又一差异点:写
PreToolUse拦截逻辑时,别以为它能「先跑完再决定其他钩子跑不跑」。 - 命令的工作目录是会话的
cwd。所以仓库内的钩子,官方建议别用.codex/hooks/...这种相对路径,因为Codex可能从子目录启动——用$(git rev-parse --show-toplevel)从git根算绝对路径才稳(上面例子就是这么写的)。 - Windows想用不同命令,加一个可选的
command_windows(TOML里也可写commandWindows)字段覆盖。
写成config.toml内联TOML是等价的,长这样:
[[hooks.PostToolUse]]
matcher = "^Bash$"
[[hooks.PostToolUse.hooks]]
type = "command"
command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.codex/hooks/post_tool_use.py"'
timeout = 30
statusMessage = "Reviewing Bash output"
matcher:让钩子「只在该触发的时候触发」
matcher是钩子配置里最该搞懂的一个字段。一句话:没有它,钩子会在那个事件的「每一次」都触发;有了它,你能把范围收窄。
类比:还是楼道那盏感应灯——你得圈定它的感应范围。灯如果整层楼都感应,那隔壁单元有人走动它也亮,纯浪费电。你得把感应区圈在「只有自家门口那段过道」。matcher干的就是这个「圈定范围」的活。
Codex的matcher和Claude Code有个关键区别:它是一个正则表达式(regex)字符串。对工具类事件(PreToolUse/PostToolUse),它匹配的是工具名。
| 你写的 matcher | 含义 | 例子 |
|---|---|---|
| "Bash" | 匹配 Bash 工具 | 只在跑 Bash 命令时触发 |
| "^apply_patch$" | 用正则精确匹配 | 只在改文件时触发 |
| "Edit|Write" | 改文件的别名(| 是正则的「或」) | 只在编辑 / 写文件之后触发 |
| "mcp__filesystem__.*" | 正则匹配一批 MCP 工具 | 这个 MCP server 的所有工具 |
| "*" / "" / 省略 | 匹配所有 | 该事件每次发生都跑 |
几个新手最容易栽的坑:
- 改文件的工具名是
apply_patch,不是Edit/Write。Codex改文件走的是apply_patch机制——你在matcher里可以用Edit、Write、apply_patch任意一个来匹配它(互为别名),但钩子收到的stdin里tool_name始终报的是"apply_patch"。想在脚本里判断工具名,记得认apply_patch。 - 不是所有事件都认
matcher。UserPromptSubmit和Stop这俩无视matcher,你写了也被忽略,因为它们没有「工具名」这种东西可筛,总是每次都触发。SubagentStop则不同,它的matcher作用于agent_type,是支持的——别把SubagentStop和Stop混在一起理解。 SessionStart的matcher匹配的不是工具名,而是「会话怎么起来的」(startup/resume/clear/compact)。PreToolUse拦不住所有命令。官方说得很直白:它是「护栏,不是密不透风的强制边界」——目前只拦得住简单的shell调用、apply_patch和MCP工具,更复杂的shell(走unified_exec的)、WebSearch这类它拦不到。所以别拿PreToolUse当唯一的安全防线,它是「多一道闸」,不是「万能墙」。
05 信任机制:为什么你的新钩子「默认不跑」
这一节是Codex钩子区别于Claude Code的最大不同,也是新手第一次配钩子最容易懵的地方:你明明把钩子写好了,它却没跑——因为Codex默认不信任它。
先说为什么有这道关。钩子是用你完整用户权限运行的shell脚本,能删你能删的任何文件、能联网把数据发出去。要是从网上抄一段插件、或者克隆一个仓库,里头夹带的钩子自动就跑,那等于谁都能在你机器上偷偷执行代码。所以Codex加了一道「过目签字」的关卡。
类比:新装的App第一次要权限,得你亲手点「允许」。你装个新App,它想用相机、想发通知,系统不会替你默认放开,而是弹一下「是否允许」——你不点,它就用不了。Codex的钩子信任就是这个机制:非托管的命令钩子,必须你先「审一眼、再信任」,否则它一直被跳过、不会运行。
官方把规则定得很清楚:
- Codex按钩子定义的当前哈希记录信任。所以新加的、或改动过的钩子都会被标成「待审」,在你信任之前一律跳过。
- 改一个字符,哈希变了,就得重新信任。
- 用
/hooks这个命令来查看钩子来源、审阅新增/变动的钩子、信任它们、或禁用某个非托管钩子。 - 如果启动时有钩子待审,Codex会打印一条警告,提示你去开
/hooks。
所以第一次配完钩子的标准动作是:配好 → 启动Codex(可能看到「有钩子待审」的警告)→ 敲/hooks → 找到你那条、确认命令没问题 → 信任它。之后它才会真正触发。我第一次配格式化钩子时,就是没注意这茬,改完文件死活没格式化,还以为matcher写错了,查了半天才反应过来——根本没信任,它压根没跑。
/hooks的预期行为:弹出钩子浏览器,列出所有来源的钩子,标明哪些是「待审(review)」、哪些已信任、哪些是「托管(managed)」。托管钩子(来自系统、MDM、企业requirements.toml等)是「按策略信任」的,你在这个界面里禁不掉——那是管理员焊死的。
还有个一次性的出口:如果你在CI这种「钩子来源本来就在Codex之外审过了」的场景,可以用--dangerously-bypass-hook-trust启动,跳过这次的信任校验。名字里那个dangerously就是提醒你——别在日常本机随手用,它等于把这道安全关卡关了。
06 钩子和Codex怎么对话:stdin、退出码、stdout
这一节是看懂一切的钥匙。前面的脚本怎么拿到命令内容?钩子怎么「拦」住一条命令?答案全在这套「对话机制」里,和第16篇讲的输入输出是一脉相承的。
机制本身极简单,三条管道:Codex把事件数据从stdin喂给你的脚本 → 脚本干活 → 脚本用「退出码 + stdout」告诉Codex接下来怎么办。
输入:Codex从stdin递给你一坨JSON
事件一触发,Codex会把这个事件的相关数据作为一段JSON从标准输入(stdin)塞给你的命令。每个钩子都会拿到一些共有字段:
{
"session_id": "...",
"cwd": "...",
"hook_event_name": "...",
...
}
工具类事件还会多带tool_name(比如"Bash"、"apply_patch")、tool_input(工具的具体输入,Bash和apply_patch用tool_input.command装命令)。比如Codex要跑Bash命令时,PreToolUse钩子收到的大概长这样:
{
"session_id": "abc123",
"cwd": "/Users/sarah/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/x"
}
}
看到了吧——Codex要干什么、用哪个工具、参数是什么,全在里头。你的脚本从这坨JSON里把tool_input.command抠出来,就能判断这条命令该不该拦。
输出:用「退出码」给Codex下指令
脚本干完活,靠退出码(exit code)给Codex下最基础的指令:
| 退出码 | 含义 | 效果 |
|---|---|---|
| 0 | 没意见,正常走 | 操作继续;没有任何输出也当成功 |
| 2 | 特殊信号 | 阻止 / 续轮等(不同事件含义不同,见下) |
| 其他 | 官方未明确定义 | 以实测为准 |
重点是exit 2,但它在不同事件上含义不一样,这点Codex和Claude Code不太一样,务必对清楚:
- 在
PreToolUse/UserPromptSubmit上:exit 2+ 把理由写到stderr = 拦住这次操作 / 这条提示。 - 在
PostToolUse/Stop/SubagentStop上:exit 2+ stderr不是「拦」(工具早跑完了),而是把那段理由作为反馈塞回去——PostToolUse用它替换工具结果让模型重看,Stop用它让Codex再续一轮(详细行为见第03节)。
换句话说,「拦得住」的只有Pre类事件;Post/Stop收到exit 2是「退回 / 续轮」,撤不回已经发生的事。
进阶:用stdout返回JSON,做更细的控制
退出码只能表达粗信号。想要更细的控制(拦的同时说明原因、往上下文注入信息),就exit 0然后往stdout打印一段JSON。举两个最常用的:
① PreToolUse想拦截、并说明理由——返回permissionDecision: "deny":
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "这条命令会动生产库,已拦截"
}
}
② SessionStart想往上下文里塞点信息——返回additionalContext,那段文字会作为「额外的开发者上下文」喂给模型:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "编辑前先读一遍本仓库的代码规范。"
}
}
这里有几个Codex的输出约定坑得提醒:
PreToolUse往stdout打纯文本会被忽略——想注入上下文得用JSON里的additionalContext,想拦得用permissionDecision或exit 2,光echo一句没用。SessionStart、UserPromptSubmit的stdout纯文本会被当成上下文喂进去(这俩特殊);但Stop、SubagentStop必须输出JSON,纯文本对它们是非法的。PreToolUse不支持continue/stopReason这些字段,你硬返回,Codex会把这次钩子标记为失败、报错、然后照样继续那次工具调用——所以别在PreToolUse里指望用continue: false来阻止。
07 两个能直接抄走的真实例子
理论够了,上两个最常用、你也能直接抄的钩子。每个都标清楚「挂哪个事件、收窄到哪、配在哪个文件」。
例子一:改完文件自动格式化(PostToolUse)
就是治好开头那「漏敲格式化」毛病的那个,最实用、零风险,强烈建议每个项目都配上。
第一步,把格式化逻辑写进脚本.codex/hooks/format.py(从stdin读JSON、抠出改的文件路径、丢给格式化工具)。这里给个最小骨架:
#!/usr/bin/env python3
import json, subprocess, sys
data = json.load(sys.stdin)
# apply_patch 的 tool_input.command 里是补丁内容;实际项目里
# 通常直接对整个工作区跑格式化,简单稳妥:
subprocess.run(["ruff", "format", "."])
第二步,在项目根的.codex/hooks.json里注册它,挂到apply_patch的PostToolUse上(matcher用Edit|Write或apply_patch都行,它们是别名):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/format.py\"",
"timeout": 30,
"statusMessage": "格式化改动文件"
}
]
}
]
}
}
第三步(关键,别漏),启动Codex后敲/hooks信任这个钩子——没这一步它不跑(第05节讲过)。从此Codex每改完文件,格式化自动跑。把ruff format换成prettier --write .、gofmt -w .、black .都是一个套路。
例子二:拦掉危险命令(PreToolUse + 脚本)
这个用上了「exit 2拦截」。命令复杂时,把逻辑写进单独脚本比硬塞进JSON清爽得多。
第一步,把脚本存到.codex/hooks/block-dangerous.py:
#!/usr/bin/env python3
import json, sys
data = json.load(sys.stdin)
command = data.get("tool_input", {}).get("command", "")
if "rm -rf" in command:
print("Blocked: 检测到 rm -rf,已拦截", file=sys.stderr)
# 写 stderr,反馈给 Codex
sys.exit(2) # exit 2 = 拦住这次调用
sys.exit(0) # 其余命令放行,走正常审批流程
第二步,在.codex/hooks.json里注册,挂到Bash的PreToolUse上:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/block-dangerous.py\"",
"statusMessage": "检查 Bash 命令"
}
]
}
]
}
}
第三步,/hooks信任它。之后Codex一旦想跑带rm -rf的Bash命令,这个钩子就exit 2把它拦下,并把理由反馈给模型。
这两个例子对比着记:
| 维度 | 例子一:自动格式化 | 例子二:拦危险命令 |
|---|---|---|
| 挂的事件 | PostToolUse(事后) | PreToolUse(事前) |
| matcher | Edit|Write(= apply_patch) | Bash |
| 核心机制 | 跑完就格式化,不阻止 | exit 2 拦住 |
| 风险 | 零风险,人人可配 | 改错可能误拦正常命令 |
| 也能用规则做吗 | 不能(规则只管命令准入) | 能,简单禁令优先用规则 |
08 动手:5分钟配一个钩子并亲眼看它触发
光看不练记不住。下面带你配一个最安全、最容易看到效果的钩子——每次Codex跑完Bash命令,就把这条命令记进一个日志文件。全程不动你任何代码,纯加一段配置,跑完能亲眼验证。
第一步:找个练手目录,建钩子脚本和配置
随便找个空目录(别在重要项目里练),在里头建两个文件。
脚本.codex/hooks/log-bash.py:
#!/usr/bin/env python3
import json, sys, os
data = json.load(sys.stdin)
command = data.get("tool_input", {}).get("command", "")
log = os.path.expanduser("~/codex-bash-log.txt")
with open(log, "a") as f:
f.write(command + "\n")
配置.codex/hooks.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/log-bash.py\"",
"statusMessage": "记录 Bash 命令"
}
]
}
]
}
}
第二步:进这个目录启动Codex,信任钩子
codex
进去后敲/hooks:
/hooks
预期:弹出钩子浏览器,你会看到刚配的PostToolUse钩子,大概率标着「待审 / review」。选中它、确认命令无误后信任它(第05节讲的关键一步)。看到它从「待审」变成已信任 = 这钩子现在会真正触发了。
第三步:让Codex跑条Bash命令,触发钩子
回到对话,让它跑个无害的命令:
帮我用 ls 看一下当前目录有哪些文件
它会调用Bash工具跑ls。这一跑,PostToolUse钩子就该触发了。(钩子成功执行时通常是「静默」的,终端不会特意提示——这是正常的。)
第四步:验证钩子真跑了——查日志文件
新开一个终端,看日志:
cat ~/codex-bash-log.txt
预期:文件里出现了刚才那条ls命令(以及这个会话里Codex跑过的其他Bash命令)。看到命令被记下来了 = 钩子真的在每次Bash后自动触发了,你这条「自动化规则」立起来了。
第五步:清理(可选)
练完拆掉很简单——把.codex/hooks.json里那段删掉(钩子没有单独的「删除命令」,从配置移除即可),日志文件rm ~/codex-bash-log.txt删掉。
跑通这五步,你就把「写配置 → /hooks信任 → 触发事件 → 验证副作用」这条完整链路亲手走了一遍。以后配任何钩子,本质都是这套流程,无非换个事件、换个matcher、换个脚本——但「/hooks信任」这一步,Codex这边千万别漏。
09 钩子不触发 / 报错了,怎么查
钩子配好了不灵,是新手最常见的卡点。别瞎猜,按下面这个顺序查,基本都能定位。我把它整理成「症状 → 怎么查」:
① 是不是没在 /hooks 里信任?这是 Codex 头号坑(第 05 节);
② 改过钩子后哈希变了、又得重新信任;
③ 事件 / matcher 选错了
两个最实用的排查手段单独拎出来:
① 手动喂假数据测脚本。不用真在Codex里触发,自己造一段JSON管道喂给脚本,看它退出码对不对:
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/x"}}' | python3 .codex/hooks/block-dangerous.py
echo $? # 看退出码:拦截脚本这里应该输出 2
这是调拦截钩子的标准动作——先把脚本单独喂熟了,再挂到Codex上,省得在真实会话里反复试。
② 用codex execpolicy check验规则(规则那边的排查利器,第02节讲过)。规则没按预期放行 / 拦截时,拿命令试一下它到底怎么判,别靠猜。
还有一条值得知道:想临时全关钩子,Codex没有Claude Code那种disableAllHooks开关,而是改config.toml:
[features]
hooks = false
10 小结
这一篇把规则和钩子从「是什么」一路讲到「怎么配、怎么调」——它俩是给Codex装的「闸门」和「扳机」:规则管哪些命令能在沙箱外跑,钩子在生命周期固定点自动替你开火,把AGENTS.md里的请求升级成必然兑现的保证。
把核心串起来回顾:
| 你想干的事 | 用什么 | 关键点 |
|---|---|---|
| 让某条命令永远放行 / 禁掉 | 规则 prefix_rule | allow/prompt/forbidden,最严的赢;改完execpolicy check验 |
| 选对钩子挂载时机 | 认生命周期事件 | Pre能拦、Post补刀、Stop能续轮、SessionStart开场 |
| 写一个钩子 | 配进 hooks.json / config.toml | 三层:事件 + matcher(正则)+ 动作;timeout单位秒 |
| 让新钩子真的跑起来 | /hooks 信任 | Codex独有:非托管钩子默认不跑,改一字符要重审 |
| 让钩子和Codex对话 | stdin / 退出码 / stdout | exit 2在Pre是拦、在Post/Stop是退回/续轮 |
| 钩子不灵了 | 按顺序排查 | 先查信任、再查 matcher / 事件 / 路径 |
你现在应该能:
- 说清规则和钩子各管什么、跟「写进AGENTS.md的请求」差在哪个根本点(保证 vs 请求)
- 写一条
prefix_rule给命令立规矩并用execpolicy check验 - 知道
PreToolUse/PostToolUse/Stop/SessionStart各挂在Codex干活流程的哪一步 - 照模板往
hooks.json写一个带matcher的钩子,并记得去/hooks信任它 - 看懂钩子靠stdin / 退出码 / stdout跟Codex对话,且分得清Codex和Claude Code那几个反直觉差异(
timeout秒、apply_patch、block是续轮、信任机制)
开头那「每周漏敲十几次格式化」的苦差,到这儿你已经有能力用一个钩子永久解决了。
下一篇〔25 Worktrees 并行隔离〕——这一路你都是「一个Codex干一件事」。但真到了开发节奏快起来的时候,你会想:能不能让一个Codex修bug、另一个同时做新功能,俩互不踩脚?直接开两个会话改同一个仓库,分分钟把彼此的改动覆盖掉。下一篇聊Git worktree这个老牌但极好用的招——给每条任务一个独立工作区,让多个Codex真正并行、各扫门前雪。想想看:你现在要同时推进两件事,是不是只能干完一件再开下一件?
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
大模型API连续对话交互:上下文持久化与Token节流实践
一、引言 现在的大模型应用落地上,光靠单次独立问答已经远远不够用了。无论是办公协同的智能体、行业咨询机器人、专属业务问答系统,还是私有化部署的大模型,都得能支撑连续多轮对话、跨会话二次访问、长周期上下文关联问答这些核心能力。 在实际对话中,大家都会碰到一些共性问题:第一轮提问回复正常,第二轮就完全没
代驾系统搭建方案:订单调度与司机匹配机制
在城市夜生活日益丰富的当下,代驾早已超越“酒后找人代开”的单一场景,逐步演变为高频、即时、强时效的本地生活服务。无论是商务应酬后的返程,还是临时需要安全送车回家,用户最核心的诉求始终围绕三点:能否快速响应、司机是否靠谱、整个流程是否稳定。对于系统开发者而言,代驾平台搭建的难点恰恰也在于此——它并非简
独立开发者上云避坑:阿里云OPC节省两周配置时间
独立项目上云,说起来简单,做起来全是坑。一位拥有5年后端经验的开发者,三个月前启动了自己的“一人公司”——一个AI辅助写作SaaS,技术栈是Python Flask PostgreSQL React。本以为从本地迁移到云端就是几步操作的事,结果踩了一个又一个坑,硬生生折腾了好几周。以下是他踩坑后的复
阿里云DNS个人版19.9元/年 公网权威解析功能安全与续费说明
许多用户最近都在咨询阿里云云解析DNS个人版的价格与续费问题,这里统一整理关键信息。个人版当前新用户优惠价仅为19 9元 年,但该优惠仅限首次购买,原价实际为48元 年。因此续费时若无额外折扣,将按48元 年计费。简单来说,首年19 9元即可入手,第二年恢复原价。0 云解析DNS个人版费用阿里云云
阿里云Qwen3.7-Max深度测评:极致推理与企业级部署落地指南
Qwen3 7-Max是阿里云百炼平台最新推出的旗舰级大模型,也是Qwen3 7系列中规模最大、综合能力最强的“顶配选手”。目前开放的是纯文本模型能力,但别被这个限制误导——它面向智能体时代设计,在编程、办公生产力、长周期自主执行等场景下,表现相当能打。推理能力、多模态理解、复杂任务处理都有显著升级
- 日榜
- 周榜
- 月榜
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10
相关攻略
2026-07-07 15:11
2026-07-07 15:11
2026-07-07 15:11
2026-07-07 15:10
2026-07-07 15:10
2026-07-07 15:10
2026-07-07 15:10
2026-07-07 15:08
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程
热门话题

