How the Codex Plugin Interacts with the Codex App Server Using JSON-RPC
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.
// 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 inthis.pending, and sends the JSON line viasendMessage().setNotificationHandler(lines 76-78) – Registers callbacks to receive async notifications such asitem/*andturn/*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.
// 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
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
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
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()inplugins/codex/scripts/lib/app-server.mjs. - JSON-RPC messages are encoded/decoded by
AppServerClientBase, which manages request IDs, pending promises inthis.pending, and notification routing throughsetNotificationHandler. - High-level helpers in
plugins/codex/scripts/lib/codex.mjssuch asrunAppServerReview,runAppServerTurn, andcaptureTurnorchestrate threads, turns, and result aggregation. - The architecture supports both ephemeral per-invocation sessions (via
SpawnedCodexAppServerClient) and persistent shared runtimes (viaBrokerCodexAppServerClientandensureBrokerSession).
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.
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 →