首页 / Codex 教程 / Hooks钩子

Codex 教程

Hooks钩子

本教程共 32 篇 · 第 21 篇 · 更新于 2026-07-26 · 约 12 分钟阅读

CodexCodex 教程Hooks钩子PreToolUsePostToolUse信任机制自动化

21. Hooks钩子

本节目标:理解钩子是什么、能挂在哪些事件上、怎么配置、怎么跟 Codex 对话,以及信任机制和实用示例。

钩子是什么

写进 AGENTS.md 的指令是请求—你拜托 Codex 做的事,它大概率照办,但可能漏、可能忘。配成钩子的是保证—只要那个事件触发,动作一定执行,跟 Codex 想不想、记不记得没关系。

打个比方:AGENTS.md 里写「改完记得格式化」,三次里可能漏一次;配成 PostToolUse 钩子,Codex 每改完一个文件格式化自动跑,一次不漏。

钩子能干的事包括:

  • 改完文件自动跑格式化 / lint
  • 拦截危险命令(rm -rfgit push --force
  • 会话开始时往上下文注入项目状态
  • 把聊天发送到自定义日志系统
  • 会话结束时自动总结生成持久记忆

钩子挂在哪里:生命周期事件

钩子挂在 Codex 干活流程的特定事件上。Codex 干活是「想 -> 做 -> 看」转圈,这些事件散布在循环的前前后后。

新手先吃透这四个

事件什么时候触发最典型用法
PreToolUse工具执行前拦危险命令、改写命令(能阻止操作
PostToolUse工具产出结果之后改完文件自动格式化、跑 lint
StopCodex 答完这一轮让它「再多跑一趟」(把没过的测试再修一遍)
SessionStart会话开始 / 恢复时往上下文注入项目状态
Tip

看名字里的 PrePostPre 是「之前」,只有它能在动作发生前拦住;Post 是「之后」,工具都跑完了,撤不回已产生的副作用,只能事后补一刀。

完整事件列表

阶段事件
会话轮次期间PreToolUsePermissionRequestPostToolUsePreCompactPostCompactUserPromptSubmitSubagentStopStop
会话或子代理启动时SessionStartSubagentStart
主对话线程结束时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,不是 EditWrite。你在 matcher 里可以用 EditWriteapply_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_nametool_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,想拦得用 permissionDecisionexit 2StopSubagentStop 必须输出 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 2permissionDecision: "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 2permissionDecision: "deny"
会话开始注入信息SessionStart + additionalContext
答完再跑一轮Stop + decision: "block" + reason
让新钩子真的跑/hooks 信任它
临时全关钩子[features] hooks = false

钩子是给 Codex 装的「扳机」—在生命周期固定点自动开火,把 AGENTS.md 里的请求升级成必然兑现的保证。新手先把 PreToolUse(能拦)、PostToolUse(补刀)、Stop(续轮)、SessionStart(开场)这四个吃透。记住几个关键点:timeout 单位是秒、改文件工具名是 apply_patch、matcher 是正则、新钩子必须在 /hooks 里信任才跑。

下一章讲子代理与多智能体 V2—怎么把任务拆出去并行跑。