How the Codex Plugin Communicates with the Codex App Server: JSON-RPC Architecture Explained
The Codex plugin communicates with the Codex App Server via a JSON-RPC protocol over line-delimited JSON, selecting between direct process spawning (STDIN/STDOUT) or Unix domain socket connections (broker mode) through the CodexAppServerClient.connect method in app-server.mjs.
The openai/codex-plugin-cc repository implements a Claude Code plugin that delegates code reviews and asynchronous tasks to the Codex CLI. The communication architecture relies on a three-layer stack—transport selection, JSON-RPC client management, and high-level thread orchestration—to enable seamless interaction between the plugin and the codex app-server binary.
Transport Layer: Direct Spawn vs. Broker Mode
The entry point CodexAppServerClient.connect in plugins/codex/scripts/lib/app-server.mjs determines whether to spawn a fresh process or connect to an existing broker based on environment configuration and runtime options.
Broker Mode (Unix Domain Sockets)
When the CODEX_COMPANION_APP_SERVER_ENDPOINT environment variable (referenced internally as BROKER_ENDPOINT_ENV) is set, or when options.reuseExistingBroker triggers session lookup via loadBrokerSession, the client instantiates BrokerCodexAppServerClient. This subclass establishes a persistent connection using net.createConnection to a Unix domain socket, allowing multiple Claude Code sessions to share a single codex app-server process and its state.
Direct Mode (Process Spawning)
If no broker endpoint is available and options.disableBroker is not set, the client creates a SpawnedCodexAppServerClient. This spawns the codex app-server binary directly using spawn("codex", ["app-server"], …) and communicates over the resulting STDIN/STDOUT pipes. This mode creates an isolated, per-invocation process that terminates when the plugin closes the connection.
// 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;
}
JSON-RPC Protocol Implementation
Both transport subclasses inherit from AppServerClientBase, which implements the complete JSON-RPC 2.0 protocol handling for the Codex plugin communication.
Request-Response Management
The request(method, params) method (lines 86–98) generates unique request IDs, stores promise resolution handlers in this.pending, and transmits JSON-encoded messages through the transport-specific sendMessage implementation. This ensures synchronous-style API usage despite the underlying asynchronous message passing.
Notification Streaming
The setNotificationHandler method (lines 76–78) registers callbacks for asynchronous server events such as item/* and turn/* notifications. The handleChunk and handleLine methods (lines 107–155) buffer incoming data, split on newlines, and parse JSON to dispatch responses to pending requests or notifications to the registered handler.
Error Handling and Lifecycle
The handleExit method (lines 63–76) gracefully resolves or rejects all pending promises when the transport closes, preventing memory leaks and ensuring clean shutdowns whether the connection terminates via socket closure or process exit.
// SpawnedCodexAppServerClient.sendMessage (lines 68–75)
async sendMessage(message) {
if (!this.process) throw new Error("Process not started");
this.process.stdin.write(JSON.stringify(message) + "\n");
}
// BrokerCodexAppServerClient.sendMessage (lines 26–33)
async sendMessage(message) {
if (!this.socket) throw new Error("Socket not connected");
this.socket.write(JSON.stringify(message) + "\n");
}
High-Level Thread Operations in codex.mjs
The file plugins/codex/scripts/lib/codex.mjs exposes the public API used by slash commands, abstracting the JSON-RPC complexity into semantic operations like runAppServerReview and runAppServerTurn.
Running Code Reviews
The runAppServerReview function demonstrates the complete communication flow:
- Connection Management: Uses
withAppServer(lines 13–42) to acquire a client viaCodexAppServerClient.connect, execute operations, and ensure cleanup. - Thread Initialization: Calls
startThread(lines 332–348) to sendthread/startRPC and optionallythread/name/set. - Turn Capture: Invokes
captureTurn(lines 559–611) to install a temporary notification handler that aggregates messages until receivingturn/completed.
// plugins/codex/scripts/lib/codex.mjs
export async function runAppServerReview(cwd, options = {}) {
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: options.delivery,
target: options.target,
}),
{ onProgress: options.onProgress }
);
return {
status: buildResultStatus(turnState),
threadId: turnState.threadId,
sourceThreadId,
turnId: turnState.turnId,
reviewText: turnState.reviewText,
reasoningSummary: turnState.reasoningSummary,
};
});
}
Task Execution and Interruption
The runAppServerTurn function manages generic task execution, deciding whether to resume existing threads or start new ones before sending turn/start RPC. For immediate cancellation, interruptAppServerTurn sends a fire-and-forget turn/interrupt request to halt active processing.
Practical Communication Examples
Direct Spawn Without Broker
import { CodexAppServerClient } from "./app-server.mjs";
async function demoDirect(cwd) {
// Force direct mode – spawns new process
const client = await CodexAppServerClient.connect(cwd, { disableBroker: true });
await client.initialize();
const info = await client.request("account/read", {});
console.log("Account:", info);
await client.close(); // Terminates the spawned process
}
Connecting to Existing Broker
import { CodexAppServerClient } from "./app-server.mjs";
async function demoBroker(cwd) {
// Connects to broker via Unix socket
const client = await CodexAppServerClient.connect(cwd);
const status = await client.request("status", {});
console.log("Broker status:", status);
await client.close(); // Closes socket only; broker persists
}
Capturing Streaming Results
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 }],
})
);
return turnState.reviewText || turnState.lastAgentMessage;
});
}
Summary
- Transport Flexibility: The
CodexAppServerClient.connectmethod selects between direct mode (STDIN/STDOUT viaSpawnedCodexAppServerClient) and broker mode (Unix domain sockets viaBrokerCodexAppServerClient) based on theCODEX_COMPANION_APP_SERVER_ENDPOINTenvironment variable. - JSON-RPC Protocol:
AppServerClientBasemanages request IDs, promise resolution, notification streaming, and line-buffered JSON parsing inplugins/codex/scripts/lib/app-server.mjs. - High-Level Orchestration: Functions like
runAppServerReviewandrunAppServerTurnincodex.mjshandle thread lifecycle management, fromthread/startinitialization toturn/completedaggregation. - Resource Management: The
withAppServerhelper ensures proper client initialization and cleanup, whilecaptureTurnfilters notifications to isolate specific operation results.
Frequently Asked Questions
What protocol does the Codex plugin use to communicate with the Codex App Server?
The Codex plugin uses JSON-RPC 2.0 over line-delimited JSON streams. Each message is a single JSON object terminated by a newline, sent either over STDIN/STDOUT pipes (direct mode) or Unix domain sockets (broker mode) as implemented in app-server.mjs.
How does the plugin decide between broker mode and direct spawn mode?
The CodexAppServerClient.connect method checks the CODEX_COMPANION_APP_SERVER_ENDPOINT environment variable first. If set, it connects to that Unix socket endpoint (broker mode). If unset and disableBroker is false, it spawns a fresh codex app-server process (direct mode). The reuseExistingBroker option can also trigger broker discovery from saved session files.
Can multiple Claude Code sessions share the same Codex App Server?
Yes, when running in broker mode, multiple plugin instances can connect to the same Unix domain socket endpoint. This allows shared state and reduced startup overhead, whereas direct mode creates isolated per-session processes that terminate when the plugin closes the connection.
How does the plugin handle streaming responses from long-running Codex operations?
The captureTurn function in codex.mjs installs a temporary notification handler that filters incoming JSON-RPC notifications (such as item/added or turn/completed) by turn ID. It aggregates these messages into a TurnCaptureState object, resolving only when the server sends the turn/completed notification, effectively converting the streaming protocol into a promise-based API.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →