How Hooks Integrate with Claude Code's Lifecycle in the Codex Plugin
The Codex plugin for Claude Code uses three automated hook scripts—SessionStart, SessionEnd, and Stop—that intercept key lifecycle events to manage environment variables, terminate background jobs, and enforce optional review gates.
The openai/codex-plugin-cc repository implements a robust integration layer that allows Codex to participate in Claude Code's session lifecycle through declarative hooks. These hooks automatically execute at critical moments—when sessions begin, end, or prepare to stop—enabling the plugin to synchronize state, clean up resources, and enforce safety checks. Understanding how hooks integrate with Claude Code's lifecycle in the Codex plugin is essential for developers who want to leverage background job execution and automated code review gates.
The Three Lifecycle Hooks
The plugin defines three distinct hooks in plugins/codex/hooks/hooks.json that Claude Code invokes automatically. Each hook serves a specific purpose in managing the session boundary and runtime behavior.
SessionStart: Initializing the Environment
When a Claude Code session begins—typically after executing /codex:setup—Claude Code invokes plugins/codex/scripts/session-lifecycle-hook.mjs with the argument SessionStart and passes a JSON payload on stdin. This payload contains the session_id and transcript_path.
The handleSessionStart function extracts these values and exposes them via environment variables:
// session-lifecycle-hook.mjs
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]);
}
This makes CODEX_COMPANION_SESSION_ID and CLAUDE_TRANSCRIPT_PATH available to downstream Codex commands, allowing background jobs to associate themselves with the current Claude Code session. The hook also persists any existing CLAUDE_PLUGIN_DATA across the session boundary.
SessionEnd: Cleaning Up Resources
When a Claude Code session terminates—either because the user closed the session or the plugin called /codex:cancel—Claude Code invokes the same lifecycle script with the argument SessionEnd.
The hook performs three critical cleanup operations:
- Broker shutdown: Loads persisted broker information via
loadBrokerSessionand sends a shutdown request usingsendBrokerShutdown - Job termination: Calls
cleanupSessionJobsto iterate over any jobs with status"queued"or"running"and terminates their process trees - State removal: Removes lingering session files and state entries
The termination logic appears in session-lifecycle-hook.mjs:
// session-lifecycle-hook.mjs
for (const job of removedJobs) {
const stillRunning = job.status === "queued" || job.status === "running";
if (!stillRunning) continue;
try {
terminateProcessTree(job.pid ?? Number.NaN);
} catch { /* ignore teardown failures */ }
}
This ensures that background Codex processes do not survive a closed Claude Code session, preventing resource leaks and zombie processes.
Stop Review Gate: Intercepting Session Completion
If the user enables the review gate via /codex:setup --enable-review-gate, Claude Code invokes plugins/codex/scripts/stop-review-gate-hook.mjs right before completing a turn. This hook acts as a safety checkpoint that can block the session based on Codex feedback.
The hook execution flow follows these steps:
- Retrieves the last Claude assistant message from the session context
- Loads the
stop-review-gateprompt template and interpolates the message content - Executes the companion script (
codex-companion.mjs) as a child process with the constructed prompt - Parses the JSON return value for decision markers
The hook interprets lines starting with ALLOW: as permission to continue, while BLOCK: aborts the turn and surfaces the reason to the user:
// stop-review-gate-hook.mjs
const review = runStopReview(cwd, input);
if (!review.ok) {
emitDecision({
decision: "block",
reason: review.reason
});
}
This creates an automated checkpoint where Codex can review Claude Code's output before the user sees it, enabling quality gates in the development workflow.
How Claude Code Wires the Hooks Together
Hook registration occurs through plugins/codex/hooks/hooks.json, which declares the three events and maps them to their corresponding executable scripts. When the plugin installs, Claude Code reads this manifest and registers the handlers for the session lifecycle.
Input protocol: For SessionStart and SessionEnd, Claude Code passes a JSON payload via stdin with the following structure:
{
"hook_event_name": "SessionStart",
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/xyz.jsonl"
}
The session-lifecycle-hook.mjs script parses this input and dispatches to the appropriate handler based on the hook_event_name field.
State persistence: Both hooks rely on plugins/codex/scripts/lib/state.mjs to maintain job status, process IDs, and session mappings across the lifecycle. The broker lifecycle management in plugins/codex/scripts/lib/broker-lifecycle.mjs handles the Codex app-server startup and shutdown coordination, while plugins/codex/scripts/lib/process.mjs provides the terminateProcessTree utility for safe process cleanup.
Practical Implementation Examples
Enabling the Review Gate
To activate the stop review gate, execute:
/codex:setup --enable-review-gate
Once enabled, Claude Code automatically invokes stop-review-gate-hook.mjs before each turn completion, running a Codex review of the assistant's last response.
Manually Canceling Background Jobs
While the SessionEnd hook automatically cleans up jobs, you can manually terminate a specific job during a session:
/codex:cancel <job-id>
This command triggers the same cleanup logic that the SessionEnd hook uses, calling cleanupSessionJobs to remove the specific job from state and terminate its process tree via terminateProcessTree.
SessionStart Payload Handling
When Claude Code starts a session, it sends the initialization payload to the hook script. The following example shows how the hook processes this input to set up the environment:
{
"hook_event_name": "SessionStart",
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/xyz.jsonl"
}
The hook extracts these fields and writes them to the process environment, making them available to the companion script (codex-companion.mjs) when it executes Codex tasks.
Summary
- Three lifecycle hooks—SessionStart, SessionEnd, and Stop—provide integration points between Claude Code and the Codex plugin, declared in
plugins/codex/hooks/hooks.json. - SessionStart exports
CODEX_COMPANION_SESSION_IDandCLAUDE_TRANSCRIPT_PATHviahandleSessionStartinsession-lifecycle-hook.mjs, allowing background jobs to locate session resources. - SessionEnd automatically terminates background Codex processes and shuts down the broker via
cleanupSessionJobsandsendBrokerShutdown, preventing resource leaks. - Stop review gate optionally intercepts turn completion via
stop-review-gate-hook.mjs, usingcodex-companion.mjsto review Claude Code output and block the session if Codex returns aBLOCK:decision. - State management relies on
lib/state.mjsandlib/broker-lifecycle.mjsto persist job and broker information across the session boundary.
Frequently Asked Questions
How does the Codex plugin persist session information between Claude Code restarts?
The plugin uses plugins/codex/scripts/lib/state.mjs to maintain a persistent JSON store of job metadata—including process IDs, session IDs, and status—on disk. When Claude Code restarts and triggers the SessionStart hook, the plugin reads this state to reconnect with existing broker sessions or clean up stale jobs from previous sessions.
What happens if the SessionEnd hook fails to terminate a background job?
The terminateProcessTree function in plugins/codex/scripts/lib/process.mjs wraps process termination in a try-catch block that silently ignores teardown failures. While the hook attempts to kill all running jobs marked as "queued" or "running", exceptions during termination do not block the session closure, ensuring Claude Code can exit even if a process refuses to die immediately.
Can I use the review gate without manually enabling it every session?
No, the review gate is opt-in per session. You must execute /codex:setup --enable-review-gate after starting Claude Code to activate the Stop hook interception. This flag is session-scoped; once the session ends, the review gate deactivates until explicitly enabled again in a new session.
What is the difference between the Stop hook and manually canceling a job?
The Stop hook (stop-review-gate-hook.mjs) runs automatically before Claude Code completes a turn, reviewing the assistant's output to decide whether to allow the session to continue. Manual cancellation (/codex:cancel) explicitly terminates a specific background job during the session. The SessionEnd hook handles automatic cleanup of both scenarios when the Claude Code session closes.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →