TellWorldAI World Archive
Ask中文

Home›Practice›Practice

Practice

Codex hooks: running scripts and MCP tools in the agent loop

Codex hooks run your scripts or MCP tools at fixed points in the agent loop. Learn where hooks live, how trust works, and how to block a tool call.

Who this is for

This is for readers who want to add their own rules to Codex: blocking risky Bash commands, scanning prompts for pasted API keys, logging sessions, or running a check when a turn stops. By the end you can configure a PreToolUse hook, trust it, and know when it runs.

What hooks are

Hooks are Codex's extension mechanism. They let you run scripts or MCP tools during the agent loop.[1] The docs give examples such as sending the chat to a custom logging or analytics engine, scanning prompts to block accidentally pasted API keys, summarizing chats into persistent memories, and running a validation check when a turn stops.[1]

Hooks come from every matching source, and all of them run. When several matching command hooks share an event, they start concurrently, so one hook cannot stop another from starting.[1]

When hooks run

Hook events fire at different points in a session:[1]

  • During a turn: PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, UserPromptSubmit, SubagentStop and Stop.
  • For each model request attempt: PreModelRequest and PostModelRequest.
  • When you interrupt an active turn: Interrupt.
  • When a session or subagent starts: SessionStart and SubagentStart.
  • When the main thread ends: SessionEnd.

Where to put them

Codex finds hooks next to its active config layers, either as a hooks.json file or as inline [hooks] tables in config.toml.[1] The four most useful places are ~/.codex/hooks.json, ~/.codex/config.toml, <repo>/.codex/hooks.json and <repo>/.codex/config.toml.[1] If one layer has both forms, Codex merges them and warns at startup, so keep one form per layer.[1]

A hook has three levels: an event, a matcher group that decides when the event matches, and one or more handlers.[1] Only command and mcp_tool handlers run. prompt and agent handlers are parsed but skipped.[1] Here is an inline config.toml version with a SessionStart hook and a PreToolUse hook that checks Bash commands:

[[hooks.SessionStart]]
matcher = "^compact$"

[[hooks.SessionStart.hooks]]
type = "command"
command = '/usr/bin/python3 "$(git rev-parse --show-toplevel)/.codex/hooks/session_start.py"'
additionalContextLimit = 5000

[[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"

For a repository-local hook, resolve the script path from the git root instead of a relative path such as .codex/hooks/..., because Codex may be started from a subdirectory.[1] Commands run with the session's working directory as cwd.[1] A timeout is in seconds. Most hooks default to 600 seconds, while SessionEnd and Interrupt default to 1 second and can go up to 3 seconds.[1]

Trust comes first

Non-managed hooks do not run until you trust them. Codex stores trust against each hook's hash, so a new or edited hook is marked for review and skipped until you approve it.[1] Run /hooks in the CLI to see hook sources, review changes, trust hooks or disable individual ones.[1] The --dangerously-bypass-hook-trust flag skips the persisted trust check for a single run. The docs reserve it for one-off automation that already vets hook sources outside Codex.[1]

Turn hooks off

Hooks are on by default. To disable them, add this to config.toml:

[features]
hooks = false

The key is hooks. codex_hooks still works, but it is a deprecated alias.[1]

Blocking a tool call

PreToolUse can intercept Bash commands, file edits made through apply_patch, MCP tool calls and other local function tools. Hosted tools such as WebSearch are not covered.[1] To deny a call, print JSON like this:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Destructive command blocked by hook."
  }
}

Exit code 2 with the reason on stderr also blocks the call.[1] A hook can instead rewrite a call by returning permissionDecision: "allow" with updatedInput. For Bash and apply_patch, that object must include a string command field.[1] The docs call tool hooks a useful guardrail, not a complete enforcement boundary.[1]

PermissionRequest runs when Codex is about to ask for approval, such as a shell escalation. It can allow or deny the request. If several hooks decide, any deny wins.[1]

Keeping a turn going and running in the background

A Stop hook can keep Codex working. Returning decision: "block" with a reason makes Codex continue, using that reason as a new user prompt, rather than rejecting the turn.[1] Set "async": true on a command handler to run it in the background. Up to eight background hooks run at once per session, but they cannot block or rewrite the action that triggered them, so use synchronous hooks for policy decisions.[1]

Output size

By default Codex limits each model-visible hook output to about 2,500 tokens. Larger output is saved to a file under <temp_dir>/hook_outputs/ and replaced with a preview.[1]

Matchers and managed hooks

matcher is a regex. "*", an empty string, or no matcher matches every occurrence of a supported event. UserPromptSubmit and Stop ignore it.[1] In enterprise setups, allow_managed_hooks_only = true in requirements.toml skips hooks from user, project, session and plugin sources, but keeps managed hooks.[1]

Next steps

Start with one PreToolUse hook for Bash, confirm it with /hooks, then add more events. The full event schemas are in the official Hooks page.[1]

Sources

  1. 1
    HooksOpenAI Codex docs

Cite this page: Codex hooks: running scripts and MCP tools in the agent loop. TellWorld, https://www.tellworld.net/practice/practice-codex-hooks. Updated 2026-11-14

Related news

View all