Skip to content
OmniRoute source

ACP (Agent Client Protocol)

TL;DR: ACP lets OmniRoute spawn CLI agents (like Claude Code, Codex) as child processes instead of using HTTP APIs. This gives you “CLI-as-backend” transport.


ACP (Agent Client Protocol) is a “CLI-as-backend” transport for OmniRoute. Instead of intercepting HTTP API calls to AI providers, ACP spawns CLI agents as child processes and feeds prompts through their native interface.

Benefit Description
No API keys needed Uses your existing CLI authentication
Native protocol Uses each CLI’s native input/output format
Auto-discovery Detects installed CLIs on your system
15 built-in agents Pre-configured for popular CLI tools
Custom agents Add your own CLI tools via settings
Process management Handles lifecycle (spawn, send, kill)

ACP supports 15 built-in CLI agents out of the box:

Agent ID Display Name Binary Protocol
codex OpenAI Codex CLI codex stdio
claude Claude Code CLI claude stdio
goose Goose CLI goose stdio
openclaw OpenClaw openclaw stdio
aider Aider aider stdio
opencode OpenCode opencode stdio
cline Cline cline stdio
qwen Qwen Code qwen --acp stdio
forge ForgeCode forge stdio
amazon-q Amazon Q Developer q stdio
interpreter Open Interpreter interpreter stdio
cursor-cli Cursor CLI cursor stdio
warp Warp AI warp stdio
gemini Gemini CLI gemini stdio
zcode ZCode zcode stdio

You can add your own CLI agents via settings. Custom agents support the same features as built-in agents.


Terminal window
# Example: Install Claude Code CLI
npm install -g @anthropic-ai/claude-code
# Verify installation
claude --version

ACP automatically detects installed CLI agents on your system. No configuration needed!

Once detected, ACP can be used as a transport for any supported provider. OmniRoute will automatically use ACP when the CLI is available.


┌─────────────────┐
│ OmniRoute │
│ (HTTP Proxy) │
└────────┬────────┘
│
│ spawn()
▼
┌─────────────────┐
│ Child Process │
│ (CLI Agent) │
│ │
│ stdin ◄──────┤ Send prompt
│ stdout ──────►│ Receive response
│ stderr ──────►│ Receive errors
└─────────────────┘
  1. Spawn — ACP creates a child process for the CLI agent
  2. Send — ACP writes prompts to the process’s stdin
  3. Receive — ACP reads responses from stdout/stderr
  4. Idle Detection — ACP waits 2 seconds of inactivity before considering the response complete
  5. Kill — ACP terminates the process (SIGTERM, then SIGKILL after 5s)

ACP uses stdio (standard input/output) for communication with CLI agents. The protocol is:

  1. Send prompt — Write to stdin with a newline
  2. Wait for response — Read from stdout until idle (2s of no output)
  3. Timeout — Default 120 seconds (configurable)

Detects all installed CLI agents on the system. Results are cached for 60 seconds.

import { detectInstalledAgents } from "@/lib/acp";
const agents = detectInstalledAgents();
// Returns: CliAgentInfo[]
interface CliAgentInfo {
id: string; // e.g., "codex", "claude"
name: string; // Display name
binary: string; // Binary name to spawn
versionCommand: string; // Version detection command
version: string | null; // Detected version (null if not installed)
installed: boolean; // Whether the agent is installed
providerAlias: string; // Provider ID in OmniRoute
spawnArgs: string[]; // Arguments to pass when spawning
protocol: "stdio" | "http"; // Communication protocol
isCustom?: boolean; // Whether this is a user-defined custom agent
}

Gets only the agents that are installed and available for ACP.

import { getAvailableAgents } from "@/lib/acp";
const available = getAvailableAgents();
// Returns: CliAgentInfo[] (only installed agents)

Gets a specific agent by ID.

import { getAgentById } from "@/lib/acp";
const agent = getAgentById("claude");
// Returns: CliAgentInfo | undefined

Sets custom agent definitions from settings.

import { setCustomAgents } from "@/lib/acp";
setCustomAgents([
{
id: "my-custom-cli",
name: "My Custom CLI",
binary: "mycli",
versionCommand: "mycli --version",
providerAlias: "my-provider",
spawnArgs: [],
protocol: "stdio",
},
]);

acpManager.spawn(agentId, binary, args, env)

Section titled “acpManager.spawn(agentId, binary, args, env)”

Spawns a new CLI agent process.

import { acpManager } from "@/lib/acp";
const session = acpManager.spawn("claude", "claude", ["--print", "--output-format", "json"], {
/* custom env vars */
});
// Returns: AcpSession

Allowed agent IDs: ["claude", "codex", "gemini", "qwen"]

acpManager.sendPrompt(sessionId, prompt, timeoutMs)

Section titled “acpManager.sendPrompt(sessionId, prompt, timeoutMs)”

Sends a prompt to a CLI agent and collects the response.

import { acpManager } from "@/lib/acp";
const response = await acpManager.sendPrompt(
"acp-claude-1234567890-abc123",
"What is 2+2?",
120000 // 2 minutes timeout
);
// Returns: Promise<string>

Kills a session and cleans up.

import { acpManager } from "@/lib/acp";
const killed = acpManager.kill("acp-claude-1234567890-abc123");
// Returns: boolean

Gets all active sessions.

import { acpManager } from "@/lib/acp";
const sessions = acpManager.getActiveSessions();
// Returns: AcpSession[]

Kills all sessions.

import { acpManager } from "@/lib/acp";
acpManager.killAll();
interface AcpSession {
id: string; // Unique session ID
agentId: string; // Agent ID (e.g., "claude")
process: ChildProcess; // Child process handle
alive: boolean; // Whether the process is alive
stdoutBuffer: string; // Accumulated stdout buffer
stderrBuffer: string; // Accumulated stderr buffer
createdAt: Date; // Created timestamp
}

The AcpManager extends EventEmitter and emits the following events:

Emitted when the CLI agent writes to stdout.

acpManager.on("stdout", ({ sessionId, data }) => {
console.log(`[${sessionId}] stdout: ${data}`);
});

Emitted when the CLI agent writes to stderr.

acpManager.on("stderr", ({ sessionId, data }) => {
console.error(`[${sessionId}] stderr: ${data}`);
});

Emitted when the CLI agent process exits.

acpManager.on("exit", ({ sessionId, code, signal }) => {
console.log(`[${sessionId}] exited with code ${code}, signal ${signal}`);
});

Emitted when the CLI agent process errors.

acpManager.on("error", ({ sessionId, error }) => {
console.error(`[${sessionId}] error: ${error}`);
});

ACP inherits all environment variables from the parent process and can be extended with custom env vars:

acpManager.spawn("claude", "claude", [], {
ANTHROPIC_API_KEY: "sk-...",
DEBUG: "true",
});

Each agent has default spawn arguments defined in the registry. You can override them:

acpManager.spawn("claude", "claude", ["--print", "--verbose"], {});

Default prompt timeout is 120 seconds (2 minutes). You can override:

await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutes

Agent detection is cached for 60 seconds to avoid expensive filesystem scans. Force refresh:

import { refreshAgentCache } from "@/lib/acp";
refreshAgentCache();

ACP validates version commands to prevent command injection attacks:

const DISALLOWED_VERSION_COMMAND_CHARS = /[;&|<>`$\r\n]/;

Version commands containing these characters are rejected:

  • ; — Command separator
  • & — Background process
  • | — Pipe
  • <, > — Redirection
  • ` — Command substitution
  • $ — Variable expansion
  • \r, \n — Line breaks

ACP validates that the version command binary matches the expected binary name (unless it’s a custom agent).

Each ACP session runs in its own child process. The process is killed when the session ends or times out.


  • First call: ~50-200ms (runs version command for each agent)
  • Cached calls: <1ms (returns from cache)
  • Cache TTL: 60 seconds
  • Spawn: ~50-100ms
  • Send prompt: ~10-50ms
  • Wait for response: Depends on CLI agent (typically 1-30 seconds)
  • Kill: ~5 seconds (SIGTERM) + immediate (SIGKILL)
  • Memory per session: ~10-50MB (depends on CLI agent)
  • CPU: Minimal (I/O bound)
  • Disk: None

Problem: acpManager.spawn() throws Unknown agent: &lt;id&gt;

Solution: Only these agents are allowed in spawn():

  • claude
  • codex
  • gemini
  • qwen

Other agents must be spawned manually or via custom agent definitions.

Problem: acpManager.sendPrompt() throws Session ${sessionId} is not alive

Solution: The session may have exited or been killed. Check session status:

const session = acpManager.getSession(sessionId);
if (!session?.alive) {
// Re-spawn the session
acpManager.spawn("claude", "claude", [], {});
}

Problem: acpManager.sendPrompt() throws ACP timeout after 120000ms

Solution: Increase the timeout:

await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutes

Problem: detectInstalledAgents() doesn’t find your CLI

Solutions:

  1. Check PATH: Ensure the CLI is in your system PATH
  2. Check version command: Run claude --version manually
  3. Check permissions: Ensure the CLI is executable
  4. Custom agent: Add a custom agent definition for non-standard CLIs

Problem: ACP can’t execute the CLI

Solutions:

  1. Check file permissions: chmod +x /usr/local/bin/claude
  2. Check ownership: Ensure OmniRoute has read/execute permissions
  3. Check SELinux/AppArmor: May block process spawning

import { acpManager, detectInstalledAgents } from "@/lib/acp";
// Detect installed agents
const agents = detectInstalledAgents();
const claude = agents.find((a) => a.id === "claude");
if (claude?.installed) {
// Spawn a new session
const session = acpManager.spawn("claude", claude.binary, ["--print", "--output-format", "json"]);
// Send a prompt
const response = await acpManager.sendPrompt(
session.id,
"Explain quantum computing in 100 words"
);
console.log("Claude's response:", response);
// Clean up
acpManager.kill(session.id);
}
import { acpManager, getAvailableAgents } from "@/lib/acp";
const available = getAvailableAgents();
// Try Claude first, fallback to Codex
let agentId = "claude";
if (!available.find((a) => a.id === "claude")) {
if (available.find((a) => a.id === "codex")) {
agentId = "codex";
} else {
throw new Error("No ACP-compatible CLI agent found");
}
}
const agent = available.find((a) => a.id === agentId)!;
const session = acpManager.spawn(agentId, agent.binary, agent.spawnArgs);
const response = await acpManager.sendPrompt(session.id, "Hello!");
acpManager.kill(session.id);
import { setCustomAgents, detectInstalledAgents } from "@/lib/acp";
// Register a custom CLI agent
setCustomAgents([
{
id: "my-llm-cli",
name: "My LLM CLI",
binary: "myllm",
versionCommand: "myllm --version",
providerAlias: "my-llm-provider",
spawnArgs: ["--format", "json"],
protocol: "stdio",
},
]);
// Now detectInstalledAgents() will include "my-llm-cli"
const agents = detectInstalledAgents();


  • AionUi Project — Inspiration for ACP auto-detection
  • ACP Source Code — Implementation details
    • manager.ts — Process lifecycle management
    • registry.ts — Agent discovery and registration
    • index.ts — Public API exports

OmniRoute source repository (a58000c7685f)

HagiCode

HagiCode is an agentic coding workspace: structured workflows, multi-agent execution, and Hero Dungeon views turn ideas into shipped software.

Turn ideas into polished, usable software with a smarter, faster, and more enjoyable agentic coding workflow.

HagiCode light theme main interface screenshot
  • SmartStructured workflows turn intent into an executable path from idea to shipped change.
  • EfficientMulti-agent workflows keep research, implementation, and review moving in parallel.
  • FunHero Dungeon interfaces make long coding sessions visual, collaborative, and rewarding.
Visit HagiCode