# How the Codex Plugin Interacts with the Codex App Server Using JSON-RPC

> Learn how the Codex plugin uses JSON-RPC to communicate with the Codex app server via STDIN/STDOUT pipes or Unix domain sockets. Understand request tracking and notification streaming.

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

---

**The Codex plugin communicates with the `codex app-server` binary via a JSON-RPC protocol over either STDIN/STDOUT pipes (direct spawn) or a Unix domain socket (broker mode), using a client library that handles request/response tracking, notification streaming, and graceful shutdowns.**

The openai/codex-plugin-cc repository implements a Claude Code plugin that orchestrates AI-powered code reviews and task execution. Understanding how the Codex plugin interacts with the Codex app server reveals a sophisticated three-layer architecture that manages transport selection, JSON-RPC message encoding, and high-level workflow orchestration through specific methods in `app-server.mjs` and `codex.mjs`.

## Transport Selection: Direct Spawn vs. Broker Mode

The interaction begins in `plugins/codex/scripts/lib/app-server.mjs`, where `CodexAppServerClient.connect()` determines whether to spawn a new process or reuse an existing broker session.

### The Connection Entry Point

The static method `CodexAppServerClient.connect(cwd, options)` evaluates environment variables and options to select the transport mechanism. It checks for `CODEX_COMPANION_APP_SERVER_ENDPOINT` (referenced internally as `BROKER_ENDPOINT_ENV`) to decide between broker and direct modes.

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

```

### Broker Mode (Unix Domain Socket)

When a broker endpoint is available, the client instantiates `BrokerCodexAppServerClient`, which opens a Unix domain socket using `net.createConnection`. This mode allows multiple Claude sessions to share a single `codex app-server` process, reducing overhead and maintaining state across invocations. The `sendMessage` implementation (lines 26-33) writes JSON lines directly to the socket.

### Direct Mode (STDIN/STDOUT)

If no broker endpoint exists or `disableBroker` is set, the client creates a `SpawnedCodexAppServerClient` that spawns the binary directly using `spawn("codex", ["app-server"], …)`. Communication occurs over the child process's STDIN and STDOUT pipes, creating an isolated per-invocation session. The `sendMessage` method (lines 68-75) handles transport-specific writing to STDIN.

## JSON-RPC Protocol Implementation

Both transport implementations inherit from `AppServerClientBase` in `app-server.mjs`, which implements the JSON-RPC wire protocol independently of the underlying transport.

### Core Client Architecture

The base class provides essential RPC functionality:

- **`request(method, params)`** (lines 86-98) – Creates a unique request ID, stores a `{resolve, reject}` pair in `this.pending`, and sends the JSON line via `sendMessage()`.
- **`setNotificationHandler`** (lines 76-78) – Registers callbacks to receive async notifications such as `item/*` and `turn/*` events.
- **`handleChunk` / `handleLine`** (lines 107-155) – Split incoming data on newlines and parse JSON, routing responses to pending requests or notification handlers.
- **`handleExit`** (lines 63-76) – Catches process termination, rejects all pending promises with the exit error, and records the error state for graceful degradation.

This architecture ensures that whether communicating via socket or pipe, the JSON-RPC semantics remain consistent.

## High-Level Workflow Orchestration

The file `plugins/codex/scripts/lib/codex.mjs` builds convenience helpers atop the JSON-RPC client to handle complex multi-step workflows like reviews and task execution.

### Running Code Reviews

The `runAppServerReview` function demonstrates the full orchestration flow. It uses `withAppServer` (lines 13-42) to manage client lifecycle, `startThread` (lines 332-348) to send the `"thread/start"` RPC, and `captureTurn` to aggregate streaming notifications into a final result.

```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;
    // …setup progress…
    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 for the specific turn ID, aggregates them into a `TurnCaptureState`, and resolves when receiving `"turn/completed"`. It forwards unrelated notifications to the previous handler to ensure other plugin functions remain operational.

### Executing Task Turns

`runAppServerTurn` follows a similar pattern but first decides whether to resume an existing thread or start fresh, then sends `"turn/start"` with the user prompt. This supports interactive task delegation where the plugin delegates coding tasks to the Codex app server.

### Interrupting Operations

For cancellation support, `interruptAppServerTurn` sends a fire-and-forget `"turn/interrupt"` RPC. This method returns immediately without awaiting a response, allowing the `/codex:cancel` command to terminate long-running operations instantly.

## Practical Implementation 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 a Shared Broker

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

async function demoBroker(cwd) {
  // Assume a broker is already running; no `disableBroker` flag
  const client = await CodexAppServerClient.connect(cwd);
  const status = await client.request("status", {}); // any supported RPC
  console.log("Broker status:", status);
  await client.close(); // just closes the socket; the broker stays alive
}

```

### Capturing a Turn Manually

```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 socket) via `CodexAppServerClient.connect()` in `plugins/codex/scripts/lib/app-server.mjs`.
- **JSON-RPC** messages are encoded/decoded by `AppServerClientBase`, which manages request IDs, pending promises in `this.pending`, and notification routing through `setNotificationHandler`.
- High-level helpers in `plugins/codex/scripts/lib/codex.mjs` such as `runAppServerReview`, `runAppServerTurn`, and `captureTurn` orchestrate threads, turns, and result aggregation.
- The architecture supports both ephemeral per-invocation sessions (via `SpawnedCodexAppServerClient`) and persistent shared runtimes (via `BrokerCodexAppServerClient` and `ensureBrokerSession`).

## Frequently Asked Questions

### What transport protocols does the Codex plugin support?

The Codex plugin supports two transport mechanisms for interacting with the Codex app server: direct process spawning using STDIN/STDOUT pipes, and Unix domain sockets via a broker. The selection is handled automatically by `CodexAppServerClient.connect()` based on the presence of the `CODEX_COMPANION_APP_SERVER_ENDPOINT` environment variable.

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

The `AppServerClientBase` class implements `setNotificationHandler()` to register callbacks for async messages like `turn/completed` or `item/*`. During active turns, the `captureTurn` function temporarily intercepts these notifications to aggregate state while forwarding unrelated messages to maintain plugin functionality across concurrent operations.

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

Yes. When running in broker mode, the plugin connects to a persistent Unix domain socket managed by the broker lifecycle utilities in `broker-lifecycle.mjs`. This allows multiple sessions to share a single `codex app-server` process, maintaining state and reducing startup overhead compared to spawning a new process per invocation.

### What happens if the app server process exits unexpectedly?

The `handleExit` method in `AppServerClientBase` (lines 63-76) catches process termination, rejects all pending promises stored in `this.pending` with the exit error, and triggers cleanup. This ensures that hanging `request()` calls fail gracefully rather than remaining unresolved indefinitely, preventing the plugin from freezing during communication failures.