AGENTS.md 和 Skill 是写给模型看的规章,hook 则是运行时的闸机:模型准备调用工具时,先由 hook 判断是否允许。它不是提醒,而是可以直接拒绝调用的执行机制。最近 Codex hooks 支持异步执行和直接调用 MCP 工具,但权限边界也需要一起理解。

Hook 和规则文件的差别
Skill 与 AGENTS.md 进入上下文,模型读到后通常会遵守,但没有机制保证。Hook 不进入上下文,而是挂在运行时事件上。模型请求执行 rm -rf build 时,事件 JSON 会交给脚本;脚本拒绝,工具调用就不会发生。
事件分布
启动阶段有 SessionStart 和 SubagentStart,脚本标准输出会作为上下文注入。结束阶段是 SessionEnd,默认超时只有 1 秒、最长 3 秒,不适合执行重任务。工作阶段包括 PreToolUse、PermissionRequest、PostToolUse、UserPromptSubmit、PreCompact、PostCompact、Stop 和 SubagentStop。
最小配置示例
Hook 可以放在 ~/.codex/hooks.json、~/.codex/config.toml、仓库的 .codex/hooks.json 或 .codex/config.toml,插件也可以携带 hook。多层配置会一起加载,同层 JSON 和 TOML 会合并,启动时可能给出警告。
[[hooks.PreToolUse]]
matcher = "^Bash$"
[[hooks.PreToolUse.hooks]]
type = "command"
command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py"'
timeout = 30
statusMessage = "Checking Bash command"
脚本从标准输入取得 JSON,字段包括 session_id、transcript_path、cwd、hook_event_name 和 model,部分事件还带 turn_id 与 permission_mode。退出码为 0 且没有输出时表示放行。真正执行的 handler 类型是 command 和 mcp_tool;prompt、agent 当前会被解析后跳过。
不同事件的拦截方式
PreToolUse返回permissionDecision: "deny",或退出码 2 并写明原因,可以阻止工具调用;也可以用updatedInput改写调用参数。PermissionRequest返回 allow 会跳过人工确认,返回 deny 会直接拒绝;多个 hook 中只要一个拒绝,结果就是拒绝。PostToolUse发生在副作用之后,不能撤回操作,但可以替换工具结果,提醒模型继续处理。Stop和SubagentStop返回decision: "block"会让任务继续,并把理由变成新的提示;想真正停止要使用continue: false。UserPromptSubmit可在输入进入模型前拦截,PreCompact和PostCompact可控制上下文压缩。
continue、stopReason、suppressOutput 在 PreToolUse 和 PermissionRequest 上没有实际拦截作用,permissionDecision: "ask" 也尚未实现。

异步 Hook 和 MCP Hook
给 command handler 加 "async": true,输出不会阻塞当前轮,而会在下一个安全点送入。它适合静态检查等不影响当前决策的慢任务:
{
"type": "command",
"command": "python3 ~/.codex/hooks/post_tool_use.py",
"async": true,
"timeout": 120
}
一个会话最多同时运行 8 个异步 hook,完成顺序不保证;会话结束时未完成的任务会被取消。异步 hook 不能 block、批准或改写调用,需要拦截时必须使用同步 hook,SessionEnd 始终同步执行。
MCP hook 可以直接调用已连接的 MCP server:
{
"type": "mcp_tool",
"server": "scanner",
"tool": "scan_patch",
"input": { "patch": "${tool_input.command}" },
"timeout": 30,
"statusMessage": "Scanning edited files"
}
占位符从事件负载取值,独立占位符保留 JSON 类型,嵌在字符串中则变成文本。这类 hook 不会自动启动 MCP server,也不会触发审批或连锁触发其他 hook。

信任、沙箱与安全边界
非受管 hook 首次运行前需要人工信任,信任绑定在定义哈希上;脚本改一个字,哈希变化后就要重新审查。/hooks 可以查看来源、状态并单独禁用 hook。安装插件也不等于自动信任其 hook。
--dangerously-bypass-hook-trust 只临时生效,风险应由名字本身提醒。企业可以使用 allow_managed_hooks_only,只运行受管 hook;完全关闭则在配置中设置 [features] hooks = false。
Hook 是护栏,不是完整安全边界。WebSearch 等托管工具不触发 hook,write_stdin 也不会重新触发 PreToolUse。真正的越权防护仍要依靠沙箱与权限策略。输出默认限制在约 2500 token,超出部分会落到临时文件,脚本不要把密钥写入 stdout。

结论
Hooks 的价值在于把高频、明确的动作变成运行时约束:例如禁止改生产配置、提交前必须过 lint、触碰 migrations 目录先请求确认。日常使用可以先写一个 PreToolUse hook 管住 Bash,再逐步加入异步检查和 MCP 扫描。它能减少依赖模型自觉,但不能替代沙箱、权限和人工判断。