Hooks钩子
本教程共 32 篇 · 第 21 篇 · 更新于 2026-07-26 · 约 12 分钟阅读
21. Hooks钩子
本节目标:理解钩子是什么、能挂在哪些事件上、怎么配置、怎么跟 Codex 对话,以及信任机制和实用示例。
钩子是什么
写进 AGENTS.md 的指令是请求—你拜托 Codex 做的事,它大概率照办,但可能漏、可能忘。配成钩子的是保证—只要那个事件触发,动作一定执行,跟 Codex 想不想、记不记得没关系。
打个比方:AGENTS.md 里写「改完记得格式化」,三次里可能漏一次;配成 PostToolUse 钩子,Codex 每改完一个文件格式化自动跑,一次不漏。
钩子能干的事包括:
- 改完文件自动跑格式化 / lint
- 拦截危险命令(
rm -rf、git push --force) - 会话开始时往上下文注入项目状态
- 把聊天发送到自定义日志系统
- 会话结束时自动总结生成持久记忆
钩子挂在哪里:生命周期事件
钩子挂在 Codex 干活流程的特定事件上。Codex 干活是「想 -> 做 -> 看」转圈,这些事件散布在循环的前前后后。
新手先吃透这四个
| 事件 | 什么时候触发 | 最典型用法 |
|---|---|---|
PreToolUse | 工具执行前 | 拦危险命令、改写命令(能阻止操作) |
PostToolUse | 工具产出结果之后 | 改完文件自动格式化、跑 lint |
Stop | Codex 答完这一轮 | 让它「再多跑一趟」(把没过的测试再修一遍) |
SessionStart | 会话开始 / 恢复时 | 往上下文注入项目状态 |
Tip看名字里的
Pre和Post:Pre是「之前」,只有它能在动作发生前拦住;Post是「之后」,工具都跑完了,撤不回已产生的副作用,只能事后补一刀。
完整事件列表
| 阶段 | 事件 |
|---|---|
| 会话轮次期间 | PreToolUse、PermissionRequest、PostToolUse、PreCompact、PostCompact、UserPromptSubmit、SubagentStop、Stop |
| 会话或子代理启动时 | SessionStart、SubagentStart |
| 主对话线程结束时 | SessionEnd(不会为子代理运行) |
钩子配在哪
Codex 在两个地方找钩子配置:独立的 hooks.json 文件,或 config.toml 里内联的 [hooks] 表。最常用的四个位置:
| 配置文件 | 生效范围 | 能共享给团队吗 |
|---|---|---|
~/.codex/hooks.json | 你的所有项目 | 否 |
~/.codex/config.toml | 你的所有项目 | 否 |
<repo>/.codex/hooks.json | 仅当前项目 | 是,可提交进 Git |
<repo>/.codex/config.toml | 仅当前项目 | 是 |
Note多个来源的钩子会全部加载、一起跑。高优先级配置层不会替换低优先级的钩子,是叠加关系。同一层里别同时写
hooks.json和内联[hooks],Codex 会合并但会在启动时警告你。
配置结构
钩子配置就三层:事件 > matcher > 动作。
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"
}
]
}
]
}
}
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"
关键配置项
| 字段 | 说明 |
|---|---|
type | 目前只有 "command" 类型会真正运行 |
command | 要执行的 shell 命令 |
timeout | 超时时间,单位是秒(不是毫秒),默认 600 |
statusMessage | 可选,显示在界面上的状态提示 |
command_windows | 可选,Windows 专用命令覆盖 |
Warning
timeout的单位是秒,不是毫秒。省略不写时默认 600 秒。SessionEnd默认只有 1 秒,最大 3 秒。
matcher:让钩子只在该触发时触发
matcher 是一个正则表达式字符串,用来过滤钩子什么时候触发。没有它,钩子在那个事件的每一次都会触发。
| 你写的 matcher | 含义 |
|---|---|
"Bash" | 匹配 Bash 工具 |
"^apply_patch$" | 精确匹配改文件操作 |
"Edit|Write" | 改文件的别名(正则的「或」) |
"mcp__filesystem__.*" | 匹配一批 MCP 工具 |
"*" / "" / 省略 | 匹配所有 |
不同事件的 matcher 过滤对象不同:
| 事件 | matcher 过滤的对象 |
|---|---|
PreToolUse / PostToolUse | 工具名 |
SessionStart | 启动来源(startup/resume/clear/compact) |
PreCompact / PostCompact | 触发源(manual/auto) |
SubagentStart / SubagentStop | 子代理类型 |
UserPromptSubmit / Stop | 不支持,写了会被忽略 |
Note改文件的工具名是
apply_patch,不是Edit或Write。你在 matcher 里可以用Edit、Write、apply_patch任意一个来匹配它(互为别名),但钩子收到的 stdin 里tool_name始终报的是"apply_patch"。
信任机制:新钩子默认不跑
这是新手第一次配钩子最容易懵的地方:你明明写好了钩子,它却没跑。
原因:Codex 默认不信任非托管的命令型钩子。 钩子是用你完整用户权限跑的 shell 脚本,能删你能删的任何文件、能联网。如果从网上抄一段代码,里头夹带的钩子自动就跑,等于谁都能在你机器上偷偷执行代码。
Codex 的解决方案是「过目签字」:
- Codex 按钩子定义的当前哈希记录信任
- 新加的或改动过的钩子都会被标成「待审」,信任前一律跳过
- 改一个字符,哈希变了,就得重新信任
用 /hooks 命令来查看、审核和信任钩子:
/hooks
Warning第一次配完钩子的标准动作:配好 -> 启动 Codex -> 敲
/hooks-> 找到你那条、确认命令没问题 -> 信任它。之后它才会真正触发。没这一步,钩子压根不跑。
如果启动时有钩子待审,Codex 会打印警告提示你去开 /hooks。
来自 system、MDM、cloud 或 requirements.toml 的托管钩子是「按策略信任」的,你在这个界面里禁不掉。
钩子怎么跟 Codex 对话
钩子和 Codex 靠三条管道通信:stdin 喂 JSON、退出码下指令、stdout 的 JSON 做精细控制。
输入:stdin 收到一坨 JSON
事件触发时,Codex 把事件数据作为 JSON 从标准输入塞给你的脚本。通用字段包括:
| 字段 | 含义 |
|---|---|
session_id | 当前会话 ID |
cwd | 会话的工作目录 |
hook_event_name | 当前事件名 |
transcript_path | 会话记录文件路径(可能为 null) |
model | 当前激活的模型 slug |
permission_mode | 当前权限模式 |
工具类事件还会多带 tool_name 和 tool_input。比如 PreToolUse 收到的:
{
"session_id": "abc123",
"cwd": "/Users/sarah/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/x"
}
}
输出:退出码
| 退出码 | 含义 |
|---|---|
0 | 没意见,正常继续 |
2 | 特殊信号(不同事件含义不同) |
exit 2 在不同事件上含义不同:
PreToolUse/UserPromptSubmit:把理由写到 stderr = 拦住这次操作PostToolUse/Stop/SubagentStop:不是「拦」(工具早跑完了),而是把理由作为反馈塞回去
Note「拦得住」的只有
Pre类事件。Post/Stop收到exit 2是「退回 / 续轮」,撤不回已经发生的事。
输出:stdout 返回 JSON
想要更细的控制,exit 0 然后往 stdout 打印 JSON。
PreToolUse 拦截并说明理由:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "这条命令会动生产库,已拦截"
}
}
SessionStart 往上下文注入信息:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "编辑前先读一遍本仓库的代码规范。"
}
}
Tip
PreToolUse往 stdout 打纯文本会被忽略。想注入上下文得用 JSON 里的additionalContext,想拦得用permissionDecision或exit 2。Stop和SubagentStop必须输出 JSON,纯文本对它们是非法的。
关闭钩子
Hooks 默认启用。在 config.toml 中关闭:
[features]
hooks = false
实用示例
示例一:改完文件自动格式化
写脚本 .codex/hooks/format.py:
#!/usr/bin/env python3
import json, subprocess, sys
data = json.load(sys.stdin)
subprocess.run(["ruff", "format", "."])
注册到 .codex/hooks.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/format.py\"",
"timeout": 30,
"statusMessage": "格式化改动文件"
}
]
}
]
}
}
配完后记得 /hooks 信任它。把 ruff format 换成 prettier --write .、gofmt -w .、black . 都是一个套路。
示例二:拦掉危险命令
写脚本 .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)
sys.exit(2)
sys.exit(0)
注册到 .codex/hooks.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/block-dangerous.py\"",
"statusMessage": "检查 Bash 命令"
}
]
}
]
}
}
Tip拦危险命令,简单的命令前缀禁令也可以用规则(Rules)来做,一行声明无需写脚本。要按命令内容做复杂判断(比如「只拦删特定目录的
rm」),才上钩子。别两边都配同一条。
钩子不触发怎么排查
| 症状 | 最可能的原因 |
|---|---|
| 钩子压根不触发 | 没在 /hooks 里信任;改过钩子后哈希变了要重新信任;事件/matcher 选错了 |
/hooks 里没有我配的钩子 | JSON 格式错了(不允许尾逗号、不允许注释);文件位置/文件名错了 |
| 报 command not found | 路径不对,用 $(git rev-parse --show-toplevel) 拼绝对路径 |
| 想拦却没拦住 | 挂错事件了(拦操作要用 PreToolUse);没用 exit 2 或 permissionDecision: "deny" |
| 钩子超时被杀 | timeout 单位是秒(默认 600),长任务适当调大 |
手动喂假数据测脚本:
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/x"}}' | python3 .codex/hooks/block-dangerous.py
echo $?
小结
| 你想干啥 | 怎么做 |
|---|---|
| 挂自动格式化 | PostToolUse + matcher: "Edit|Write" + 格式化脚本 |
| 拦危险命令 | PreToolUse + matcher: "Bash" + exit 2 或 permissionDecision: "deny" |
| 会话开始注入信息 | SessionStart + additionalContext |
| 答完再跑一轮 | Stop + decision: "block" + reason |
| 让新钩子真的跑 | /hooks 信任它 |
| 临时全关钩子 | [features] hooks = false |
钩子是给 Codex 装的「扳机」—在生命周期固定点自动开火,把 AGENTS.md 里的请求升级成必然兑现的保证。新手先把 PreToolUse(能拦)、PostToolUse(补刀)、Stop(续轮)、SessionStart(开场)这四个吃透。记住几个关键点:timeout 单位是秒、改文件工具名是 apply_patch、matcher 是正则、新钩子必须在 /hooks 里信任才跑。
下一章讲子代理与多智能体 V2—怎么把任务拆出去并行跑。