钩子要调 MCP,用 type mcp_tool,不要再包一层 shell

server / tool / input 直接打到已连接的 MCP。服务器没起来不会拦操作。prompt 和 agent 类型会解析但跳过。

生命周期钩子可以直接调 MCP,不必再写 npx … 包装。JSON 示例:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "mcp_tool",
            "server": "scanner",
            "tool": "scan_patch",
            "input": { "patch": "${tool_input.command}" },
            "timeout": 30,
            "statusMessage": "Scanning edited files"
          }
        ]
      }
    ]
  }
}

server 必须是已经连上的 MCP 名。钩子不会替你启动或重连。input 里用 ${field.nested} 从事件取值:整段占位保留 JSON 类型,嵌在字符串里就变成文本。

官方约束:

  • 只跑 commandmcp_toolprompt / agent 写了也不会执行
  • mcp_tool 始终同步,不走工具批准,也不会再触发别的钩子
  • 超时取钩子 timeout 和服务器 tool_timeout_sec 里较短的那个
  • SessionEnd 不支持 mcp_toolSessionStart 可能在 MCP 就绪前就跑,那时不会拦会话
  • 工具返回 block 才会拦住操作。缺服务器、连不上、工具不存在、执行报错都不拦

不要把 mcp_tool 的 Stop 当成合规完成门。服务器没配或起不来时,codex exec --json 仍可能直接 turn.completed。讨论中的 failureMode 还没落地,不要抄进配置。真正的门继续用能 exit 2 / 返回 deny 的 command 钩子,并自己确认 MCP 已在 /mcp 里。

codex mcp add 再信任钩子。新开会话后 /hooks 应能看到 server 和 tool 字段。

来源