On this pageBefore you start

Codex Hooks: How to Set Them Up and Use Them

Xtrusio9 min read

Short answer

Short answer: Codex hooks are scripts, or MCP tool calls, that Codex runs at set points in a session, such as before a tool runs or when a turn stops. They are on by default. Add a hooks.json file at ~/.codex/ or <repo>/.codex/, or use inline [hooks] tables in config.toml. Name an event, an optional matcher and a command. Then open /hooks in the Codex CLI to review and trust the hook, because Codex skips new or changed hooks until you do. Source: OpenAI's hooks page, read 4 November 2026.

A developer at a code display beside the headline Codex Hooks

A developer wants Codex to refuse a destructive shell command. A rule in a prompt is not enough, because the request needs a check at the moment a shell tool runs. A hook can inspect that call and deny it before the command starts.

Before you start

You need the Codex CLI and a Git repository. By the end, you will have a trusted PreToolUse hook that checks every Bash call and can deny a command.

What Codex hooks are

Hooks extend Codex with scripts or MCP tools that run during the agentic loop. A hook can log activity, check a prompt before submission or validate work when a turn stops. Matching hooks from every loaded file run, and command hooks for the same event start together.

Before a non-managed hook can run, Codex requires you to review and trust the exact hook definition.

Official wording: OpenAI, Hooks, read 4 November 2026, developers.openai.com

Trust applies to the hook's current hash. When its definition changes, Codex marks it for review and skips it until you trust the new version.

How hook trust works

Trust is tied to the exact definition. It is not blanket approval for every script with the same filename. A new hook or any changed definition gets a new review state, so a quiet skip often means the trust step is incomplete rather than the script being broken.

Open /hooks before debugging the handler. Review where the hook came from, the event it uses, its matcher and the command it will run. Trust it only when those parts match what you intended. If the source is no longer needed, disable the individual hook there instead of weakening every policy.

The project layer has its own boundary. A repository hook is ignored when the project's .codex/ configuration is untrusted, while user and system hooks can still load. This keeps a cloned repository from silently adding a local command to your sessions.

Where Codex looks for hooks

Choose a user file when the hook should apply across projects. Choose a repository file when the rule belongs with one codebase.

Useful hook locations

Scope

JSON file

Inline configuration

User

~/.codex/hooks.json

~/.codex/config.toml

Repository

<repo>/.codex/hooks.json

<repo>/.codex/config.toml

Source: OpenAI, Hooks, read 4 November 2026.

Codex loads matching hooks from all active layers. A higher-precedence layer does not replace lower-layer hooks. If one layer has both formats, Codex merges them and warns at startup. OpenAI recommends using one representation per layer. Repository hooks load only when the project .codex/ layer is trusted.

Set up your first hook in the CLI

Choose the hooks file, add the event and command, write the script, trust it in /hooks, then run Codex and check.

Choose the hooks file; Add the event and command; Write the script; Trust it in /hooks; Run Codex and check

Step 1: Choose the hooks file

Create .codex/hooks.json in the repository. Use ~/.codex/hooks.json instead when the same hook should apply to every project.

You should see: a startup warning if the new hook still needs review.

In our example: the policy belongs to one repository, so it uses the repository file.

Step 2: Add the event and command

Save this complete file as .codex/hooks.json. It wraps OpenAI's documented PreToolUse block in the required top-level hooks object. The hook matches Bash and runs the policy script from the Git root.

{
  "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"
          }
        ]
      }
    ]
  }
}

A hook has 3 levels: the event, a matcher group and one or more handlers. Commands run from the session working directory, so resolving a repository script from the Git root keeps its path stable when Codex starts in a subdirectory.

You should see: Codex mark the new definition for review and skip it until trusted.

In our example: the matcher limits this policy to Bash calls.

Step 3: Write the policy script

Save the script in the worked example below as .codex/hooks/pre_tool_use_policy.py, which is the path the documented command runs. Every command hook receives one JSON object on standard input. For Bash, the tool name is Bash and the command is in tool_input.command. Exit with no output to allow the call. Return the documented deny object to block it.

You should see: the script receive the pending Bash command before the tool runs.

In our example: the script below rejects a command that contains rm -rf.

Step 4: Review and trust the hook

Start the Codex CLI and enter /hooks. Inspect the source and exact definition, then trust it. The command also lets you disable an individual non-managed hook.

You should see: the hook listed as trusted for its current definition.

In our example: trust the repository hook only after reading its command and script.

Step 5: Run Codex and check

First, test our script directly with this safe input:

printf '%s' '{"tool_name":"Bash","tool_input":{"command":"rm -rf build"}}' | python3 .codex/hooks/pre_tool_use_policy.py

You should see: the deny object printed.

Then create an empty test folder named build. In Codex, ask it to run rm -rf build on that empty folder. Never use a real target.

You should see: the tool call blocked with the hook's reason.

In our example: the direct test proves the script returns a deny object before the same command reaches the tool.

Our example (illustrative).

Our example: refuse-rm-rf

Save this file as .codex/hooks/pre_tool_use_policy.py. The small policy script is ours, not OpenAI's. It uses only the documented input fields and deny shape.

import json
import re
import sys

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

if tool_name == "Bash" and re.search(r"(^|\s)rm\s+-rf(\s|$)", command):
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": "Destructive command blocked by hook."
        }
    }))

The deny object follows this official shape:

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

Plain text on standard output is ignored for PreToolUse. You can also exit with code 2 and write the blocking reason to standard error. Exit code 0 with no output lets Codex continue.

Which events can you hook?

A matcher is a regular expression. An asterisk, an empty string or no matcher matches every occurrence of an event that supports matching.

Events and matcher behavior

Event

When it runs

Matcher filters

PreToolUse

Before a supported local tool

Tool name

PostToolUse

After a supported local tool

Tool name

PermissionRequest

When Codex requests permission

Tool name

SessionStart

When a session starts

Startup, resume, clear, compact or fork

PreCompact and PostCompact

Around compaction

Manual or auto

UserPromptSubmit

Before a prompt is submitted

Ignored

Stop

When a turn stops

Ignored

Source: OpenAI, Hooks, read 4 November 2026.

Choose the event by timing. Use PreToolUse when the decision must happen before a local tool call. Use PostToolUse for review or logging after output exists. Use UserPromptSubmit when the policy is about the user's prompt, and use Stop when the session should continue after Codex would otherwise stop.

Then narrow the event only when its matcher is meaningful. Tool events match on tool name, so ^Bash$ avoids running a shell policy for other local tools. Compaction events match manual or auto. A matcher on UserPromptSubmit or Stop is ignored, so place the condition inside the handler instead.

The event list also tells you where not to put a policy. A review after the tool runs cannot reverse a file change or command side effect. A stop hook can ask the session to continue, but it does not act like a tool denial. Pick the point where the decision can still have the effect you want.

The same PreToolUse hook can be written inline 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"

Block or steer with other events

A UserPromptSubmit hook can block a prompt with this documented response:

{
  "decision": "block",
  "reason": "Ask for confirmation before doing that."
}

A blocking Stop response makes Codex continue and uses its reason as a new prompt. PostToolUse runs too late to undo a tool's side effects. A background hook also cannot block, approve or rewrite the operation that started it.

Choose command or MCP tool handlers

Command handlers run a local process and receive their input as JSON on standard input. They are a practical fit for a repository script that already lives beside the code. An MCP tool handler calls a configured server tool instead. Prompt and agent handler types may parse, but the hooks page says they are skipped.

Use synchronous handlers for policy. Setting async to true starts a command hook in the background while work continues. That is useful for logging, but a background hook cannot block or change the operation. A command hook's timeout is measured in seconds; when omitted, most hooks use 600 seconds.

What hooks do not cover

Shell commands use the local tool hook path and match as Bash. Hosted tools such as WebSearch do not use that path, so PreToolUse and PostToolUse do not run for them. OpenAI describes tool hooks as a guardrail, not a complete enforcement boundary.

Keep model-visible hook output concise. Codex limits each such message to roughly 2,500 tokens by default, and context from several hooks adds up. For more ways to split work safely, see Codex subagents. For repository settings beyond hooks, use the Codex CLI configuration guide.

A hook guards what Codex does in your repository. What AI engines say about your product is a separate check: Xtrusio's free check shows when ChatGPT and Gemini mention you, which competitors appear instead and what to work on next.

If your hook does not run

Start with trust, then check the active layer, feature setting, matcher and tool path.

  • New or changed hook: open /hooks, review the exact definition and trust it.
  • Repository hook missing: confirm the project .codex/ layer is trusted.
  • Hooks disabled: remove the setting below or set it to true.
  • Matcher missed: match the tool name, or remove the matcher from an event that ignores it.
  • Hosted tool: do not expect local tool hooks to intercept WebSearch.
[features]
hooks = false

For already-vetted one-off automation, --dangerously-bypass-hook-trust runs enabled hooks without stored trust for that invocation. The flag removes a safety check, so use it only when another process has already vetted the hook source.

Get your free AI visibility check

See when ChatGPT and Gemini mention you, which competitors appear instead and what to work on next with Xtrusio's free AI visibility check.

Sources

OpenAI and Xtrusio pages read 4 November 2026.

  1. Hooks, OpenAI
  2. Xtrusio home page, Xtrusio

Frequently asked questions

Are Codex hooks on by default?

Yes. Hooks are enabled by default. Set hooks = false under [features] in config.toml to turn them off.

Where do I put a Codex hook file?

Use ~/.codex/hooks.json for user-wide hooks or the repository's.codex/hooks.json for repository hooks. You can also define hooks inline in the matching config.toml.

Why does my new hook not run?

Codex skips new or changed non-managed hooks until you review and trust their exact definition in /hooks.

Can a hook block a shell command?

Yes. A synchronous PreToolUse hook can return a deny decision before a Bash call runs.

What happens when 2 hooks match one event?

All matching hooks run. Matching command hooks for the same event start concurrently.

Topics

  • codex hooks

Xtrusio

AI visibility research

Xtrusio is the Perception Engine for B2B brands.

Free check

See how AI engines answer about you

Get your free AI visibility check

Talk to us about the managed plan (free trial)