codex-orchestration

Authoritative reference for OpenAI Codex CLI's orchestration surface - skills discovery paths, agents/openai.yaml schema, hooks.json events and stdin JSON payload, notify config, and AGENTS.md precede

SKILL.md

GitHub

Codex Orchestration

How OpenAI Codex CLI discovers skills, fires hooks, and loads instructions. Use this skill instead of web-searching the docs each time.

Source of truth: developers.openai.com/codex/{skills,hooks,config-advanced,config-reference,guides/agents-md}. Verify against live docs if behavior seems off - orchestration features evolve.

When to Activate

  • Install a skill and need the correct target directory
  • Wire a Codex hook and need the event name, matcher, or payload shape
  • Debug why an implicit skill isn't triggering on a matching prompt
  • Author or review agents/openai.yaml for a skill
  • Explain to a teammate why format-on-edit hooks don't work in Codex
  • Migrate hook or skill conventions from Claude Code to Codex
  • Add a notify program for turn-complete events
  • Write or update this repo's README/hooks documentation

Discovery: Skills

Skills are folders containing SKILL.md (required) plus optional scripts/, references/, assets/, and agents/openai.yaml. Codex scans these paths (earlier wins on activation; duplicate names do NOT merge - both appear in selector):

Priority Path Scope
1 $CWD/.agents/skills Current folder
2 $CWD/../.agents/skills Parent folder
3 $REPO_ROOT/.agents/skills Repo root
4 $HOME/.agents/skills User
5 /etc/codex/skills Admin
6 Bundled System

Common mistake: copying skills to ~/.codex/skills/ - that path does nothing. Use ~/.agents/skills/.

SKILL.md Format

---
name: kebab-case-name
description: What it does, when to use it, AND when NOT to use it. Codex matches this for implicit invocation.
---

The body loads only after the skill activates. Front-load triggers and scope boundaries in description.

agents/openai.yaml (Optional)

Put it at skills/<name>/agents/openai.yaml:

interface:
  display_name: "User-facing name"
  short_description: "User-facing description"
  icon_small: "./assets/small-logo.svg"
  icon_large: "./assets/large-logo.png"
  brand_color: "#3B82F6"
  default_prompt: "Optional surrounding prompt"

policy:
  allow_implicit_invocation: true

dependencies:
  tools:
    - type: "mcp"
      value: "serverName"
      description: "Required MCP server"

Set allow_implicit_invocation: false for skills that should only fire on explicit /skills or $skillname invocation.

Activation Methods

Method How
Implicit Codex auto-selects based on description matching the user prompt
Explicit (menu) User runs /skills
Explicit (inline) User types $skillname in a prompt

Hooks (Experimental)

Enable with:

# ~/.codex/config.toml
[features]
codex_hooks = true

Config lives at ~/.codex/hooks.json or <repo>/.codex/hooks.json.

Events

Event Fires Matcher matches
SessionStart Session start or resume "startup" or "resume"
PreToolUse Before tool execution Tool name - Bash only currently
PostToolUse After tool completion Tool name - Bash only currently
UserPromptSubmit User submits prompt N/A - use "*" or omit
Stop Turn concludes N/A - use "*" or omit

Critical limit: PreToolUse/PostToolUse match Bash only. File-edit hooks (format-on-save) cannot fire through Codex hooks. Use pre-commit, husky, or a Makefile target.

hooks.json Schema

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "hooks/scripts/guard_force_push.sh",
            "statusMessage": "Checking force-push",
            "timeout": 10
          }
        ]
      }
    ]
  }
}

Stdin JSON Payload

Hook scripts receive JSON on stdin, not as $1. Common fields:

Field Meaning
session_id UUID
transcript_path Path to the session transcript
cwd Working directory
hook_event_name Matches the event
model Model name
turn_id Current turn UUID
tool_input Tool-specific - for Bash, has command

Parse with jq:

payload="$(cat)"
cmd="$(printf '%s' "$payload" | jq -r '.tool_input.command // empty')"

Exit Codes

Code Meaning
0 Allow / continue
2 Block the tool call (PreToolUse)
Other non-zero Error surfaced to operator

For SessionStart, UserPromptSubmit, Stop, scripts can emit a JSON object on stdout with: continue (bool), stopReason (string), systemMessage (string), suppressOutput (bool).

notify (Stable, simpler)

Separate from hooks.json. Lives in config.toml:

notify = ["python3", "/path/to/notify.py"]

Fires on agent-turn-complete only. Receives JSON as argv[1] (single string arg), NOT stdin. Fields: type, thread-id, turn-id, cwd, input-messages, last-assistant-message.

AGENTS.md Precedence

Codex builds an instruction chain from AGENTS.md files:

  1. ~/.codex/AGENTS.override.md (if present, else ~/.codex/AGENTS.md)
  2. Repo root AGENTS.md
  3. Subdirectory AGENTS.md files closer to $CWD

@file references inside AGENTS.md resolve and inline, so AGENTS.md can pull in skill bodies directly:

## Domain Guidance
- Security audits: @.agents/skills/security/SKILL.md

BAD / GOOD: Installing a Skill

BAD

cp -r my-skill ~/.codex/skills/     # wrong directory; Codex ignores this

GOOD

mkdir -p ~/.agents/skills
cp -r my-skill ~/.agents/skills/    # user-scope discovery
# or
mkdir -p ./.agents/skills
cp -r my-skill ./.agents/skills/    # repo-scope discovery

BAD / GOOD: Writing a PreToolUse Hook

BAD

#!/usr/bin/env bash
# Assumes the command arrives as $1 - it doesn't.
if [[ "$1" == *"rm -rf"* ]]; then exit 2; fi

GOOD

#!/usr/bin/env bash
set -euo pipefail
payload="$(cat)"
cmd="$(printf '%s' "$payload" | jq -r '.tool_input.command // empty')"
if [[ "$cmd" == *"rm -rf /"* ]]; then
  echo "Blocking dangerous rm -rf /" >&2
  exit 2
fi
exit 0

BAD / GOOD: Format-on-edit

BAD

{
  "hooks": {
    "PostToolUse": [
      { "matcher": "Edit", "hooks": [{ "type": "command", "command": "prettier --write" }] }
    ]
  }
}

Edit is not a matchable tool name for PostToolUse today. The hook never fires.

GOOD

Move it to pre-commit:

repos:
  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v3.1.0
    hooks: [{ id: prettier }]

Or expose it via make fmt and document it in AGENTS.md under ## Environment Setup.

Codex vs Claude Code Differences

Concern Codex Claude Code
Skill install dir .agents/skills/ .claude/skills/ or plugin packs
Skill frontmatter name, description name, description, allowed-tools, etc.
Hooks config hooks.json (experimental) settings.json hooks block (GA)
Pre/PostToolUse matchers Bash only All tools including Edit, Write, etc.
Hook input stdin JSON stdin JSON
Turn-complete hook notify in config.toml Stop hook
Instructions file AGENTS.md CLAUDE.md

Debugging Checklist

When a skill or hook isn't firing:

  • Confirm the skill folder lives under one of the six .agents/skills paths. Duplicate names surface in the selector, they don't merge.
  • Check description for triggering vocabulary. A vague description will miss implicit activation.
  • For explicit invocation, run /skills and verify the skill is listed.
  • For hooks, verify [features] codex_hooks = true is set and hooks.json parses as valid JSON.
  • For PreToolUse/PostToolUse, confirm the matcher is Bash - Edit/Write tools won't fire it.
  • Add a trace line to the hook script: echo "fired: $(date)" >> /tmp/codex-hook.log. If nothing appears, Codex is not invoking the hook.
  • Tail the transcript path from the hook payload if you need to see what Codex saw.

Checklist

  • Skills install under .agents/skills/ (not .codex/skills/)
  • Every SKILL.md description declares scope boundary ("do NOT use for ...")
  • agents/openai.yaml added when a skill needs implicit-invocation policy or MCP dependencies
  • hooks.json lives at ~/.codex/hooks.json or <repo>/.codex/hooks.json
  • Hook scripts parse JSON from stdin, not $1
  • PreToolUse hooks use matcher "Bash" (no other tools supported yet)
  • Format-on-edit is routed through pre-commit or make, not hooks.json
  • [features] codex_hooks = true set in config.toml before expecting hooks to fire
  • notify is used only for agent-turn-complete; payload arrives via argv[1]
  • AGENTS.md uses @file imports to pull skill bodies into long-lived context when needed