How the Codex Plugin Interacts with the Codex App Server: JSON-RPC Architecture and Transport Layer

The Codex plugin communicates with the Codex app server via a JSON-RPC protocol transmitted over either a direct STDIN/STDOUT pipe or a Unix domain socket broker connection, enabling seamless thread management and real-time notification streaming.

The openai/codex-plugin-cc repository implements a robust three-layer communication stack that bridges Claude Code with the local Codex CLI installation. Understanding how the plugin interacts with the Codex app server reveals a sophisticated transport abstraction that supports both ephemeral process spawning and persistent broker sessions. This architecture allows the plugin to start review threads, delegate coding tasks, and capture streaming output through a unified JavaScript API.

Transport Selection: Direct Spawn vs. Broker Connection

The entry point for all communication is CodexAppServerClient.connect() in plugins/codex/scripts/lib/app-server.mjs. This static method determines whether to spawn a fresh app-server process or connect to an existing broker endpoint based on environment configuration and runtime options.

Broker Mode (Unix Domain Socket)

When the environment variable CODEX_COMPANION_APP_SERVER_ENDPOINT (referenced internally as BROKER_ENDPOINT_ENV) is present, the client instantiates BrokerCodexAppServerClient. This subclass establishes a connection via net.createConnection() to a Unix domain socket, enabling multiple Claude sessions to share a single, long-running Codex app-server instance. The broker persists across plugin invocations, reducing startup overhead and maintaining state between commands.

Direct Mode (STDIN/STDOUT)

If no broker endpoint is configured and broker reuse is disabled, the client creates a SpawnedCodexAppServerClient that spawns the codex app-server binary directly using Node.js spawn(). This mode creates an isolated process that communicates over STDIN/STDOUT pipes, terminating when the client closes the connection. Direct mode ensures functionality in environments without a running broker but incurs the overhead of process initialization for each operation.

// plugins/codex/scripts/lib/app-server.mjs
static async connect(cwd, options = {}) {
  let brokerEndpoint = null;
  if (!options.disableBroker) {
    brokerEndpoint = options.brokerEndpoint ??
        options.env?.[BROKER_ENDPOINT_ENV] ??
        process.env[BROKER_ENDPOINT_ENV] ??
        null;
    if (!brokerEndpoint && options.reuseExistingBroker) {
      brokerEndpoint = loadBrokerSession(cwd)?.endpoint ?? null;
    }
    if (!brokerEndpoint && !options.reuseExistingBroker) {
      const brokerSession = await ensureBrokerSession(cwd, { env: options.env });
      brokerEndpoint = brokerSession?.endpoint ?? null;
    }
  }
  const client = brokerEndpoint
      ? new BrokerCodexAppServerClient(cwd, { …options, brokerEndpoint })
      : new SpawnedCodexAppServerClient(cwd, options);
  await client.initialize();
  return client;
}

JSON-RPC Client Implementation

Both transport implementations inherit from AppServerClientBase, which provides the core JSON-RPC protocol handling in plugins/codex/scripts/lib/app-server.mjs. This base class manages request lifecycle, notification routing, and graceful shutdown procedures.

Request-Response Handling

The request(method, params) method (lines 86-98) generates unique request IDs, stores pending promise resolvers in a Map, and transmits JSON-RPC formatted messages via the transport-specific sendMessage() implementation. When responses arrive, the client matches them to pending requests by ID and resolves the associated promises.

Notification Streaming

Asynchronous events from the app-server—such as item/* and turn/* messages—are handled through setNotificationHandler() (lines 76-78). The handleChunk() and handleLine() methods (lines 107-155) implement newline-delimited JSON parsing, buffering incoming data until complete messages are received and dispatched to registered handlers.

Transport-Specific Send Methods

Subclasses implement sendMessage() according to their transport mechanism. SpawnedCodexAppServerClient.sendMessage() (lines 68-75) writes to the child process STDIN, while BrokerCodexAppServerClient.sendMessage() (lines 26-33) transmits over the Unix socket. Both implementations ensure atomic message delivery with proper newline termination required by the JSON-RPC line protocol.

High-Level API Operations

The plugins/codex/scripts/lib/codex.mjs file exposes developer-friendly wrappers that orchestrate complex multi-step interactions with the app server. These functions handle thread initialization, turn execution, and result aggregation.

Thread and Turn Management

The withAppServer() helper (lines 13-42) manages client lifecycle, automatically connecting and cleaning up resources after the provided async function completes. Within this context, startThread() (lines 332-348) sends the thread/start RPC and optionally sets thread names via thread/name/set.

Capturing Review Results

The runAppServerReview() function demonstrates the complete interaction flow:

// plugins/codex/scripts/lib/codex.mjs
export async function runAppServerReview(cwd, options = {}) {
  const availability = getCodexAvailability(cwd);
  if (!availability.available) {
    throw new Error("Codex CLI is not installed …");
  }

  return withAppServer(cwd, async (client) => {
    emitProgress(options.onProgress, "Starting Codex review thread.", "starting");
    const thread = await startThread(client, cwd, {
      model: options.model,
      sandbox: "read-only",
      ephemeral: true,
      threadName: options.threadName,
    });
    const sourceThreadId = thread.thread.id;
    
    const turnState = await captureTurn(
      client,
      sourceThreadId,
      () =>
        client.request("review/start", {
          threadId: sourceThreadId,
          delivery,
          target: options.target,
        }),
      { onProgress: options.onProgress, … }
    );

    return {
      status: buildResultStatus(turnState),
      threadId: turnState.threadId,
      sourceThreadId,
      turnId: turnState.turnId,
      reviewText: turnState.reviewText,
      reasoningSummary: turnState.reasoningSummary,
      stderr: cleanCodexStderr(client.stderr),
    };
  });
}

The captureTurn() function (lines 559-611) installs a temporary notification handler that filters messages belonging to the specific turn ID, aggregating streaming output into a TurnCaptureState object. It resolves when receiving the turn/completed notification, ensuring all file changes, reasoning summaries, and agent messages are collected before returning control to the caller.

Generic Task Execution

runAppServerTurn() follows a similar pattern but first checks for existing threads to resume before starting fresh turns via the turn/start RPC. For immediate cancellation, interruptAppServerTurn() sends the turn/interrupt notification as a fire-and-forget message, enabling the /codex:cancel command to halt running operations.

Plugin Command Integration

Each slash-command defined in plugins/codex/commands/*.md imports these high-level helpers to implement user-facing functionality. The review command invokes runAppServerReview(), while status commands query session state through getSessionRuntimeStatus(). Because all commands utilize the same JSON-RPC client abstraction, they function identically regardless of whether the underlying transport uses a direct spawn or broker connection.

Summary

  • The plugin selects between direct spawn (STDIN/STDOUT) and broker connection (Unix domain socket) transport modes via CodexAppServerClient.connect() in app-server.mjs.
  • JSON-RPC messaging is handled by AppServerClientBase, which manages request IDs, notification handlers, and line-buffered JSON parsing.
  • High-level operations in codex.mjs provide thread lifecycle management and turn capture capabilities that aggregate streaming results into structured return values.
  • The architecture supports both ephemeral process isolation and persistent broker sessions, enabling flexibility across different workflow requirements.

Frequently Asked Questions

What transport protocols does the Codex plugin use to communicate with the app server?

The plugin uses JSON-RPC over two transport mechanisms: direct STDIN/STDOUT pipes when spawning the codex app-server binary directly, or Unix domain sockets when connecting to a broker endpoint. The transport is selected automatically based on the presence of the CODEX_COMPANION_APP_SERVER_ENDPOINT environment variable and the disableBroker option passed to CodexAppServerClient.connect().

How does the plugin handle real-time notifications from the Codex app server?

The AppServerClientBase class implements notification handling through setNotificationHandler(), which registers callbacks for asynchronous events like item/* and turn/* messages. During active turns, the captureTurn() function temporarily intercepts these notifications to aggregate streaming output, file changes, and status updates while forwarding unrelated messages to maintain global plugin functionality.

What is the difference between broker mode and direct spawn mode in the Codex plugin?

Broker mode creates a BrokerCodexAppServerClient that connects to a persistent Unix socket, allowing multiple Claude sessions to share a single app-server instance and maintain state across commands. Direct spawn mode creates a SpawnedCodexAppServerClient that launches a fresh codex app-server process for each operation, providing isolation at the cost of startup overhead. Broker mode is preferred for performance, while direct mode ensures compatibility when no broker is available.

How does the plugin manage Codex threads and turns programmatically?

The plugin uses the withAppServer() helper to manage client connections, startThread() to initialize new conversations via the thread/start RPC, and captureTurn() to execute and monitor specific tasks. These functions handle the complexity of request-response pairing, notification filtering, and result aggregation, exposing a simple JavaScript API that returns structured data including reviewText, reasoningSummary, and threadId.

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 →