Kipt

Codex hooks: run scripts at every step of the Codex loop

How Codex hooks work: hooks.json and config.toml locations, every lifecycle event, matchers, blocking tool calls, trust review with /hooks and background hooks.

By the Kipt team · Updated 24 October 2026

Codex hooks are commands or MCP tool calls that Codex runs at fixed points in its agent loop, such as before a shell command, after a file edit, or when a turn ends. You define them in hooks.json or in [hooks] tables in config.toml, and Codex runs every matching hook after you review and trust it.

This guide covers where hooks live, the events you can hook into, how to block or rewrite a tool call, and the limits to know about. Everything here comes from OpenAI's Codex hooks documentation, read on 24 October 2026.

What can you do with Codex hooks?

OpenAI's docs list these example uses:

  • Send the chat to your own logging or analytics system.
  • Scan prompts so nobody pastes an API key by accident.
  • Summarize chats into persistent memories.
  • Run a validation check when a turn stops, so standards are enforced.
  • Change prompting when Codex works in a certain directory.

In practice, hooks are where you put rules that should run every time, not only when the model remembers them. An instruction in AGENTS.md asks Codex to run the linter; a Stop hook can make the turn continue until it does.

Where does Codex look for hooks?

Codex discovers hooks next to its active config layers, in two forms: a hooks.json file, or inline [hooks] tables inside config.toml. The four locations you will use most:

Location Scope
~/.codex/hooks.json You, in every project
~/.codex/config.toml You, in every project
<repo>/.codex/hooks.json Everyone working in this repo
<repo>/.codex/config.toml Everyone working in this repo

A few loading rules matter:

  • Hooks add up. If several sources define hooks, Codex loads all matching ones. A higher-precedence layer does not replace hooks from a lower one.
  • One format per layer. If a layer has both hooks.json and inline [hooks], Codex merges them and warns at startup.
  • Projects must be trusted. Project-local hooks load only when the project's .codex/ layer is trusted. In an untrusted project, your user and system hooks still load.
  • Plugins can bring hooks. An enabled plugin can bundle hooks in hooks/hooks.json or point to them from .codex-plugin/plugin.json. See the Codex plugins guide.

The Codex config guide explains how user and project layers combine.

Which events can Codex hooks run on?

When Events
During a turn PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, UserPromptSubmit, SubagentStop, Stop
When you interrupt an active turn Interrupt (not for subagents)
When a session or subagent starts SessionStart, SubagentStart
When the main thread ends SessionEnd (not for subagents)

What each one is good for:

  • SessionStart adds context when a session starts, resumes, clears, compacts or forks. Plain text on stdout becomes extra developer context.
  • UserPromptSubmit sees each prompt before it is sent and can block it, which is how you catch pasted secrets.
  • PreToolUse runs before shell commands, apply_patch file edits, MCP tool calls and most other local tools. It can deny or rewrite the call.
  • PermissionRequest runs only when Codex is about to ask you for approval. It can allow, deny, or let the normal prompt appear.
  • PostToolUse runs after a tool returns, including Bash commands that fail. It cannot undo the action, but it can replace the result with feedback for the model.
  • PreCompact and PostCompact run around context compaction.
  • SubagentStart and SubagentStop run for Codex subagents.
  • Stop runs when a turn is about to end and can make Codex keep going.
  • Interrupt and SessionEnd are for logging and cleanup. They cannot change what happens.

What does a hooks.json file look like?

Hooks have three levels: an event, a matcher group that decides when the event matches, and one or more handlers. This .codex/hooks.json runs a policy check before every shell command:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 \"$(git rev-parse --show-toplevel)/.codex/hooks/pre_tool_use_policy.py\"",
            "statusMessage": "Checking Bash command",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

The same hook as inline TOML in config.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"

Handler fields worth knowing:

  • type: command or mcp_tool. The prompt and agent types are parsed but skipped today.
  • timeout: in seconds. The default is 600 for most events. SessionEnd and Interrupt default to 1 second and allow at most 3.
  • statusMessage: text shown while the hook runs.
  • async: set true to run a command hook in the background.
  • commandWindows: a Windows-only override of command.

Commands run with the session's working directory. Because Codex may start in a subfolder, the docs recommend resolving repo hooks from the git root, as above, instead of a relative path like .codex/hooks/....

How do matchers work?

matcher is a regex. Use "*", "" or leave it out to match everything. What it filters depends on the event:

Event Matcher filters
PreToolUse, PostToolUse, PermissionRequest Tool name, such as Bash, apply_patch or mcp__filesystem__read_file
SessionStart startup, resume, clear, compact or fork
PreCompact, PostCompact manual or auto
SubagentStart, SubagentStop The subagent type
UserPromptSubmit, Stop, Interrupt Not supported, any matcher is ignored

For file edits, apply_patch also matches Edit and Write, so Edit|Write catches every patch. MCP tools use the mcp__<server>__<tool> form, and mcp__filesystem__.* matches a whole server. Hosted tools such as web search do not go through the local hook path, so hooks cannot see them.

How do you block or rewrite a tool call?

Every command hook gets one JSON object on stdin. Shared fields include session_id, cwd, hook_event_name, model and transcript_path, and tool events add tool_name and tool_input. For Bash, the command is in tool_input.command.

Here is an example policy script for the hook above, written for this guide rather than taken from OpenAI's docs. It denies any command containing rm -rf, using the deny shape the docs document:

#!/usr/bin/env python3
import json, sys

event = json.load(sys.stdin)
command = event.get("tool_input", {}).get("command", "")

if "rm -rf" in command:
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": "Destructive command blocked by hook."
        }
    }))
# exit 0 with no output lets the call continue

Other responses PreToolUse supports:

  • Exit code 2 with the reason on stderr also blocks the call.
  • Add context without blocking by returning hookSpecificOutput.additionalContext.
  • Rewrite the call by returning permissionDecision: "allow" with updatedInput. For Bash and apply_patch, updatedInput must include a string command.

permissionDecision: "ask" is parsed but not supported yet: Codex marks the hook as failed and lets the tool call run. For PermissionRequest, return decision.behavior set to allow or deny; if several hooks answer, any deny wins.

To keep Codex working, a Stop hook returns {"decision": "block", "reason": "Run one more pass over the failing tests."}. Codex continues the turn and uses the reason as a new prompt. The input includes stop_hook_active, so your script can avoid sending Codex round in circles. Unlike SessionStart, Stop expects JSON on stdout when it exits 0; plain text is invalid.

How do you trust, review and turn off hooks?

Before a non-managed hook runs, Codex asks you to review and trust its exact definition. Trust is stored against a hash of the hook, so a new or edited hook is skipped until you approve it again. Type /hooks in the Codex CLI to see hook sources, review changes, trust hooks or disable individual ones. If hooks need review, Codex prints a startup warning pointing you to /hooks.

For scripted runs that already vet hook sources, --dangerously-bypass-hook-trust runs enabled hooks without stored trust for that one invocation.

To turn hooks off entirely:

[features]
hooks = false

Admins can force the same setting in requirements.toml, define managed hooks that run scripts from a managed_dir (their own tooling installs the scripts), and set allow_managed_hooks_only = true to ignore user, project, session and plugin hooks. Managed hooks are trusted by policy and cannot be disabled from /hooks.

What are the limits of Codex hooks?

  • Not a security boundary. The docs say to "Treat tool hooks as a useful guardrail, not a complete enforcement boundary." Some tool paths skip hooks, and hosted tools are not covered. Pair hooks with the sandbox and approvals.
  • Concurrent, not chained. Matching command hooks for the same event start at the same time, so one hook cannot stop another from starting.
  • Background hooks can't decide. An async hook cannot block, approve or rewrite anything. Codex runs up to eight at once per session (more wait their turn) and cancels unfinished ones when the session ends.
  • Output is capped. Model-visible hook output is limited to roughly 2,500 tokens by default. Longer output is saved to a temp file and the model gets a preview. Raise it per handler with additionalContextLimit, and keep secrets out of hook output.
  • MCP tool hooks use existing connections. A mcp_tool handler calls a tool on an MCP server that is already connected; it never starts one. Errors and missing servers do not block the operation.

For the exact wire format, OpenAI publishes generated schemas in the openai/codex repository. If you also use Claude Code or Cursor, compare the Claude Code hooks guide and the Cursor hooks guide. Codex sets CLAUDE_PLUGIN_ROOT and CLAUDE_PLUGIN_DATA for plugin hooks "for compatibility with existing plugin hooks."

Where should the rest of your Codex setup live?

Hooks enforce rules; skills hold the know-how Codex follows. Kipt keeps your prompts and Agent Skills as Markdown in a private GitHub repo you own and installs every skill into Codex or Claude Code with npx skills add https://github.com/<you>/<repo>.git --all -g. The free SKILL.md generator turns a prompt you already use into a skill.

Questions

Where do I put Codex hooks?

Put them in a hooks.json file or in inline [hooks] tables in config.toml, next to an active config layer. The four common spots are ~/.codex/hooks.json, ~/.codex/config.toml, <repo>/.codex/hooks.json and <repo>/.codex/config.toml. Project hooks load only when the project's .codex/ layer is trusted.

Are Codex hooks turned on by default?

Yes. Hooks are enabled by default, but each non-managed hook must be reviewed and trusted before it runs. To turn hooks off, set hooks = false under [features] in config.toml. The older codex_hooks key still works as a deprecated alias.

How do I block a command with a Codex hook?

Add a PreToolUse hook with matcher Bash. Your script reads the tool call as JSON on stdin and returns a hookSpecificOutput object with permissionDecision set to deny and a reason. You can also exit with code 2 and write the reason to stderr.

Why is my Codex hook not running?

A likely reason is that it has not been trusted yet. Codex records trust against a hash of the hook definition, so new or edited hooks are skipped until you approve them in /hooks. Also check that the project is trusted, that your matcher regex fits the tool name, and that hooks are not disabled in [features].

Can a Codex hook keep the agent working after it stops?

Yes. A Stop hook can return decision block with a reason. Codex then continues the turn and uses your reason as a new prompt, which is useful for a check such as making sure the tests pass before the turn ends.

More in Codex

All guides

Keep your prompts and skills in one place, in your own GitHub. Install every skill with one command.

Get started

Kipt is an independent product, not affiliated with or endorsed by Anthropic, OpenAI, Cursor or GitHub. Claude, ChatGPT, Codex, Cursor and GitHub are trademarks of their owners.