# How the Codex Plugin Interacts with the Codex App Server: Architecture and Implementation

> Understand how the Codex plugin communicates with the Codex app server via JSON-RPC using STDIN/STDOUT or Unix domain sockets. Explore the architecture and implementation details.

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

---

**The Codex plugin communicates with the Codex app server through a JSON‑RPC protocol that supports two transport modes—direct STDIN/STDOUT spawn or Unix domain socket broker connection—with all operations orchestrated through `CodexAppServerClient.connect` in `app-server.mjs`.**

The **openai/codex-plugin-cc** repository implements a Claude Code plugin that delegates code review and task execution to OpenAI's Codex CLI. Understanding how the Codex plugin interacts with the Codex app server reveals a layered architecture designed for flexibility: you can spawn ephemeral app-server processes or share a persistent broker across multiple Claude sessions. This article examines the transport selection, JSON‑RPC protocol, and high-level API that make this integration work.

---

## 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 which transport to use based on environment configuration and options.

```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;
    // Re‑use an existing broker session if requested …
    if (!brokerEndpoint && options.reuseExistingBroker) {
      brokerEndpoint = loadBrokerSession(cwd)?.endpoint ?? null;
    }
    // …or create a fresh broker session.
    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 method evaluates three sources for a broker endpoint:

- **`options.brokerEndpoint`** – explicit configuration passed by the caller
- **`options.env[BROKER_ENDPOINT_ENV]` or `process.env[BROKER_ENDPOINT_ENV]`** – the environment variable `CODEX_COMPANION_APP_SERVER_ENDPOINT`
- **`loadBrokerSession(cwd)`** – a persisted session file from a previous broker launch

If no broker endpoint is found and `disableBroker` is falsy, the code calls `ensureBrokerSession()` from `broker-lifecycle.mjs` to start a new broker. Otherwise, it falls back to direct mode.

### Direct Mode: `SpawnedCodexAppServerClient`

When `brokerEndpoint` is null, the plugin spawns a fresh `codex app-server` process:

```javascript
// SpawnedCodexAppServerClient implementation (app-server.mjs lines 68-75)
sendMessage(message) {
  if (!this.childProcess) throw new Error("Child process not available");
  this.childProcess.stdin.write(JSON.stringify(message) + "\n");
}

```

This client writes JSON‑RPC messages to the child process's **STDIN** and reads responses from **STDOUT**. The process terminates when `client.close()` is called, making this mode suitable for one-off operations.

### Broker Mode: `BrokerCodexAppServerClient`

When a broker endpoint is available, the plugin connects via **Unix domain socket**:

```javascript
// BrokerCodexAppServerClient implementation (app-server.mjs lines 26-33)
sendMessage(message) {
  if (!this.socket) throw new Error("Socket not connected");
  this.socket.write(JSON.stringify(message) + "\n");
}

```

The broker persists across multiple plugin invocations, enabling session reuse and background task management. Closing this client only terminates the socket—the broker process continues running.

---

## JSON‑RPC Client Implementation: `AppServerClientBase`

Both transport subclasses inherit from `AppServerClientBase`, which implements the protocol logic in `plugins/codex/scripts/lib/app-server.mjs`:

| Feature | Implementation Details |
|---------|----------------------|
| **Request/Response correlation** | `request(method, params)` generates unique IDs, stores `{resolve, reject}` pairs in `this.pending`, and returns a Promise (lines 86-98) |
| **Notification handling** | `setNotificationHandler(callback)` registers async message receivers for events like `item/*` and `turn/*` (lines 76-78) |
| **Line-delimited JSON parsing** | `handleChunk` buffers incoming data, `handleLine` splits on newlines and parses JSON (lines 107-155) |
| **Graceful shutdown** | `handleExit` resolves or rejects all pending promises and records exit errors (lines 63-76) |
| **Transport abstraction** | Subclasses implement `sendMessage(message)` for their specific channel |

The base class enforces **one JSON line per message**, the standard for JSON‑RPC over streams. This design keeps the protocol simple while supporting both synchronous request-response cycles and asynchronous server-pushed notifications.

---

## High-Level API: Operations in `codex.mjs`

The file `plugins/codex/scripts/lib/codex.mjs` builds convenient helpers on top of the JSON‑RPC client. These functions are what the slash-commands actually invoke.

### Running a Code Review: `runAppServerReview`

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

```

This function demonstrates the standard pattern:

1. **`withAppServer`** (lines 13-42) – acquires a client via `CodexAppServerClient.connect`, executes the callback, then guarantees cleanup
2. **`startThread`** (lines 332-348) – sends `"thread/start"` RPC with configuration, optionally followed by `"thread/name/set"`
3. **`captureTurn`** (lines 559-611) – the most complex helper: installs a temporary notification filter, aggregates messages into `TurnCaptureState`, and resolves when `"turn/completed"` arrives

The `captureTurn` helper is particularly important—it ensures that concurrent turns don't interfere by routing only matching notifications to the active capture while forwarding others to any previously installed handler.

### Running a Generic Turn: `runAppServerTurn`

Similar to `runAppServerReview`, but sends `"turn/start"` with user-provided input. It first checks whether to resume an existing thread (via `options.threadId`) or create a new one, making it suitable for interactive task delegation.

### Interrupting Execution: `interruptAppServerTurn`

A simple fire-and-forget RPC that sends `"turn/interrupt"`, used by the `/codex:cancel` command to abort in-progress work.

---

## Practical Code 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) {
  // Uses default broker detection from environment
  const client = await CodexAppServerClient.connect(cwd);
  const status = await client.request("status", {});
  console.log("Broker status:", status);
  await client.close(); // socket closes; broker process continues
}

```

### Manual Turn Capture

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

```

---

## Key Source Files

| File | Purpose |
|------|---------|
| `plugins/codex/scripts/lib/app-server.mjs` | Transport selection, JSON‑RPC base classes, broker/direct implementations |
| `plugins/codex/scripts/lib/codex.mjs` | Public API: `runAppServerReview`, `runAppServerTurn`, `captureTurn`, `withAppServer` |
| `plugins/codex/scripts/lib/broker-lifecycle.mjs` | Broker session management: `ensureBrokerSession`, `loadBrokerSession` |
| `plugins/codex/scripts/lib/process.mjs` | Process utilities: `binaryAvailable`, `terminateProcessTree` |
| `plugins/codex/commands/*.md` | Slash-command definitions that invoke the helpers |

---

## Summary

- The Codex plugin **selects transport dynamically** via `CodexAppServerClient.connect`, choosing between **direct STDIN/STDOUT spawn** or **Unix socket broker connection** based on environment and options.
- All communication uses **line-delimited JSON‑RPC**, with `AppServerClientBase` handling request correlation, notification routing, and graceful shutdown.
- High-level operations in **`codex.mjs`** orchestrate thread lifecycle, turn execution, and result aggregation while insulating command implementations from protocol details.
- The architecture supports both **ephemeral, isolated sessions** (direct mode) and **persistent, shared runtimes** (broker mode) without changing the calling code.

---

## Frequently Asked Questions

### How does the plugin choose between direct and broker mode?

The plugin checks for a broker endpoint in this priority order: explicit `options.brokerEndpoint`, the `CODEX_COMPANION_APP_SERVER_ENDPOINT` environment variable, or a persisted session file. If none are found and `disableBroker` is not set, it starts a new broker. Otherwise, it spawns the app-server directly. This logic is centralized in `CodexAppServerClient.connect()`.

### What happens to pending requests when the app-server exits?

`AppServerClientBase.handleExit()` (lines 63-76) iterates through all pending promises in `this.pending` and rejects them with an appropriate error. This ensures that in-flight operations fail fast rather than hanging indefinitely when the underlying transport closes.

### Can multiple Claude Code sessions share the same Codex app-server?

Yes. When using **broker mode**, the Unix domain socket allows multiple clients to connect to a single persistent app-server process. The `reuseExistingBroker` option and `loadBrokerSession()` helper enable this sharing by reading endpoint information from a session file stored in the working directory.

### What JSON‑RPC methods does the app-server expose?

The plugin invokes methods including `"thread/start"`, `"thread/name/set"`, `"turn/start"`, `"review/start"`, `"turn/interrupt"`, `"account/read"`, and `"status"`. Notifications like `"item/*"` and `"turn/completed"` stream progress back to the client. The full protocol is defined by the `codex app-server` binary, not the plugin code.