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

> Understand how the Codex plugin communicates with its app server using JSON-RPC. Explore STDIN/STDOUT pipes and Unix domain sockets for thread management and notifications.

- Repository: [OpenAI/codex-plugin-cc](https://github.com/openai/codex-plugin-cc)
- Tags: architecture
- Published: 2026-07-05

---

**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.

```js
// 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:

```js
// 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`.