Issue 00 · Autumn 2026日本語 · RSS
AgentKoji

Codex CLI Hooks: A Practical Introduction

How Codex CLI hooks work: where hooks.json and [hooks] live, trust review with /hooks, a PreToolUse hook that blocks Bash commands, and what hooks can't stop.

TL;DR

  • Codex hooks run scripts or MCP tools during the agentic loop. They're on by default, load from hooks.json or [hooks] tables in config.toml under ~/.codex/ and <repo>/.codex/, and every matching hook from every source runs.
  • A non-managed hook runs only after you review and trust it with /hooks. Trust is recorded against the hook's hash, so a new or changed hook is skipped until you trust it again, and project hooks load only when the project's .codex/ layer is trusted.
  • A PreToolUse hook blocks a call by returning permissionDecision: "deny" in JSON or by exiting 2 with the reason on stderr. Hosted tools such as WebSearch don't go through tool hooks, and the docs call hooks a useful guardrail, not a complete enforcement boundary.

Codex hooks let you run your own scripts or MCP tools at fixed points in the agent loop: before a tool runs, after it runs, when a turn stops, and more. This guide covers what hooks are, where Codex looks for them, the trust review you have to pass before a hook runs, a minimal PreToolUse hook that blocks risky Bash commands, one PostToolUse and one Stop example, a checklist for hooks that don’t fire, and the limits. Everything here comes from the official hooks page, checked 2026-11-03. We have not run the hook examples below in Codex. They follow the documented format but are unverified.

What Codex hooks are

Hooks are an extensibility framework for Codex. They let you run scripts or MCP tools during the agentic loop. The docs list uses such as sending the chat to a logging engine, scanning prompts for pasted API keys, and running a validation check when a turn stops.

Hooks are enabled by default. Three rules shape how they run:

The events:

WhenEvents
During a turnPreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, UserPromptSubmit, SubagentStop, Stop
For each model request attemptPreModelRequest, PostModelRequest
When you interrupt an active turnInterrupt (not for subagents)
When a session or subagent startsSessionStart, SubagentStart
When the main thread endsSessionEnd (not for subagents)

Source: https://developers.openai.com/codex/hooks (checked 2026-11-03)

If you know Claude Code hooks

Some things look familiar. Codex uses event names such as PreToolUse, PostToolUse, UserPromptSubmit and Stop, a PreToolUse hook can deny with hookSpecificOutput.permissionDecision, and exit code 2 blocks with the reason on stderr. Three Codex-specific points are worth knowing before you copy a Claude Code hook across:

We compared only what the two sets of docs state; we haven’t tested whether a given Claude Code hook script behaves the same in Codex. For Claude Code’s side, see Claude Code Hooks in Practice and the Claude Code hooks reference: every event and exit code on one page.

Where Codex looks for hooks

Codex reads hooks from hooks.json files or inline [hooks] tables in config.toml, next to its active config layers. The four most useful locations are:

If more than one source exists, Codex loads all matching hooks; a higher-precedence layer doesn’t replace a lower one. If one layer has both hooks.json and inline [hooks], Codex merges them and warns at startup, so pick one form per layer. Enabled plugins can bundle hooks too, and those go through the same trust review.

Project-local hooks load only when the project’s .codex/ layer is trusted. In an untrusted project, Codex still loads your user and system hooks.

Source: https://developers.openai.com/codex/hooks (checked 2026-11-03)

Review and trust hooks

Before a non-managed hook can run, you review and trust its exact definition. Codex records trust against the hook’s current hash, so a new or changed hook is marked for review and skipped until you trust it. Editing a trusted hook therefore turns it off until you review it again.

Run /hooks in the CLI to see hook sources, review new or changed hooks, trust them, or disable individual non-managed hooks. When hooks need review at startup, Codex prints a warning telling you to open /hooks. Managed hooks from system, MDM, cloud or requirements.toml sources are trusted by policy and can’t be disabled from that browser.

For one-off automation that vets hook sources some other way, --dangerously-bypass-hook-trust runs enabled hooks without persisted trust for that invocation.

Source: https://developers.openai.com/codex/hooks (checked 2026-11-03)

A minimal PreToolUse hook that blocks risky Bash commands

A hook has three levels: an event, a matcher group that decides when the event matches, and one or more handlers. Put this in <repo>/.codex/hooks.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "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"
          }
        ]
      }
    ]
  }
}

Commands run with the session’s working directory, and Codex may be started from a subdirectory, so the docs recommend resolving repo-local scripts from the git root as above. The same hook 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"

Every command hook receives one JSON object on stdin. For Bash, the command is in tool_input.command. Plain text on stdout is ignored, so the script answers in JSON. Save it as .codex/hooks/pre_tool_use_policy.py (unverified example):

#!/usr/bin/env python3
"""PreToolUse hook: deny Bash commands that match a blocklist. Unverified example."""
import json
import re
import sys

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

# Patterns are examples; adjust them to your repository's policy.
BLOCKED = [
    (r"\brm\s+-\w*r\w*f|\brm\s+-\w*f\w*r", "recursive forced delete"),
    (r"\bgit\s+push\b.*\s(--force|-f)\b", "force push"),
]

for pattern, label in BLOCKED:
    if re.search(pattern, command):
        print(json.dumps({
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": f"Blocked by repository policy: {label}.",
            }
        }))
        sys.exit(0)

sys.exit(0)

Exit 0 with no output counts as success, and the tool call goes ahead. Instead of JSON, you can exit with code 2 and write the reason to stderr. Codex also accepts the older {"decision": "block", "reason": "..."} shape.

Don’t return permissionDecision: "ask", continue: false, stopReason or suppressOutput from PreToolUse. They’re parsed but not supported yet, and Codex marks the hook run as failed and continues the tool call. A regex blocklist like this one is easy to get around, so test it against the inputs you care about. More blocklist patterns are in PreToolUse Hook Examples for Claude Code (Copy-Paste), and the holes we found in our first git guard are in Claude Code Hooks in Practice.

Then start Codex in the repository, open /hooks, review the new hook and trust it. Until you do, it’s skipped.

Source: https://developers.openai.com/codex/hooks (checked 2026-11-03)

PostToolUse and Stop: one use each

PostToolUse runs after a supported tool produces output, including Bash commands that exit non-zero. It can’t undo what the tool did. Returning decision: "block" or exiting 2 replaces the tool result with your feedback, and the model continues from it. A typical use is flagging edits to generated files after a command ran (unverified example):

{
  "decision": "block",
  "reason": "The command changed generated files. Regenerate them instead of editing by hand.",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "Files under gen/ are generated."
  }
}

Stop runs when a turn is about to end, and its matcher is ignored. It must print JSON when it exits 0; plain text is invalid. decision: "block" doesn’t reject the turn: Codex continues and sends your reason as a new prompt. That suits a “run the tests before you finish” check (unverified example):

{
  "decision": "block",
  "reason": "Run one more pass over the failing tests."
}

The input includes stop_hook_active, which tells you whether this turn was already continued by Stop; check it so the hook doesn’t keep the turn going forever. If any matching Stop hook returns continue: false, that takes precedence over the others’ continuation decisions.

Source: https://developers.openai.com/codex/hooks (checked 2026-11-03)

When a hook doesn’t run

Check these in order:

Source: https://developers.openai.com/codex/hooks (checked 2026-11-03)

For the same kind of checklist on the Claude Code side, see Claude Code Hooks Not Working? A Checklist.

Limits

PreToolUse and PostToolUse cover shell commands, apply_patch, MCP tools and most other local function tools. Hosted tools such as WebSearch don’t use the local function-tool hook path, so tool hooks never see them. Some specialized tool paths can also opt out of the default hook path. The docs say to treat tool hooks as a useful guardrail, not a complete enforcement boundary.

Two more limits from the same page:

Source: https://developers.openai.com/codex/hooks (checked 2026-11-03)

To get Codex CLI set up first, see Getting Started with Codex CLI.

Summary

FAQ

Where do I put Codex CLI hooks?
In hooks.json or in inline [hooks] tables in config.toml. The docs name four practical locations: ~/.codex/hooks.json, ~/.codex/config.toml, <repo>/.codex/hooks.json and <repo>/.codex/config.toml. If several exist, Codex loads all matching hooks.
Why doesn't my Codex hook run?
The usual causes are a hook you haven't trusted yet (new or changed hooks are skipped until you trust them in /hooks), a project .codex/ layer that isn't trusted, a matcher that doesn't match the tool name or is ignored for that event, a timeout, or hooks turned off with [features] hooks = false.
How does a Codex PreToolUse hook block a command?
Return JSON on stdout with hookSpecificOutput.permissionDecision set to "deny" and a permissionDecisionReason, or exit with code 2 and write the reason to stderr. Plain text on stdout is ignored.
Are Codex hooks a security boundary?
No. Hosted tools such as WebSearch don't use the tool hook path, some specialized tool paths can opt out, and the docs say to treat tool hooks as a useful guardrail, not a complete enforcement boundary.
How long can a Codex hook run?
timeout is in seconds. If you omit it, most hooks get 600 seconds. SessionEnd and Interrupt default to 1 second and support up to 3 seconds.