How the Codex Plugin Integrates with Claude Code Hooks (SessionStart, SessionEnd, Stop)

The Codex plugin registers three Claude Code lifecycle hooks—SessionStart, SessionEnd, and Stop—via plugins/codex/hooks/hooks.json, with each hook executing a Node script that manages session state, cleanup, and optional review gates.

The openai/codex-plugin-cc repository implements deep integration with Claude Code's plugin architecture by hooking into session lifecycle events. These hooks enable the plugin to persist session metadata, gracefully shut down background Codex processes, and optionally gate session termination with automated code reviews. This article breaks down the exact mechanism, file paths, and execution flow for each hook.

Hook Registration in hooks.json

All three hooks are declared in plugins/codex/hooks/hooks.json, which Claude Code reads during plugin initialization:

{
  "description": "Optional stop-time review gate for Codex Companion.",
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-lifecycle-hook.mjs\" SessionStart",
            "timeout": 5
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-lifecycle-hook.mjs\" SessionEnd",
            "timeout": 5
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop-review-gate-hook.mjs\"",
            "timeout": 900
          }
        ]
      }
    ]
  }
}

Key implementation details:

  • ${CLAUDE_PLUGIN_ROOT} expands to the plugin's installation directory, ensuring portable script paths.
  • SessionStart and SessionEnd use a 5-second timeout—sufficient for lightweight env-file operations.
  • Stop uses a 900-second (15-minute) timeout to accommodate full Codex review cycles.

SessionStart Hook: Capturing Session Context

When Claude Code initializes a new session, it fires SessionStart and pipes a JSON payload to session-lifecycle-hook.mjs. The handleSessionStart function (lines 77-82) extracts and persists three values:

function handleSessionStart(input) {
  appendEnvVar(SESSION_ID_ENV, input.session_id);
  appendEnvVar(TRANSCRIPT_PATH_ENV, input.transcript_path);
  appendEnvVar(PLUGIN_DATA_ENV, process.env[PLUGIN_DATA_ENV]);
}

The script receives this input structure via stdin:

{
  "session_id": "1234-abcd",
  "transcript_path": "/home/me/.claude/projects/1234-abcd.jsonl",
  "cwd": "/home/me/my-repo"
}

Environment variables written to the Claude env file:

Variable Purpose
CODEX_COMPANION_SESSION_ID Correlates Codex jobs with the active Claude session
CLAUDE_TRANSCRIPT_PATH Enables the transfer command to read conversation history
CLAUDE_PLUGIN_DATA Passes plugin-specific state between hooks

These variables enable later hooks to scope all Codex operations to the correct session.

SessionEnd Hook: Graceful Teardown

The SessionEnd hook triggers handleSessionEnd (lines 83-114), which orchestrates a four-stage cleanup:

async function handleSessionEnd(input) {
  const cwd = input.cwd || process.cwd();
  const brokerSession = loadBrokerSession(cwd) ?? null;
  const brokerEndpoint = brokerSession?.endpoint ?? null;

  if (brokerEndpoint) {
    await sendBrokerShutdown(brokerEndpoint);
  }

  cleanupSessionJobs(cwd, input.session_id || process.env[SESSION_ID_ENV]);
  teardownBrokerSession({ endpoint: brokerEndpoint, pidFile, logFile, sessionDir, pid, killProcess: terminateProcessTree });
  clearBrokerSession(cwd);
}

Cleanup sequence:

  1. Broker shutdown – If a Codex App-Server broker is running, send it a graceful shutdown request.
  2. Job termination – cleanupSessionJobs kills any queued or running Codex jobs matching the session ID.
  3. Process cleanup – teardownBrokerSession terminates the broker process tree and removes pid/log files.
  4. State clearing – clearBrokerSession deletes persisted broker metadata from disk.

This ensures no orphaned processes survive Claude session termination.

Stop Hook: The Review Gate

The Stop hook implements an optional review gate that can block session termination if Codex finds issues in the final Claude response. Unlike the lifecycle hooks, it runs stop-review-gate-hook.mjs with a 15-minute timeout.

Activation and Flow

The gate activates only when config.stopReviewGate is true. Execution proceeds through these stages:

  1. Configuration load – getConfig(workspaceRoot) reads plugin settings.
  2. Job detection – listJobs → filterJobsForCurrentSession → sortJobsNewestFirst identifies running Codex work.
  3. Prompt construction – buildStopReviewPrompt injects last_assistant_message into plugins/codex/prompts/stop-review-gate.md.
  4. Review execution – runStopReview spawns codex-companion.mjs with the assembled prompt.
  5. Decision parsing – parseStopReviewOutput scans for ALLOW: or BLOCK: directives.

Blocking Decision Output

When Codex returns BLOCK:, the hook emits this JSON to stdout:

emitDecision({
  decision: "block",
  reason: "...explanation..."
});

Claude Code consumes this output and surfaces the rationale to the user, preventing session termination until issues are addressed.

Simulated Hook Invocation

Test the SessionStart behavior manually by piping JSON to the hook script:

cat <<EOF | node plugins/codex/scripts/session-lifecycle-hook.mjs SessionStart
{
  "session_id": "1234-abcd",
  "transcript_path": "/home/me/.claude/projects/1234-abcd.jsonl",
  "cwd": "/home/me/my-repo"
}
EOF

This writes export statements to $CLAUDE_ENV_FILE (when set), making session data available to subprocesses.

Test the Stop gate similarly:

cat <<EOF | node plugins/codex/scripts/stop-review-gate-hook.mjs
{
  "session_id": "1234-abcd",
  "last_assistant_message": "Here is the final implementation..."
}
EOF

Output will be either {"decision":"allow"} or {"decision":"block","reason":"..."}.

Key Implementation Files

Purpose Path
Hook descriptor plugins/codex/hooks/hooks.json
Session lifecycle script plugins/codex/scripts/session-lifecycle-hook.mjs
Stop-gate review script plugins/codex/scripts/stop-review-gate-hook.mjs
Review prompt template plugins/codex/prompts/stop-review-gate.md
Job and configuration state plugins/codex/scripts/lib/state.mjs
Broker management utilities plugins/codex/scripts/lib/broker-lifecycle.mjs

Summary

  • SessionStart persistence – Stores session_id and transcript_path in environment variables for cross-hook coordination.
  • SessionEnd cleanup – Gracefully terminates the Codex broker and all session-associated jobs via session-lifecycle-hook.mjs.
  • Stop gating – Optionally blocks session termination with AI-reviewed rationale when stopReviewGate is enabled.
  • Timeout scaling – 5 seconds for lightweight lifecycle hooks, 15 minutes for comprehensive code review.
  • State isolation – All operations are scoped to the active session ID, preventing cross-session interference.

Frequently Asked Questions

What triggers the SessionStart and SessionEnd hooks in Claude Code?

Claude Code fires SessionStart when a new conversation session begins and SessionEnd when the session terminates normally. These are automatic lifecycle events that require no user action. The plugin's session-lifecycle-hook.mjs receives session metadata via stdin and handles environment setup or teardown accordingly.

How does the Stop hook differ from SessionEnd?

Stop runs when the user issues /stop or Claude Code initiates session termination, but before the session actually ends. This allows the plugin to intercept and potentially block the operation. SessionEnd runs after termination is confirmed and handles final cleanup. The Stop hook's 15-minute timeout supports lengthy Codex reviews; SessionEnd uses 5 seconds for rapid cleanup.

Can the review gate be disabled?

Yes. Set stopReviewGate: false in the plugin configuration (managed via getConfig in state.mjs). When disabled, the Stop hook script still executes but skips the review logic and immediately allows termination.

What happens if the broker fails to shut down gracefully?

The handleSessionEnd function in session-lifecycle-hook.mjs passes terminateProcessTree as killProcess to teardownBrokerSession. If sendBrokerShutdown times out or fails, the broker process and its descendants are forcefully terminated, and all associated state files are removed via clearBrokerSession.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →