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.
What Is ACP?
Section titled “What Is ACP?”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.
Why Use ACP?
Section titled “Why Use ACP?”| 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) |
Supported CLI Agents
Section titled “Supported CLI Agents”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 |
Custom Agents
Section titled “Custom Agents”You can add your own CLI agents via settings. Custom agents support the same features as built-in agents.
Quick Start
Section titled “Quick Start”Step 1: Install a CLI Agent
Section titled “Step 1: Install a CLI Agent”# Example: Install Claude Code CLInpm install -g @anthropic-ai/claude-code
# Verify installationclaude --versionStep 2: ACP Auto-Detection
Section titled “Step 2: ACP Auto-Detection”ACP automatically detects installed CLI agents on your system. No configuration needed!
Step 3: Use ACP Transport
Section titled “Step 3: Use ACP Transport”Once detected, ACP can be used as a transport for any supported provider. OmniRoute will automatically use ACP when the CLI is available.
How ACP Works
Section titled “How ACP Works”Architecture
Section titled “Architecture”┌─────────────────┐│ OmniRoute ││ (HTTP Proxy) │└────────┬────────┘ │ │ spawn() ▼┌─────────────────┐│ Child Process ││ (CLI Agent) ││ ││ stdin ◄──────┤ Send prompt│ stdout ──────►│ Receive response│ stderr ──────►│ Receive errors└─────────────────┘Process Lifecycle
Section titled “Process Lifecycle”- Spawn — ACP creates a child process for the CLI agent
- Send — ACP writes prompts to the process’s stdin
- Receive — ACP reads responses from stdout/stderr
- Idle Detection — ACP waits 2 seconds of inactivity before considering the response complete
- Kill — ACP terminates the process (SIGTERM, then SIGKILL after 5s)
Communication Protocol
Section titled “Communication Protocol”ACP uses stdio (standard input/output) for communication with CLI agents. The protocol is:
- Send prompt — Write to stdin with a newline
- Wait for response — Read from stdout until idle (2s of no output)
- Timeout — Default 120 seconds (configurable)
API Reference
Section titled “API Reference”Registry Functions
Section titled “Registry Functions”detectInstalledAgents()
Section titled “detectInstalledAgents()”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}getAvailableAgents()
Section titled “getAvailableAgents()”Gets only the agents that are installed and available for ACP.
import { getAvailableAgents } from "@/lib/acp";
const available = getAvailableAgents();// Returns: CliAgentInfo[] (only installed agents)getAgentById(id)
Section titled “getAgentById(id)”Gets a specific agent by ID.
import { getAgentById } from "@/lib/acp";
const agent = getAgentById("claude");// Returns: CliAgentInfo | undefinedsetCustomAgents(agents)
Section titled “setCustomAgents(agents)”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", },]);Manager Functions
Section titled “Manager Functions”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: AcpSessionAllowed 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>acpManager.kill(sessionId)
Section titled “acpManager.kill(sessionId)”Kills a session and cleans up.
import { acpManager } from "@/lib/acp";
const killed = acpManager.kill("acp-claude-1234567890-abc123");// Returns: booleanacpManager.getActiveSessions()
Section titled “acpManager.getActiveSessions()”Gets all active sessions.
import { acpManager } from "@/lib/acp";
const sessions = acpManager.getActiveSessions();// Returns: AcpSession[]acpManager.killAll()
Section titled “acpManager.killAll()”Kills all sessions.
import { acpManager } from "@/lib/acp";
acpManager.killAll();Session Interface
Section titled “Session Interface”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}Events
Section titled “Events”The AcpManager extends EventEmitter and emits the following events:
stdout
Section titled “stdout”Emitted when the CLI agent writes to stdout.
acpManager.on("stdout", ({ sessionId, data }) => { console.log(`[${sessionId}] stdout: ${data}`);});stderr
Section titled “stderr”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}`);});Configuration
Section titled “Configuration”Environment Variables
Section titled “Environment Variables”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",});Spawn Arguments
Section titled “Spawn Arguments”Each agent has default spawn arguments defined in the registry. You can override them:
acpManager.spawn("claude", "claude", ["--print", "--verbose"], {});Timeouts
Section titled “Timeouts”Default prompt timeout is 120 seconds (2 minutes). You can override:
await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutesDetection Cache
Section titled “Detection Cache”Agent detection is cached for 60 seconds to avoid expensive filesystem scans. Force refresh:
import { refreshAgentCache } from "@/lib/acp";
refreshAgentCache();Security
Section titled “Security”Command Injection Prevention
Section titled “Command Injection Prevention”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
Binary Name Validation
Section titled “Binary Name Validation”ACP validates that the version command binary matches the expected binary name (unless it’s a custom agent).
Process Isolation
Section titled “Process Isolation”Each ACP session runs in its own child process. The process is killed when the session ends or times out.
Performance
Section titled “Performance”Detection Performance
Section titled “Detection Performance”- First call: ~50-200ms (runs
versioncommand for each agent) - Cached calls: <1ms (returns from cache)
- Cache TTL: 60 seconds
Prompt Performance
Section titled “Prompt Performance”- Spawn: ~50-100ms
- Send prompt: ~10-50ms
- Wait for response: Depends on CLI agent (typically 1-30 seconds)
- Kill: ~5 seconds (SIGTERM) + immediate (SIGKILL)
Resource Usage
Section titled “Resource Usage”- Memory per session: ~10-50MB (depends on CLI agent)
- CPU: Minimal (I/O bound)
- Disk: None
Troubleshooting
Section titled “Troubleshooting”“Unknown agent” Error
Section titled ““Unknown agent” Error”Problem: acpManager.spawn() throws Unknown agent: <id>
Solution: Only these agents are allowed in spawn():
claudecodexgeminiqwen
Other agents must be spawned manually or via custom agent definitions.
“Session not alive” Error
Section titled ““Session not alive” Error”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", [], {});}“ACP timeout” Error
Section titled ““ACP timeout” Error”Problem: acpManager.sendPrompt() throws ACP timeout after 120000ms
Solution: Increase the timeout:
await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutesCLI Not Detected
Section titled “CLI Not Detected”Problem: detectInstalledAgents() doesn’t find your CLI
Solutions:
- Check PATH: Ensure the CLI is in your system PATH
- Check version command: Run
claude --versionmanually - Check permissions: Ensure the CLI is executable
- Custom agent: Add a custom agent definition for non-standard CLIs
Permission Denied
Section titled “Permission Denied”Problem: ACP can’t execute the CLI
Solutions:
- Check file permissions:
chmod +x /usr/local/bin/claude - Check ownership: Ensure OmniRoute has read/execute permissions
- Check SELinux/AppArmor: May block process spawning
Examples
Section titled “Examples”Example 1: Spawn and Use Claude Code
Section titled “Example 1: Spawn and Use Claude Code”import { acpManager, detectInstalledAgents } from "@/lib/acp";
// Detect installed agentsconst 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);}Example 2: Auto-Discovery with Fallback
Section titled “Example 2: Auto-Discovery with Fallback”import { acpManager, getAvailableAgents } from "@/lib/acp";
const available = getAvailableAgents();
// Try Claude first, fallback to Codexlet 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);Example 3: Custom Agent
Section titled “Example 3: Custom Agent”import { setCustomAgents, detectInstalledAgents } from "@/lib/acp";
// Register a custom CLI agentsetCustomAgents([ { 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();What’s Next?
Section titled “What’s Next?”- API Reference — REST API endpoints
- Provider Reference — All 352 providers
- MCP Server — Model Context Protocol integration
- A2A Server — Agent-to-Agent protocol
- Cloud Agent — Cloud-based agents
Reference
Section titled “Reference”- AionUi Project — Inspiration for ACP auto-detection
- ACP Source Code — Implementation details
manager.ts— Process lifecycle managementregistry.ts— Agent discovery and registrationindex.ts— Public API exports
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.

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