# How the Codex Plugin Integrates with the Codex App Server Protocol: A Complete Technical Guide

> Learn how the Codex plugin integrates with the Codex app server protocol via JSON-RPC for seamless thread management, review execution, and turn-based output capture. Explore technical details.

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

---

**The Codex plugin establishes a JSON-RPC connection to the `codex app-server` binary using either direct STDIN/STDOUT pipes or a Unix domain socket broker, then exposes high-level helpers like `runAppServerReview` and `runAppServerTurn` to manage threads, execute reviews, and capture turn-based outputs.**

The `openai/codex-plugin-cc` repository implements a Claude Code plugin that communicates with the local Codex CLI through a structured protocol. Understanding how the Codex plugin integrates with the Codex app server protocol reveals a three-layer architecture: transport selection, JSON-RPC message handling, and high-level orchestration APIs that enable seamless code reviews and task delegation.

## Transport Selection: Direct Spawn vs. Broker Connection

The integration begins in `plugins/codex/scripts/lib/app-server.mjs` with the `CodexAppServerClient.connect` method. This static factory function determines whether to spawn a fresh `codex app-server` process or connect to an existing broker endpoint.

### Direct Mode: STDIN/STDOUT Communication

When no broker endpoint is available and `disableBroker` is set to `true`, the client instantiates `SpawnedCodexAppServerClient`. This subclass spawns the binary directly and communicates over STDIN/STDOUT pipes.

```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;
  }
  const client = brokerEndpoint
      ? new BrokerCodexAppServerClient(cwd, { …options, brokerEndpoint })
      : new SpawnedCodexAppServerClient(cwd, options);
  await client.initialize();
  return client;
}

```

### Broker Mode: Unix Domain Socket Reuse

If the environment variable `CODEX_COMPANION_APP_SERVER_ENDPOINT` is defined, or if `reuseExistingBroker` locates a saved session, the client creates a `BrokerCodexAppServerClient`. This maintains a persistent connection through a Unix domain socket, allowing multiple Claude sessions to share a single app-server instance.

```javascript
// plugins/codex/scripts/lib/app-server.mjs
// Within connect() method:
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;
}

```

## JSON-RPC Protocol Implementation

Both transport modes inherit from `AppServerClientBase`, which implements the complete JSON-RPC 2.0 protocol in `app-server.mjs`. This base class manages request lifecycle, notification routing, and graceful shutdown.

### Request and Response Handling

The `request(method, params)` method generates unique request IDs, stores Promise resolvers in a `pending` map, and sends JSON-encoded messages.

```javascript
// plugins/codex/scripts/lib/app-server.mjs
request(method, params) {
  const id = ++this.requestId;
  return new Promise((resolve, reject) => {
    this.pending.set(id, { resolve, reject });
    this.sendMessage({ jsonrpc: "2.0", id, method, params });
  });
}

```

### Notification Routing

Asynchronous server events flow through `setNotificationHandler`, which allows callers to register callbacks for server-side notifications like `item/*` and `turn/*` events.

```javascript
// plugins/codex/scripts/lib/app-server.mjs
setNotificationHandler(handler) {
  this.notificationHandler = handler;
}

```

### Transport-Specific Message Delivery

Subclasses implement `sendMessage(message)` according to their transport:

- **SpawnedCodexAppServerClient**: Writes to `child.stdin` with newline termination
- **BrokerCodexAppServerClient**: Writes to the socket connection

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

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

```

## High-Level API for Reviews and Turns

The `plugins/codex/scripts/lib/codex.mjs` file builds on the JSON-RPC client to provide the public API used by slash commands. These functions handle thread lifecycle, progress emission, and notification aggregation.

### Executing Code Reviews with `runAppServerReview`

The `runAppServerReview` function establishes a connection, starts an ephemeral read-only thread, and initiates a review via the `review/start` RPC method.

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

```

### Capturing Turn State and Notifications

The `captureTurn` function (lines 559–611) installs a temporary notification handler that filters messages for a specific turn ID, aggregates them into a `TurnCaptureState` object, and resolves when the server emits `turn/completed`.

```javascript
// plugins/codex/scripts/lib/codex.mjs
async function captureTurn(client, threadId, startTurn, options) {
  return new Promise((resolve, reject) => {
    const state = { threadId, turnId: null, messages: [] };
    
    const handler = (notification) => {
      if (notification.method === "turn/completed") {
        resolve(state);
      } else if (notification.method.startsWith("item/")) {
        state.messages.push(notification.params);
      }
    };
    
    client.setNotificationHandler(handler);
    startTurn().catch(reject);
  });
}

```

### Interrupting Active Operations

For canceling in-progress work, the `interruptAppServerTurn` function sends a fire-and-forget `turn/interrupt` RPC:

```javascript
// plugins/codex/scripts/lib/codex.mjs
export async function interruptAppServerTurn(client, threadId, turnId) {
  await client.request("turn/interrupt", { threadId, turnId });
}

```

## Implementation Examples

### Direct Spawn Without Broker

Force a fresh process spawn when you need isolated execution:

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

async function directSpawnExample(cwd) {
  const client = await CodexAppServerClient.connect(cwd, { 
    disableBroker: true 
  });
  await client.initialize();
  
  const account = await client.request("account/read", {});
  console.log("Account:", account);
  
  await client.close(); // Terminates the child process
}

```

### Connecting to an Existing Broker

Reuse a persistent broker session for better performance across multiple operations:

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

async function brokerExample(cwd) {
  // Reads CODEX_COMPANION_APP_SERVER_ENDPOINT env var automatically
  const client = await CodexAppServerClient.connect(cwd);
  
  const status = await client.request("status", {});
  console.log("Broker status:", status);
  
  await client.close(); // Closes socket only; broker remains running
}

```

### Custom Turn Implementation

Implement a custom task turn using the low-level primitives:

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

async function customTask(cwd, userPrompt) {
  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: userPrompt }],
      })
    );

    return turnState.lastAgentMessage;
  });
}

```

## Summary

- **Transport Flexibility**: The `CodexAppServerClient.connect` factory selects between `SpawnedCodexAppServerClient` (direct STDIN/STDOUT) and `BrokerCodexAppServerClient` (Unix socket) based on the `CODEX_COMPANION_APP_SERVER_ENDPOINT` environment variable and options.
- **JSON-RPC Foundation**: `AppServerClientBase` provides robust request/response tracking, notification handling, and line-buffered JSON parsing over both transport types.
- **High-Level Orchestration**: Functions like `runAppServerReview`, `runAppServerTurn`, and `captureTurn` in `codex.mjs` manage thread lifecycle, aggregate streaming notifications, and expose clean JavaScript APIs to the plugin's slash commands.
- **Resource Management**: The `withAppServer` helper ensures clients are properly closed after operations, while broker mode allows long-running shared sessions across multiple Claude Code instances.

## Frequently Asked Questions

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

**Direct mode** spawns a new `codex app-server` process for each connection using STDIN/STDOUT pipes, providing complete isolation but higher startup overhead. **Broker mode** connects to a persistent Unix domain socket, allowing multiple Claude sessions to share a single app-server instance for faster response times and reduced resource usage.

### How does the plugin handle JSON-RPC message parsing?

The `AppServerClientBase` class implements line-buffered parsing in `handleChunk` and `handleLine` methods (lines 107–155 of `app-server.mjs`). It splits incoming data on newlines, parses each line as JSON, and routes messages to either pending request promises or the registered notification handler based on the presence of an `id` field.

### Can I interrupt a running Codex review or task?

Yes. The `interruptAppServerTurn` function in `codex.mjs` sends a `turn/interrupt` RPC request to the app server. This is invoked by the `/codex:cancel` command and immediately terminates the active turn without closing the underlying thread or transport connection.

### What RPC methods does the app server expose to the plugin?

The plugin utilizes several JSON-RPC methods including `thread/start` for session creation, `review/start` for code analysis, `turn/start` for generic task execution, `turn/interrupt` for cancellation, and `account/read` for authentication status. All methods follow the JSON-RPC 2.0 specification with request IDs and structured parameters.