Skip to content

Codex CLI Adapter

Native event names: PascalCase (Claude-like). Reference doc: https://developers.openai.com/codex/hooks.

Codex shares Claude's response envelope (hookSpecificOutput.permissionDecision, additionalContext) and exit-code semantics. Differences from Claude: extra context fields (turn_id, tool_use_id, last_assistant_message) and the apply_patch tool name for file edits.

Stdin → unified

Codex hook_event_nameUnified
SessionStartSessionStart (preserves source, lifts model)
PreToolUsePreToolUse (tool aliased; native_tool preserves the original; _native keeps turn_id, tool_use_id)
PostToolUsePostToolUse (lifts model, native_tool)
UserPromptSubmitUserPromptSubmit (lifts model)
StopStop (preserves stop_hook_active; lifts last_assistant_message and model)

Codex also emits PermissionRequest as a separate permission-gating event with its own response shape (hookSpecificOutput.decision.behavior: "allow" | "deny" plus optional decision.message). The adapter folds it into the unified PreToolUse handler so you write one handler that covers both. On the wire, the bridge serializes the response as PermissionRequest's nested shape when the original event was PermissionRequest. To distinguish in your handler, read event._native.hook_event_name. Codex's PermissionRequest does not accept decision: "ask"; passing it logs a warning and falls back to allow.

apply_patch aliasing

Codex uses one tool name for all file modifications:

jsonc
{
  "tool_name": "apply_patch",
  "tool_input": {
    "patch": "..."
  }
}

The adapter aliases this to tool: "Edit":

ts
defineHook({
  PreToolUse: (event) => {
    if (event.tool === "Edit") {
      // works on Claude (tool_name=Edit), Codex (apply_patch), Cursor (afterFileEdit/preToolUse)
    }
  },
});

The unified event.tool === "Write" does not fire for Codex, even if the patch creates a brand-new file: Codex emits one tool name (apply_patch) for both creates and updates, and the bridge collapses both to Edit. Two ways to disambiguate:

  • Read event.native_tool (always set; equals the host's original tool name, e.g. "apply_patch" for Codex, "Edit" or "Write" for Claude).
  • Parse event.tool_input.patch and look for *** Add File: (create) vs *** Update File: (update).

Other tool names pass through unchanged.

Unified → stdout

Unified fieldCodex wire mapping
decision on PreToolUsehookSpecificOutput.permissionDecision + exit 2 if deny
reason on PreToolUsehookSpecificOutput.permissionDecisionReason
decision: "deny" on otherstop-level decision: "block" + reason + exit 2
additional_context on context-injection eventshookSpecificOutput.additionalContext

Dropped fields

  • user_message: Codex has no human-facing channel; warn + drop
  • modified_input: not wired on Codex; warn + drop

Round-trip example

Stdin (note turn_id + apply_patch):

jsonc
{
  "hook_event_name": "PreToolUse",
  "session_id": "sess-codex",
  "cwd": "/Users/x/proj",
  "model": "gpt-5",
  "turn_id": "turn-001",
  "tool_use_id": "tu-001",
  "tool_name": "apply_patch",
  "tool_input": {
    "patch": "*** Begin Patch..."
  }
}

The handler from Getting Started checks tool === "Bash"; apply_patch aliases to Edit, not Bash, so the response is:

ts
{
  decision: "allow"
}

Stdout (consumed by Codex):

jsonc
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow"
  }
}

exit code: 0

Generated config

.codex/hooks.toml (TOML, not JSON):

toml
# >>> agent-hooks-bridge: managed block; do not edit between markers

[[hooks]]
event = "SessionStart"
command = "./.hooks/format.ts --host codex"

[[hooks]]
event = "PreToolUse"
command = "./.hooks/format.ts --host codex"
matcher = "*"

[[hooks]]
event = "PostToolUse"
command = "./.hooks/format.ts --host codex"
matcher = "*"

[[hooks]]
event = "UserPromptSubmit"
command = "./.hooks/format.ts --host codex"

[[hooks]]
event = "Stop"
command = "./.hooks/format.ts --host codex"

# <<< agent-hooks-bridge

Re-running install replaces the block between the markers in place. Anything outside the markers (your existing TOML config) is preserved.

Not affiliated with Anthropic, Anysphere, or OpenAI. Supported by LogicStar AI.