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

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.

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

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

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

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

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

// 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:

// 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:

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:

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:

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →