How the Session Lifecycle Hook Manages Broker Endpoint Sharing in the Codex Plugin

The session lifecycle hook in openai/codex-plugin-cc centralizes broker endpoint management by persisting connection details to a JSON state file and falling back to environment variables, ensuring all Codex sessions within a workspace share a single JSON-RPC broker process.

The Codex plugin runs a persistent broker process (the app-server) that exposes a JSON-RPC endpoint for tool execution. The session lifecycle hook (session-lifecycle-hook.mjs) orchestrates how multiple editor sessions discover, share, and gracefully shut down this broker. By leveraging a state-persistence pattern, the hook guarantees that concurrent sessions reuse the same endpoint while providing deterministic cleanup when the last session terminates.

Broker Endpoint Persistence Architecture

The foundation of endpoint sharing relies on a state file that outlives individual process instances.

Storing the Broker Session

When the broker starts, ensureBrokerSession() in plugins/codex/scripts/lib/broker-lifecycle.mjs generates a platform-specific endpoint via createBrokerEndpoint()—using unix: sockets on POSIX or named pipes (pipe:\\.\pipe\...) on Windows—and persists the session metadata to disk.

The saveBrokerSession() function writes a JSON file named broker.json inside the plugin’s state directory:

// broker-lifecycle.mjs
export function saveBrokerSession(cwd, session) {
  const stateDir = resolveStateDir(cwd);
  fs.mkdirSync(stateDir, { recursive: true });
  fs.writeFileSync(
    path.join(stateDir, "broker.json"),
    `${JSON.stringify(session, null, 2)}\n`,
    "utf8"
  );
}

This file contains the endpoint URL, PID file path, and log file location, making the broker discoverable by any subsequent process in the same workspace.

Retrieving the Shared Endpoint

Any process can locate the active broker by calling loadBrokerSession(), which reads the same broker.json file:

// broker-lifecycle.mjs
export function loadBrokerSession(cwd) {
  const stateFile = resolveBrokerStateFile(cwd);
  if (!fs.existsSync(stateFile)) return null;
  return JSON.parse(fs.readFileSync(stateFile, "utf8"));
}

If the file exists, the function returns the session object; otherwise, it returns null, signaling that a new broker must be started.

Session Lifecycle Hook Integration

The session-lifecycle-hook.mjs script acts as the gatekeeper for session startup and teardown, ensuring the broker endpoint is available to every Codex session.

Endpoint Resolution Strategy

When handling a SessionEnd event (or any lifecycle transition), the hook resolves the broker location through a prioritized lookup:

  1. State file first: It calls loadBrokerSession(cwd) to read the persisted broker.json.
  2. Environment fallback: If the state file is missing, it checks for CODEX_COMPANION_APP_SERVER_ENDPOINT, CODEX_COMPANION_APP_SERVER_PID_FILE, and CODEX_COMPANION_APP_SERVER_LOG_FILE environment variables. This allows parent processes to inject endpoint details without writing to disk.
  3. Null safety: If neither source provides data, the hook proceeds with a null session, skipping broker-related cleanup.
// session-lifecycle-hook.mjs
const brokerSession = loadBrokerSession(cwd) ??
  (process.env[BROKER_ENDPOINT_ENV] ? {
      endpoint: process.env[BROKER_ENDPOINT_ENV],
      pidFile: process.env[PID_FILE_ENV] ?? null,
      logFile: process.env[LOG_FILE_ENV] ?? null
    } : null);

Graceful Shutdown Sequence

During SessionEnd, the hook performs a coordinated teardown:

  • JSON-RPC shutdown: It sends a broker/shutdown request to brokerSession.endpoint via sendBrokerShutdown().
  • Process cleanup: It invokes teardownBrokerSession() to terminate the broker process and delete the PID file.
  • State removal: It calls clearBrokerSession() to unlink broker.json and remove socket/pipe files.
  • Job cleanup: It executes cleanupSessionJobs() to terminate any dangling child processes associated with the terminating session.

Cross-Session Endpoint Sharing Mechanism

The design ensures single-broker-per-workspace semantics while maintaining cross-process visibility.

Singleton Broker Enforcement

When a new session starts, ensureBrokerSession() checks for an existing valid endpoint by calling loadBrokerSession() followed by waitForBrokerEndpoint(). If the endpoint is reachable, the existing session object is returned immediately, preventing duplicate broker processes. This guarantees that all active Codex sessions share the same JSON-RPC endpoint for tool execution.

Environment Variable Propagation

To support nested process trees, the broker lifecycle exports critical paths via environment variables defined in plugins/codex/scripts/lib/app-server.mjs:

  • CODEX_COMPANION_APP_SERVER_ENDPOINT
  • CODEX_COMPANION_APP_SERVER_PID_FILE
  • CODEX_COMPANION_APP_SERVER_LOG_FILE

Child processes spawned by the hook inherit these variables, enabling them to locate the broker even if the broker.json file has not yet been written or has been temporarily locked.

Implementation Example

The following script demonstrates how to invoke the lifecycle hook programmatically, ensuring the broker endpoint is shared across session boundaries:

// my-script.mjs
import { spawn } from "node:child_process";
import path from "node:path";

// Trigger SessionStart to initialize or connect to the shared broker
spawn(
  "node",
  [
    path.resolve("plugins/codex/scripts/session-lifecycle-hook.mjs"),
    "SessionStart"
  ],
  {
    env: {
      CODEX_COMPANION_SESSION_ID: "session-12345",
      CODEX_TRANSCRIPT_PATH: "/tmp/transcript.json"
    }
  }
);

When the session concludes, invoking the same script with the SessionEnd argument triggers the hook to locate the shared endpoint via loadBrokerSession(), issue the shutdown command, and purge the state files.

Summary

  • The session lifecycle hook persists broker connection details to broker.json via saveBrokerSession() in broker-lifecycle.mjs, enabling cross-process discovery.
  • Endpoint resolution follows a fallback chain: state file first, then environment variables (CODEX_COMPANION_APP_SERVER_ENDPOINT), ensuring robustness in varied execution contexts.
  • ensureBrokerSession() enforces a singleton broker per workspace by verifying endpoint reachability before spawning new processes.
  • During SessionEnd, the hook orchestrates graceful shutdown via sendBrokerShutdown(), teardownBrokerSession(), and cleanupSessionJobs().

Frequently Asked Questions

How does the session lifecycle hook locate the broker if the state file is deleted?

If broker.json is missing, the hook falls back to environment variables CODEX_COMPANION_APP_SERVER_ENDPOINT, CODEX_COMPANION_APP_SERVER_PID_FILE, and CODEX_COMPANION_APP_SERVER_LOG_FILE. This fallback mechanism allows parent processes to pass endpoint details directly to child sessions without relying on filesystem state.

What prevents multiple brokers from starting in the same workspace?

The ensureBrokerSession() function in broker-lifecycle.mjs calls loadBrokerSession() to check for an existing session file, then validates the endpoint with waitForBrokerEndpoint(). If the endpoint is responsive, it returns the existing session instead of spawning a new broker process, ensuring all sessions share a single JSON-RPC endpoint.

Which files are responsible for broker endpoint generation and cleanup?

broker-endpoint.mjs generates platform-specific endpoint strings (Unix sockets or Windows named pipes), while broker-lifecycle.mjs handles persistence via saveBrokerSession() and cleanup via clearBrokerSession(). The session-lifecycle-hook.mjs script coordinates these operations during session transitions.

How is the broker shut down when the last Codex session ends?

The hook's handleSessionEnd function retrieves the broker session, sends a JSON-RPC broker/shutdown request to the endpoint, then invokes teardownBrokerSession() to kill the process and delete the state file. It also runs cleanupSessionJobs() to terminate any remaining child processes associated with the session.

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 →