Skip to main content

Enabling and Using Codex's experimental Hooks

TL;DR


Background

This article was verified against Codex v0.115.0 (March 2026). Since this is an in-development feature, behavior may change in future versions.

Codex doesn’t officially have a Hooks feature yet. The official docs say nothing about it, and the Changelog only mentions a one-liner: “experimental hooks engine.”

But it turns out a working Hooks implementation is already shipping under the “under development” status. Running codex features list revealed a codex_hooks feature flag, and once enabled, hooks actually fire.

The trouble is that the config JSON structure and the list of supported events are documented nowhere — so I read the Rust source in the OSS openai/codex repository (Apache License 2.0), specifically codex-rs/hooks/, to nail down the spec. This post summarizes what I found.


1. Discovery and enablement

Checking the feature flag

Codex has a codex features list command that prints the feature flag list. That’s how I learned about Hooks in the first place: I ran it and saw codex_hooks listed as “under development.”

$ codex features list
codex_hooks    under development  false

Disabled by default.

Enable it

# persisted to config.toml
codex features enable codex_hooks

Once enabled, you’ll see this warning at startup:

⚠ Under-development features enabled: codex_hooks. Under-development features are incomplete
  and may behave unpredictably.

2. Configuration file

Path and format

ItemValue
Global config~/.codex/hooks.json
Project config<project>/.codex/hooks.json
FormatJSON

Codex’s general config lives in ~/.codex/config.toml (TOML), but Hooks config is a separate hooks.json file in JSON. Writing hooks into config.toml won’t be picked up.

~/.codex/
├── config.toml      ← general settings & feature flags
└── hooks.json       ← hooks config (JSON)

JSON structure

The structure is a bit unusual: instead of putting handlers directly under each event name, you need an extra wrapper layer. In the source (codex-rs/hooks/src/engine/config.rs), it’s defined as a MatcherGroup struct with matcher (a regex filter) and hooks (an array of handlers).

hooks.<EventName>[].matcher  → filter condition (optional)
hooks.<EventName>[].hooks[]  → array of handlers to run

A concrete JSON looks like this:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo 'session started'",
            "timeout": 10
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo 'session stopped'",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

timeout is optional and defaults to 600 seconds.

What doesn’t work

If you don’t know about the nested structure and put handlers directly under the event array, nothing fires:

{
  "hooks": {
    "SessionStart": [
      {
        "type": "command",
        "command": "echo 'session started'"
      }
    ]
  }
}

I actually tried this form first, hit silence, and only realized the nesting was required after reading the source. With no official docs, the source is the only accurate reference.


3. Supported events

The three supported events

EventWhen it fires
SessionStartWhen a session starts or resumes
UserPromptSubmitWhen the user submits a prompt
StopWhen the agent finishes responding

Tool execution and subagent-related events aren’t implemented yet.

Handler types

A “handler type” specifies what to actually run when a hook fires. The source (codex-rs/hooks/src/engine/config.rs) defines three types — command, prompt, and agent — but only command (run a shell command) actually works today.

TypeBehaviorStatus
commandRuns a shell commandWorks
promptInjects a prompt into the agent’s contextNot implemented (logs "skipping prompt hook")
agentSpawns another agent to handle the workNot implemented

async: true (asynchronous execution) is also defined but not implemented. Once prompt lands, you could “inject extra instructions on every prompt submit”; with agent, you could “kick off a review agent after each response.” For now, though, the only option is “event fires → shell command runs.”

JSON payload passed to stdin

Hook commands run via $SHELL -l -c "<command>", and a JSON payload is piped to stdin.

{
  "session_id": "019d07b0-...",
  "cwd": "/Users/user/project",
  "hook_event_name": "SessionStart",
  "model": "o3-pro",
  "permission_mode": "default",
  "source": "startup",
  "transcript_path": null
}
FieldDescription
session_idSession ID (UUID v7)
cwdWorking directory
hook_event_nameEvent name
modelModel in use
permission_modeApproval policy
sourceStart type (SessionStart only)
transcript_pathPath to the conversation history file (currently null)

Some fields are added per-event:

FieldEventDescription
sourceSessionStartstartup / resume / clear
promptUserPromptSubmitThe user’s input text
turn_idUserPromptSubmit / StopConversation turn identifier
last_assistant_messageStopThe last assistant response
stop_hook_activeStopWhether a Stop hook is active

session_id, cwd, hook_event_name, model, and permission_mode are common to all events.


4. Use case: unified management of multiple agent sessions

I built a desktop app that uses AI coding agent Hooks to centrally track the state of multiple sessions. It listens for SessionStart / Stop events and updates a session list view.

Now that Codex supports Hooks too, I extended the same app to manage Codex sessions alongside the others.

What I configure in hooks.json

When you write events and commands into hooks.json, Codex runs the commands when those events fire. Conversely, if hooks.json is empty or missing, nothing happens — even with the feature flag on.

In the example below, every event does the same thing: read the JSON payload from stdin via $(cat) and forward it as-is to my app’s API with curl.

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "curl -s -X POST http://localhost:3000/api/hook -H 'Content-Type: application/json' -d \"$(cat)\"",
            "timeout": 10
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "curl -s -X POST http://localhost:3000/api/hook -H 'Content-Type: application/json' -d \"$(cat)\"",
            "timeout": 10
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "curl -s -X POST http://localhost:3000/api/hook -H 'Content-Type: application/json' -d \"$(cat)\"",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

The receiving app inspects session_id and hook_event_name in the JSON to update session state. Because this pattern is shared across other tools’ Hooks, a single API endpoint can centralize multiple agent tools together.


5. Limitations and outlook

Current limitations

The OSS upside

For all those limitations, Codex is OSS under Apache License 2.0, which means you can verify the Hooks implementation directly against the source. Everything in this post came from reading codex-rs/hooks/src/. Being able to fall back to the source when the spec is unclear is a real comfort.

What’s next

The Discussion #2150 thread on GitHub has lots of community comments requesting Hooks features, which signals strong interest. I’d expect tool events and additional handler types to land over time.


Summary

Codex Hooks is still in development with limited functionality, but flipping the feature flag gets you working hooks today. For use cases like detecting session start/end and running commands, it’s already useful enough.


References

ZSL
ZSL

AI Engineer

Researching and practicing development workflows powered by Generative AI.