Skip to content

Provider CLI integration

AgentMux does not call AI provider APIs directly. Instead, it manages a provider CLI subprocess — claude, codex, gemini, or others — inside a PTY, and communicates with the provider’s API through that CLI. This page describes how that subprocess is created, configured, and supervised.

ProviderCLI binarynpm packageController typeOutput format
Claude Codeclaude@anthropic-ai/claude-codepersistentstream-json (NDJSON)
Codexcodex@openai/codexsubprocessJSONL — codex exec --json (NDJSON, one JSON object per line; not a single JSON blob)
Geminigemini@google/gemini-clisubprocessstream-json (NDJSON)
Qwen Codeqwen@qwen-code/qwen-codesubprocessstream-json (NDJSON) in Claude Code’s frame shape, read by the Claude translator as qwen-stream-json
Kimikimi— (PyPI kimi-cli, installed by you)subprocessstream-json (NDJSON)
Muxcodemuxcode@agentmuxai/muxcodesubprocessstream-json (NDJSON) — reuses the Claude translator
Antigravityagy— (Google’s installer)subprocessstream-json (NDJSON) in its own schema, with its own translator (agy-stream-json)
OpenClawopenclawopenclawacpACP: JSON-RPC 2.0 over stdio
GitHub Copilot CLIcopilot@github/copilotacpACP
Pipi-acppi-acp, with @earendil-works/pi-coding-agentacpACP
Kilo Code CLIkilo@kilocode/cliacpACP
Cline CLIclineclineacpACP
OpenCodeopencodeopencode-aiacpACP
goosegoose— (GitHub release, installed by you)acpACP
Grok Buildgrok— (xAI’s installer)acpACP
DeepSeek Harnessdsh@deepseek-ai/dshacpACP
Aideraider— (PyPI aider-chat, through Aider’s installer)terminalNone: its own UI in a terminal pane
Crushcrush@charmland/crushterminalNone: its own UI in a terminal pane
Muse Codemuse— (Meta’s installer)terminalNone: its own UI in a terminal pane

The controller type determines the spawn strategy: persistent keeps the CLI process alive between turns (Claude Code); subprocess spawns a fresh process per turn; acp keeps one process speaking the Agent Client Protocol over stdio; terminal opens a terminal pane that runs the CLI’s own UI (see The terminal tier).

Provider definitions live in frontend/app/view/agent/providers/catalog.ts (frontend) and crates/srv/src/backend/providers.rs (backend, source of truth for the table above).

“Provider” throughout this page — and “harness” in some older code/specs, the same axis under a different name — means which CLI tool is driving the session: Claude Code, Codex, Gemini, and so on. That is a distinct concept from model vendor: the underlying LLM backend actually serving responses for that session.

Each provider declares its default vendor(s) via supported_vendors in crates/srv/src/backend/providers.rs — most-default-first, e.g. Claude Code → ["anthropic"], Codex → ["openai"], OpenClaw → ["openai", "anthropic", "google"] since it’s model-agnostic. This is purely descriptive/display data (drives the dual-icon vendor badge and the agent picker’s default-vendor inference); it doesn’t gate anything at spawn time.

What does gate spawn-time behavior is base_url_env_var — the environment variable a provider reads to redirect its model vendor backend off the default endpoint (e.g. Claude Code’s ANTHROPIC_BASE_URL, pointing it at Bedrock, Vertex, OpenRouter, or a custom proxy). This is set only where independently verified per provider, not guessed — as of this writing only Claude Code has a confirmed one.

The effective vendor an agent is actually using is computed, not stored: a non-empty model_vendor_base_url override means the provider has been redirected off its default backend, so the effective vendor reads as "custom" regardless of what the provider declares — an empty/absent override means the provider is talking to its own default vendor, the first entry in supported_vendors (or the provider id itself as a fallback, for an uncataloged provider). See resolveEffectiveVendor() in frontend/app/view/agent/providers/catalog.ts.

Before spawning, AgentMux resolves the CLI binary path via the ResolveCliCommand RPC. The resolution order is:

  1. AgentMux-managed install, for a provider with an npm package: ~/.agentmux/shared/cli/<provider>/<pinned version>/, shared by every AgentMux version and channel on the machine. AgentMux installs it there at the pinned version the first time it’s needed, so agents run a known, tested version regardless of what the user has globally installed.
  2. System PATH, then the installer’s own folder, for a provider with no npm package (Kimi, Antigravity, goose, Grok Build, Aider, Muse Code). AgentMux never runs their installers. It looks on PATH, then in the folder each vendor’s installer uses by default, because a running AgentMux keeps the PATH it started with. If neither has the CLI, the launch fails with the vendor’s install command as a hint.

Source: crates/srv/src/server/cli_handlers.rs, crates/srv/src/backend/cli_install.rs, and known_install_paths in crates/srv/src/backend/providers.rs.

Launch arguments are assembled by frontend/app/view/agent/buildRuntimeArgs.ts. When they are applied depends on the controller type:

  • persistent (Claude Code): Args are built once at the time the CLI subprocess is spawned and do not change between turns — the process stays alive for the session.
  • subprocess (Codex, Gemini, others): Args are rebuilt per turn, since a fresh process is spawned for each turn.

The construction process:

  1. Start from base args — ProviderDefinition.launchArgs (e.g., ["-p", "--output-format", "stream-json", "--verbose", "--include-partial-messages"] for Claude).
  2. Strip conflicting flags — any --dangerously-skip-permissions, --permission-mode, --yolo, --model, and --effort flags already in the base are removed so runtime values take precedence.
  3. Append permission mode — provider-dependent:
    • Claude/others: --permission-mode <value> (e.g., default, bypass-permissions)
    • Kimi / Gemini / Qwen: only --yolo is supported
    • Codex: no permission flag (baked into base args)
  4. Append model and effort (Claude only):
    • --model <opus|sonnet|haiku>
    • --effort <low|medium|high> — skipped for Haiku (returns HTTP 400)
  5. Codex model special case — the model value is prepended to the trailing - positional arg rather than passed as --model.

This means the same agent definition can be launched with different models or effort levels without changing the stored definition — the runtime config overrides are applied at spawn time.

Step 3 — PTY creation (the blockcontroller)

Section titled “Step 3 — PTY creation (the blockcontroller)”

The blockcontroller (crates/srv/src/backend/blockcontroller/shell/mod.rs) is the component that creates and supervises the PTY. It owns the agent subprocess from birth to death.

let pty_system = native_pty_system();
let pair = pty_system.openpty(pty_size)?;

native_pty_system() from the portable-pty crate selects the platform implementation:

  • Windows: ConPTY (Windows Pseudo Console API)
  • Unix: POSIX posix_openpt()

The PTY size is seeded from the frontend-computed terminal dimensions before the process starts, avoiding a post-spawn resize race. Fallback is 25 rows × 200 columns.

The following environment variables are injected into the agent’s PTY at spawn:

VariableValue
AGENTMUX_LOCAL_URLhttp://127.0.0.1:<port> — sidecar base URL
AGENTMUX_BLOCKIDPane UUID — the agent’s own pane context
AGENTMUX_AGENT_IDAgent’s registered name
TERM, COLORTERM, TERM_PROGRAMTerminal capability hints
PATHAugmented with AgentMux-managed binary paths

AGENTMUX_AUTH_KEY is not placed in the agent PTY environment (stripped at sidecar startup, security PR #801). Only internal tooling that requires it (agentmux-mcp, agentmux-bashwrap) receives it via their own spawn configuration.

See Environment variable contract for the full variable list.

Once the process is running, the blockcontroller runs three concurrent async tasks:

TaskResponsibility
PTY read loopReads process stdout → line-buffer → JSON parse → FileStore + WPS events
Input loopRoutes the input channel → PTY writer (user turns, resize events, signals)
Wait loopMonitors process exit, updates block status, classifies failures

The blockcontroller state machine has three states: INIT → RUNNING → DONE.

Each provider emits a different JSON format. The backend defines a Translator trait (crates/srv/src/agents/translator/mod.rs) for mapping provider-specific JSON frames to the internal AgentEvent enum, designed to be provider-agnostic (the runner holds Box<dyn Translator>):

pub trait Translator: Send {
fn translate(&mut self, frame: Value) -> Vec<AgentEvent>;
}

Only Claude Code has a concrete implementation today (ClaudeTranslator, crates/srv/src/agents/translator/claude.rs) — it’s what the drone Agent-block’s headless runner (crates/srv/src/agents/runner.rs) uses. The interactive subprocess-controller path (what actually drives an agent pane) doesn’t route non-Claude providers’ frames through this trait yet; see the Codex-specific note below for how Codex is parsed today.

Raw bytes from the PTY are accumulated in a line buffer before JSON parsing:

  • Lines not starting with { are fast-rejected (terminal UI output, not structured events)
  • The buffer preserves raw bytes (not lossy-decoded strings) so multi-byte UTF-8 sequences that span chunk boundaries are handled correctly
  • Buffer cap is 1 MiB (AGENT_LINE_BUFFER_CAP); reset if no newline is found within that window

Parsed events are published on the WPS scope agent_event:<block_id> and simultaneously written as raw bytes to the xterm.js renderer.

Source: crates/srv/src/backend/blockcontroller/shell/translation.rs extract_agent_events().

Frame typeMaps to
stream_event.content_block_delta.text_deltaAssistantText event
stream_event.content_block_stop (tool_use)ToolUse event
result frame with cost_usdCost + Done events
result frame with is_error: trueFailure classification

Source: crates/srv/src/agents/translator/claude.rs.

OpenClaw, GitHub Copilot CLI, Pi, Kilo Code CLI, Cline CLI, OpenCode, goose, Grok Build and DeepSeek Harness speak the Agent Client Protocol: JSON-RPC 2.0 over stdio rather than NDJSON. One controller drives all of them (crates/srv/src/backend/blockcontroller/acp.rs), and one translator renders them (frontend/app/view/agent/providers/acp-translator.ts), so a new ACP harness needs configuration, not protocol code.

  • Session start. After initialize, the controller sends session/new, or session/load to resume when the agent says it supports that. Both carry the agent’s MCP servers from its .mcp.json, AgentMux’s own agentmux server included, in ACP’s shape. Every ACP agent gets the stdio servers; HTTP and SSE servers go only to an agent whose initialize response says it takes them.
  • Permissions. AgentMux approves tool use by default, as it does for every harness: it answers session/request_permission with the agent’s “allow always” option, else “allow once”. AgentMux adds no permission flags to an ACP harness’s command line.
  • Before launch. Two harnesses need a file in their own config folder first. Grok Build reads project rules only in a trusted folder, so AgentMux adds the agent’s own folder to Grok’s trusted folders, and nothing else. DeepSeek Harness uploads its session logs to DeepSeek unless a profile patch turns that off, so AgentMux writes that patch when there is none yet.

Codex is launched as codex exec --json --dangerously-bypass-approvals-and-sandbox - (launch_args in crates/srv/src/backend/providers.rs): the exec subcommand runs non-interactively, --json emits JSONL — newline-delimited JSON, one frame per line, not a single JSON document — and the trailing - reads the prompt from stdin. Its init frame looks like {"type":"thread.started","thread_id":"..."}, contrasted with Claude’s {"type":"system","subtype":"init","session_id":"..."} and Gemini’s {"type":"init","session_id":"..."} — Codex is the one provider keyed on thread_id rather than session_id (session_id_field: "thread_id").

Codex frames go through the same line-buffered, fast-reject front door described above, but unlike Claude there is currently no dedicated CodexTranslator implementing the Translator trait — only crates/srv/src/agents/translator/claude.rs exists today, consumed by the drone Agent-block’s headless runner (crates/srv/src/agents/runner.rs). For the interactive subprocess-controller path, Codex’s own frames are instead classified generically in-line (classify_output_line, session-id capture) and published as raw WPS events for the frontend to render — see crates/srv/src/backend/blockcontroller/subprocess/host_spawn.rs.

Aider, Crush and Muse Code have no interface AgentMux can parse: no ACP, and no structured output it can read (Muse Code’s own protocol needs a controller of its own, which doesn’t exist yet). They use the terminal controller type. Launching one turns the agent’s pane into a terminal pane (view: "term", an interactive command run on start) that runs the CLI with its launch arguments in the agent’s folder (TERMINAL_PANE_META in frontend/app/view/agent/launch-args.ts).

AgentMux still does the launch work it does for every harness: it writes the startup instructions file (AGENTS.md) and sets the CLI’s config folder and opt-out variables. Where the CLI won’t read the file by itself, the launch passes it: Aider runs as aider --read AGENTS.md. Muse Code runs with --trust-workspace, so it reads project rules in the agent’s folder.

What the terminal tier doesn’t get: an AgentMux transcript, token counts, resume, AgentMux’s MCP tools, or a model picker. The CLI’s own UI does those. A terminal-tier CLI also can’t be a .cmd wrapper on Windows, so for Muse Code AgentMux runs the native binary its installer downloaded.

Crush moves to ACP when upstream ships it, and Muse Code when AgentMux has a controller for its protocol.

Step 5 — Subprocess lifecycle and signal handling

Section titled “Step 5 — Subprocess lifecycle and signal handling”

When an agent is stopped (via agent.stop RPC or user action):

Unix:

  1. SIGTERM sent to the process group (negative PID) — signals the CLI and all its children
  2. Grace period waits (KILL_GRACE_SECS = 5)
  3. SIGKILL sent if the process is still running
  4. PTY writer (input_tx) is dropped, delivering EOF/SIGHUP as a belt-and-suspenders signal

Windows:

  • child.kill() only — no signal support in Win32

Source: crates/srv/src/backend/blockcontroller/shell/lifecycle.rs stop().

When the CLI exits unexpectedly:

  1. Stdout/stderr readers are drained with a 2-second timeout
  2. The result frame’s is_error flag is checked
  3. In-band API errors (401 auth failure, rate limits) are detected from synthetic assistant frames
  4. The last ~40 lines of stderr are retained for failure classification
  5. crate::agents::failure::AgentFailure classifies the exit into a structured failure type surfaced to the UI

Source: crates/srv/src/backend/blockcontroller/subprocess/mod.rs.

Session IDs are captured from the CLI’s stdout and persisted in block metadata. Most providers resume with a trailing flag: on the next turn, the frontend appends --resume <session_id> (Claude) or -r <session_id> (Gemini) so conversation context is restored without re-sending history.

Codex is different: it resumes via a subcommand, not a flag. Its continuation invocation is codex exec resume <thread_id> [flags] -, not codex exec [flags] - --resume <thread_id>. ProviderConfig::resume_strategy_str() reports this as "codex-exec" (every other provider reports "flag" or "none"), and build_turn_argv/build_codex_argv in crates/srv/src/backend/blockcontroller/subprocess/argv.rs handle it by inserting resume <thread_id> immediately after the exec subcommand while keeping the trailing stdin marker - last — a plain flag-append would produce an invalid Codex invocation.

The abstraction is hybrid — not a single unified interface throughout:

LayerApproach
Frontend provider definitionsStatic lookup table (PROVIDERS record), conditional branching on provider.id
Argument constructionConditional logic per provider in buildRuntimeArgs.ts
Backend spawnConditional on ProviderDefinition.controllerType (persistent / subprocess / acp / terminal)
Backend stream parsingAbstract Translator trait — currently only Claude Code has a concrete implementation (translate(frame) -> Vec<AgentEvent>); other providers’ interactive-pane frames are parsed generically inline (see Step 4’s Codex note)