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

> Understand how the Codex plugin communicates with the Codex app server using the JSON-RPC protocol via STDIN/STDOUT pipes or Unix domain sockets.

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

---

**The Codex plugin communicates with the Codex app server using a JSON-RPC protocol over either a direct STDIN/STDOUT pipe or a Unix domain socket via a broker, implemented in `app-server.mjs` and orchestrated through high-level helpers in `codex.mjs`.**

The open-source `openai/codex-plugin-cc` repository enables Claude Code to interact with the Codex CLI through a sophisticated communication layer. Understanding how the Codex plugin communicates with the Codex app server is essential for developers extending or debugging the integration. The architecture relies on three distinct layers: transport selection, JSON-RPC message handling, and high-level API abstraction.

## Transport Selection: Direct vs. Broker Mode

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

**Direct mode** spawns the `codex app-server` binary as a child process and communicates over STDIN/STDOUT pipes. This is implemented by the `SpawnedCodexAppServerClient` class.

**Broker mode** connects to a Unix domain socket managed by a persistent broker process. This uses the `BrokerCodexAppServerClient` class and is triggered when the `CODEX_COMPANION_APP_SERVER_ENDPOINT` environment variable (referenced internally as `BROKER_ENDPOINT_ENV`) is set.

```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;
}

```

The broker approach allows multiple Claude Code sessions to share a single Codex app-server instance, while direct mode provides isolation per invocation.

## JSON-RPC Protocol Implementation

Once the transport is established, both client subclasses inherit from `AppServerClientBase` to handle the **JSON-RPC protocol** encoding and decoding. This base class manages request/response correlation and notification dispatching.

**Request handling** occurs through the `request(method, params)` method, which generates a unique ID, stores a `{resolve, reject}` pair in `this.pending`, and sends the JSON line to the transport.

**Notification handling** uses `setNotificationHandler()` to register callbacks for async messages like `item/*` and `turn/*` events.

**Line buffering and parsing** is handled by `handleChunk()` and `handleLine()` (lines 107-155), which split incoming data on newlines and parse JSON. Error handling and graceful shutdown are managed by `handleExit()` (lines 63-76), which resolves or rejects all pending promises when the connection closes.

Transport-specific sending is abstracted through the `sendMessage(message)` method, implemented differently in each subclass:
- `SpawnedCodexAppServerClient.sendMessage()` (lines 68-75) writes to STDIN
- `BrokerCodexAppServerClient.sendMessage()` (lines 26-33) writes to the socket

## High-Level API for Thread and Turn Management

The file `plugins/codex/scripts/lib/codex.mjs` builds on the JSON-RPC client to expose convenient helpers for the plugin commands. These functions manage the lifecycle of threads and turns.

**`withAppServer()`** (lines 13-42) opens a client via `CodexAppServerClient.connect()`, executes the provided async function, and ensures the client closes properly afterward.

**`startThread()`** (lines 332-348) sends the `"thread/start"` RPC and optionally `"thread/name/set"` to initialize a new conversation context.

**`captureTurn()`** (lines 559-611) installs a temporary notification handler that filters messages for a specific turn, aggregates them into a `TurnCaptureState`, and resolves when receiving `"turn/completed"`. It forwards unrelated notifications to the previous handler to maintain plugin functionality.

### Running a Review

The `runAppServerReview()` function demonstrates the complete communication flow:

```javascript
// 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),
    };
  });
}

```

**`runAppServerTurn()`** follows a similar pattern but decides whether to resume an existing thread or start fresh, then sends `"turn/start"` with the user prompt. **`interruptAppServerTurn()`** provides a fire-and-forget `"turn/interrupt"` RPC for cancellation commands.

## Practical Communication Examples

### Direct Spawn Without Broker

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

async function demoDirect(cwd) {
  // Force direct mode – no broker endpoint
  const client = await CodexAppServerClient.connect(cwd, { disableBroker: true });
  await client.initialize();               // starts `codex app-server`
  const info = await client.request("account/read", {});
  console.log("Account:", info);
  await client.close();                    // gracefully shuts down the child process
}

```

### Connecting to an Existing Broker

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

async function demoBroker(cwd) {
  // Assumes CODEX_COMPANION_APP_SERVER_ENDPOINT is set
  const client = await CodexAppServerClient.connect(cwd);
  const status = await client.request("status", {});
  console.log("Broker status:", status);
  await client.close(); // closes socket; broker stays alive
}

```

### Capturing Turn Output

```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 }],
      })
    );

    console.log("Final message:", turnState.lastAgentMessage);
    return turnState;
  });
}

```

## Summary

- The plugin selects between **direct spawn** (STDIN/STDOUT) and **broker mode** (Unix domain socket) via `CodexAppServerClient.connect()` in `plugins/codex/scripts/lib/app-server.mjs`.
- All communication uses **JSON-RPC** with one JSON line per message, handled by the `AppServerClientBase` class.
- **Request/response correlation** and **notification dispatching** are managed through internal maps and handler chains.
- High-level operations in `plugins/codex/scripts/lib/codex.mjs` abstract thread creation, review execution, and turn management into reusable async functions.
- The architecture supports both isolated per-command execution and shared broker sessions for multi-process workflows.

## Frequently Asked Questions

### What protocol does the Codex plugin use to communicate with the app server?

The Codex plugin uses **JSON-RPC** (JSON Remote Procedure Call) over line-delimited JSON streams. Each request, response, and notification is encoded as a single JSON line sent either through STDIN/STDOUT pipes (direct mode) or a Unix domain socket (broker mode). This protocol is implemented in `plugins/codex/scripts/lib/app-server.mjs`.

### What is the difference between broker mode and direct mode?

**Direct mode** spawns a new `codex app-server` process for each connection and communicates via STDIN/STDOUT, providing process isolation. **Broker mode** connects to a persistent Unix domain socket managed by a separate broker process, allowing multiple Claude Code sessions to share one app-server instance. The broker endpoint is controlled by the `CODEX_COMPANION_APP_SERVER_ENDPOINT` environment variable.

### How does the plugin handle asynchronous notifications from the app server?

The `AppServerClientBase` class in `app-server.mjs` provides `setNotificationHandler()` to register callbacks for async events like `item/*` and `turn/*`. The `captureTurn()` function in `codex.mjs` temporarily intercepts these notifications to aggregate turn-specific data while forwarding unrelated messages to maintain global plugin state.

### How can I interrupt a running operation in the Codex app server?

The plugin exposes `interruptAppServerTurn()`, which sends a fire-and-forget `"turn/interrupt"` JSON-RPC request to the app server. This method is invoked by the `/codex:cancel` command and does not wait for a response, immediately signaling the server to stop the current turn.