# How the Codex Plugin Communicates with the Codex App Server: JSON-RPC Architecture Explained

> Discover how the Codex plugin communicates with the Codex App Server using JSON-RPC. Learn about direct process spawning and Unix domain socket connections.

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

---

**The Codex plugin communicates with the Codex App Server via a JSON-RPC protocol over line-delimited JSON, selecting between direct process spawning (STDIN/STDOUT) or Unix domain socket connections (broker mode) through the `CodexAppServerClient.connect` method in `app-server.mjs`.**

The openai/codex-plugin-cc repository implements a Claude Code plugin that delegates code reviews and asynchronous tasks to the Codex CLI. The communication architecture relies on a three-layer stack—transport selection, JSON-RPC client management, and high-level thread orchestration—to enable seamless interaction between the plugin and the `codex app-server` binary.

## Transport Layer: Direct Spawn vs. Broker Mode

The entry point `CodexAppServerClient.connect` in `plugins/codex/scripts/lib/app-server.mjs` determines whether to spawn a fresh process or connect to an existing broker based on environment configuration and runtime options.

### Broker Mode (Unix Domain Sockets)

When the `CODEX_COMPANION_APP_SERVER_ENDPOINT` environment variable (referenced internally as `BROKER_ENDPOINT_ENV`) is set, or when `options.reuseExistingBroker` triggers session lookup via `loadBrokerSession`, the client instantiates `BrokerCodexAppServerClient`. This subclass establishes a persistent connection using `net.createConnection` to a Unix domain socket, allowing multiple Claude Code sessions to share a single `codex app-server` process and its state.

### Direct Mode (Process Spawning)

If no broker endpoint is available and `options.disableBroker` is not set, the client creates a `SpawnedCodexAppServerClient`. This spawns the `codex app-server` binary directly using `spawn("codex", ["app-server"], …)` and communicates over the resulting STDIN/STDOUT pipes. This mode creates an isolated, per-invocation process that terminates when the plugin closes the connection.

```javascript
// 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 Protocol Implementation

Both transport subclasses inherit from `AppServerClientBase`, which implements the complete JSON-RPC 2.0 protocol handling for the Codex plugin communication.

### Request-Response Management

The `request(method, params)` method (lines 86–98) generates unique request IDs, stores promise resolution handlers in `this.pending`, and transmits JSON-encoded messages through the transport-specific `sendMessage` implementation. This ensures synchronous-style API usage despite the underlying asynchronous message passing.

### Notification Streaming

The `setNotificationHandler` method (lines 76–78) registers callbacks for asynchronous server events such as `item/*` and `turn/*` notifications. The `handleChunk` and `handleLine` methods (lines 107–155) buffer incoming data, split on newlines, and parse JSON to dispatch responses to pending requests or notifications to the registered handler.

### Error Handling and Lifecycle

The `handleExit` method (lines 63–76) gracefully resolves or rejects all pending promises when the transport closes, preventing memory leaks and ensuring clean shutdowns whether the connection terminates via socket closure or process exit.

```javascript
// SpawnedCodexAppServerClient.sendMessage (lines 68–75)
async sendMessage(message) {
  if (!this.process) throw new Error("Process not started");
  this.process.stdin.write(JSON.stringify(message) + "\n");
}

// BrokerCodexAppServerClient.sendMessage (lines 26–33)
async sendMessage(message) {
  if (!this.socket) throw new Error("Socket not connected");
  this.socket.write(JSON.stringify(message) + "\n");
}

```

## High-Level Thread Operations in codex.mjs

The file `plugins/codex/scripts/lib/codex.mjs` exposes the public API used by slash commands, abstracting the JSON-RPC complexity into semantic operations like `runAppServerReview` and `runAppServerTurn`.

### Running Code Reviews

The `runAppServerReview` function demonstrates the complete communication flow:

1. **Connection Management**: Uses `withAppServer` (lines 13–42) to acquire a client via `CodexAppServerClient.connect`, execute operations, and ensure cleanup.
2. **Thread Initialization**: Calls `startThread` (lines 332–348) to send `thread/start` RPC and optionally `thread/name/set`.
3. **Turn Capture**: Invokes `captureTurn` (lines 559–611) to install a temporary notification handler that aggregates messages until receiving `turn/completed`.

```javascript
// plugins/codex/scripts/lib/codex.mjs
export async function runAppServerReview(cwd, options = {}) {
  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: options.delivery,
          target: options.target,
        }),
      { onProgress: options.onProgress }
    );

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

```

### Task Execution and Interruption

The `runAppServerTurn` function manages generic task execution, deciding whether to resume existing threads or start new ones before sending `turn/start` RPC. For immediate cancellation, `interruptAppServerTurn` sends a fire-and-forget `turn/interrupt` request to halt active processing.

## Practical Communication Examples

### Direct Spawn Without Broker

```javascript
import { CodexAppServerClient } from "./app-server.mjs";

async function demoDirect(cwd) {
  // Force direct mode – spawns new process
  const client = await CodexAppServerClient.connect(cwd, { disableBroker: true });
  await client.initialize();
  const info = await client.request("account/read", {});
  console.log("Account:", info);
  await client.close(); // Terminates the spawned process
}

```

### Connecting to Existing Broker

```javascript
import { CodexAppServerClient } from "./app-server.mjs";

async function demoBroker(cwd) {
  // Connects to broker via Unix socket
  const client = await CodexAppServerClient.connect(cwd);
  const status = await client.request("status", {});
  console.log("Broker status:", status);
  await client.close(); // Closes socket only; broker persists
}

```

### Capturing Streaming Results

```javascript
import { withAppServer, captureTurn } from "./codex.mjs";

async function demoTurn(cwd, prompt) {
  return withAppServer(cwd, async (client) => {
    const thread = await client.request("thread/start", { cwd });
    const threadId = thread.thread.id;

    const turnState = await captureTurn(
      client,
      threadId,
      () => client.request("turn/start", {
        threadId,
        input: [{ type: "text", text: prompt }],
      })
    );

    return turnState.reviewText || turnState.lastAgentMessage;
  });
}

```

## Summary

- **Transport Flexibility**: The `CodexAppServerClient.connect` method selects between **direct mode** (STDIN/STDOUT via `SpawnedCodexAppServerClient`) and **broker mode** (Unix domain sockets via `BrokerCodexAppServerClient`) based on the `CODEX_COMPANION_APP_SERVER_ENDPOINT` environment variable.
- **JSON-RPC Protocol**: `AppServerClientBase` manages request IDs, promise resolution, notification streaming, and line-buffered JSON parsing in `plugins/codex/scripts/lib/app-server.mjs`.
- **High-Level Orchestration**: Functions like `runAppServerReview` and `runAppServerTurn` in `codex.mjs` handle thread lifecycle management, from `thread/start` initialization to `turn/completed` aggregation.
- **Resource Management**: The `withAppServer` helper ensures proper client initialization and cleanup, while `captureTurn` filters notifications to isolate specific operation results.

## Frequently Asked Questions

### What protocol does the Codex plugin use to communicate with the Codex App Server?

The Codex plugin uses **JSON-RPC 2.0** over line-delimited JSON streams. Each message is a single JSON object terminated by a newline, sent either over STDIN/STDOUT pipes (direct mode) or Unix domain sockets (broker mode) as implemented in `app-server.mjs`.

### How does the plugin decide between broker mode and direct spawn mode?

The `CodexAppServerClient.connect` method checks the `CODEX_COMPANION_APP_SERVER_ENDPOINT` environment variable first. If set, it connects to that Unix socket endpoint (broker mode). If unset and `disableBroker` is false, it spawns a fresh `codex app-server` process (direct mode). The `reuseExistingBroker` option can also trigger broker discovery from saved session files.

### Can multiple Claude Code sessions share the same Codex App Server?

Yes, when running in **broker mode**, multiple plugin instances can connect to the same Unix domain socket endpoint. This allows shared state and reduced startup overhead, whereas direct mode creates isolated per-session processes that terminate when the plugin closes the connection.

### How does the plugin handle streaming responses from long-running Codex operations?

The `captureTurn` function in `codex.mjs` installs a temporary notification handler that filters incoming JSON-RPC notifications (such as `item/added` or `turn/completed`) by turn ID. It aggregates these messages into a `TurnCaptureState` object, resolving only when the server sends the `turn/completed` notification, effectively converting the streaming protocol into a promise-based API.