How Roo Code CLI Headless Mode Communicates with the VS Code Extension via IPC
The Roo Code CLI establishes a Unix socket-based IPC channel to send task commands and receive real-time events from the VS Code extension, enabling headless automation while leveraging the same extension logic.
The Roo Code project (RooCodeInc/Roo-Code) provides a command-line interface that operates without the VS Code UI, yet it must execute the same extension code that normally runs inside the editor. This is achieved through a Unix-domain socket IPC bridge that allows the headless CLI to act as a remote client to the extension's IPC server.
The IPC Architecture Overview
The communication model follows a client-server pattern over local Unix sockets. When running in headless mode, the Roo Code CLI spawns the extension as a subprocess, creates a shared socket file, and establishes a bidirectional message channel. This architecture ensures that task execution logic remains centralized in the extension while the CLI handles orchestration and I/O.
The implementation relies on the @roo-code/ipc package, which provides the IpcServer and IpcClient classes that manage the socket lifecycle and message serialization.
How the Extension Hosts the IPC Server
Extension Activation and Socket Configuration
When the VS Code extension activates, it checks for the ROO_CODE_IPC_SOCKET_PATH environment variable to determine if it should operate in IPC server mode. In src/extension.ts, the activation logic reads this variable and passes it to the API constructor:
// src/extension.ts (lines 362-367)
const socketPath = process.env.ROO_CODE_IPC_SOCKET_PATH
if (socketPath) {
const api = new API(outputChannel, context, socketPath)
// ...
}
If the variable is present, the extension knows it must accept external CLI connections rather than only handling UI interactions.
Server Initialization in the API Class
The API class in src/extension/api.ts instantiates the IpcServer and begins listening for incoming connections:
// src/extension/api.ts (lines 63-71)
if (socketPath) {
const ipc = (this.ipc = new IpcServer(socketPath, this.log));
ipc.listen(); // Binds to the Unix socket
// Handle incoming commands from CLI
ipc.on(IpcMessageType.TaskCommand, async (clientId, command) => {
// Route to appropriate handler...
});
}
Once ipc.listen() executes, the extension is ready to receive task commands from any connected CLI client.
How the CLI Connects as an IPC Client
Socket Path Generation and Environment Injection
The CLI entry point in packages/evals/src/cli/runTaskInCli.ts prepares the IPC channel before spawning the extension. It generates a unique socket path in the OS temporary directory and injects it into the subprocess environment:
// packages/evals/src/cli/runTaskInCli.ts (lines 24-30)
const ipcSocketPath = path.resolve(
os.tmpdir(),
`evals-cli-${run.id}-${task.id}.sock`
);
const env = {
...process.env,
ROO_CODE_IPC_SOCKET_PATH: ipcSocketPath, // Extension reads this
};
const subprocess = execa("pnpm", cliArgs, { env, cwd: process.cwd() });
This ensures the extension binds to the same socket file that the CLI will use for communication.
Client Connection Retry Logic
After spawning the extension subprocess, the CLI waits briefly for the server to initialize, then attempts to connect using an IpcClient. The implementation includes a retry loop to handle race conditions during startup:
// Connection attempt with retry logic
await new Promise(r => setTimeout(r, 5_000)); // Initial startup delay
let client: IpcClient | undefined;
let attempts = 10;
while (true) {
try {
client = new IpcClient(ipcSocketPath);
await pWaitFor(() => client!.isReady, {
interval: 500,
timeout: 2_000
});
break; // Successfully connected
} catch {
client?.disconnect();
if (--attempts === 0) throw new Error("Unable to connect to CLI IPC socket.");
await new Promise(r => setTimeout(r, 1_000));
}
}
Once connected, the CLI client can begin transmitting commands and listening for task events.
Bidirectional Message Flow
The IPC channel supports full-duplex communication, allowing the CLI to control task execution while receiving real-time updates from the extension.
CLI to Extension: Task Commands
The CLI initiates actions by calling client.send() with IpcMessageType.TaskCommand payloads. The extension's IpcServer receives these via its onMessage handler and emits TaskCommand events that the API class routes to appropriate handlers:
Key command types include:
TaskCommandName.StartNewTask– Initializes a new task with provided configurationTaskCommandName.SendMessage– Sends user input or prompts to the active task
When the API class receives these commands through ipc.on(IpcMessageType.TaskCommand, ...), it invokes the corresponding methods (startNewTask(), sendMessage()) using the same code paths as the VS Code UI.
Extension to CLI: Task Events and Progress
The extension broadcasts state changes back to all connected clients through the emit method in src/extension/api.ts. This method overrides the standard EventEmitter to include IPC broadcasting:
// src/extension/api.ts - API.emit method
public override emit<K extends keyof RooCodeEvents>(
eventName: K,
...args: RooCodeEvents[K]
) {
const data = {
eventName: eventName as RooCodeEventName,
payload: args
} as TaskEvent;
// Broadcast to all CLI clients
this.ipc?.broadcast({
type: IpcMessageType.TaskEvent,
origin: IpcOrigin.Server,
data,
});
return super.emit(eventName, ...args);
}
The CLI receives these events by registering a listener on the client instance:
client.on(IpcMessageType.TaskEvent, async (event) => {
const { eventName, payload } = event.data;
// Handle TaskStarted, Message, TaskFinished, etc.
});
This design ensures that task progress, LLM responses, and completion status flow back to the CLI in real-time, even though the extension runs in a separate process.
Connection Lifecycle and Shutdown
When the task completes or the CLI receives a termination signal, the client calls client.disconnect() to close the socket connection cleanly. The IpcServer detects the disconnection through its socket.disconnected handler and removes the client from its internal registry, preventing memory leaks and allowing the Unix socket file to be properly cleaned up.
If the CLI process terminates unexpectedly, the extension's server detects the broken connection on the next write attempt and handles cleanup automatically.
Summary
- Unix socket IPC enables the Roo Code CLI to communicate with the VS Code extension while running in headless mode.
- Environment variable coordination (
ROO_CODE_IPC_SOCKET_PATH) ensures both processes reference the same socket file. - Bidirectional messaging allows the CLI to send
TaskCommandmessages and receiveTaskEventupdates in real-time. - Retry logic in the CLI handles the race condition between extension startup and client connection attempts.
- Shared extension logic means headless CLI tasks execute through the exact same
APImethods as UI-triggered tasks.
Frequently Asked Questions
What transport protocol does Roo Code use for CLI-to-extension communication?
Roo Code uses Unix domain sockets (local socket files) for IPC communication. The implementation in packages/ipc/src/ipc-server.ts and packages/ipc/src/ipc-client.ts handles socket creation, binding, and message serialization. This approach provides low-latency, reliable communication between the CLI process and the extension subprocess without network overhead.
How does the extension differentiate between CLI and VS Code UI interactions?
The extension does not differentiate between sources once the connection is established. Both the CLI (via IpcClient) and the VS Code UI use the same API class methods. When in IPC mode, the API broadcasts events to all connected clients, meaning multiple CLI clients could theoretically connect simultaneously, though the standard implementation uses one CLI per extension instance.
What happens if the CLI cannot connect to the extension's IPC socket?
The CLI implements a retry mechanism with exponential backoff in runTaskInCli.ts. It attempts to instantiate the IpcClient up to 10 times with 1-second delays between attempts. If all attempts fail, it throws an "Unable to connect to CLI IPC socket" error and terminates the subprocess. This handles cases where the extension takes longer than expected to initialize.
Can this IPC mechanism work across network boundaries or only locally?
The current implementation uses Unix domain sockets, which are limited to local inter-process communication on the same machine. The socket paths are filesystem-based (e.g., /tmp/evals-cli-*.sock), making this architecture suitable for local automation and CI/CD pipelines but not for remote distributed execution without additional tunneling or proxy layers.
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 →