Multi-CLI Adapters
botmux bridges different CLIs / agents through adapters, selected via cliId in bots.json — one-click switching. Local adapters each run as their own process (under the default tmux backend you can tmux attach into the real process; explicit pty/zellij/herdr backends differ); a few are integrated over API / remotely (e.g. Mira, riff) and are not local processes.
Applies to: when you want to switch the underlying CLI, or wire up a new tool, and need its cliId and whether it takes a model param.
Doesn't apply: strict Codex-compatible distributions and wrappers / gateways (ccr, aiden x claude, …) do not need new adapters — see Codex-compatible distributions and Wrapper / gateway integration below.
Supported CLIs / Agents
The table lists the current built-in adapters (the authoritative source for cliIds is src/adapters/cli/registry.ts, which changes across versions):
The
modelfield only takes effect for adapters that support a model parameter; others ignore it. Mir CLI's extra prerequisites (login / miramcp) are in the section below.
DeepSeek Harness (dsh)
cliId: "dsh" drives a local dsh CLI (the deepseek-harness) through the bundled runner via dsh --profile <name> over the SDK JSON-RPC protocol. Prerequisites:
dshon PATH (or pointcliPathOverrideat it). Upgrade note: this is thedshcommand from the npm package@deepseek-ai/dsh. Earlier versions relied ondsh-jsonrpc-agentfrom the Python wheel — if only that older command is on PATH, the adapter will fail with "command not found" after upgrading.- Native
dshCLI configured ($DSH_HOME/settings.yaml+$DSH_HOME/.credentials.yaml; defaults to~/.dsh/...whenDSH_HOMEis unset). - The target profile (default
botmux, overridable per bot viadshProfile) lives under$DSH_HOME/profiles/<name>/(default~/.dsh/profiles/<name>/). No manual setup is needed for first use: when the profile is absent, botmux writes the skeleton (package.json+ emptycordis.yml+cordis.patch.yml) and runsdsh plugin addto install its dependencies. To add community plugins or switch LLM providers, edit that profile'scordis.patch.yml— botmux only creates these files when missing and never overwrites existing content. ConfigureDSH_HOMEonly on the daemon process; per-botenv.DSH_HOMEis reserved/rejected so the adapter's prepared paths cannot diverge from the child runtime.
The runner reads agent-default-model (provider + model) from $DSH_HOME/settings.yaml for the initialize RPC; the plugin composition is entirely controlled by the profile's cordis.patch.yml — the runner no longer generates cordis.yml.
When editing cordis.patch.yml, note that the two entry forms mean different things: - insert: [...] inserts new plugins, while a bare - id: X (no insert) overrides the config of an existing plugin and is silently skipped when that id is not present. The default patch botmux generates inserts only sdk-jsonrpc-server (the one plugin dsh-base lacks), inherits everything else from dsh-base, and disables the Web GUI plugins that would block startup in headless mode.
Session JSONL lives under $DSH_HOME/sessions/botmux/ (default ~/.dsh/sessions/botmux/). Turns are multi-turn within one runner connection; a daemon restart starts a fresh session (no context resume). The TUI runtime (dshRuntime: "tui") still keeps its own UI state under ~/.dsh-tui.
ask_user_question is bridged through a temporary DSH profile patch generated by botmux. The official runner injects the bridge directly; dshRuntime: "tui" uses a dsh-tui wrapper patch that wraps the native question provider, asks in Lark first, and falls back to the native TUI for unsupported prompts. Set BOTMUX_DSH_ASK_BRIDGE=0 and restart the session to disable the bridge.
Mir CLI and MCP Bridge
When you choose Mira -> Mir CLI (local mircli) in botmux setup, the bot is configured with cliId: "mir". This adapter runs the local mircli -p --lean, so the same system user that runs the botmux daemon must already have Mir CLI authenticated and initialized.
BotMux does not need any DevBox-specific configuration. The same rules apply on DevBox, local macOS, or other Linux machines:
mirclican be found by botmux, or the bot config pointscliPathOverrideat the absolutemirclipath.~/.mira/config.jsonalready contains adevice_id. This is usually written bymircli mcp --device-id <id>or Mir CLI's own initialization flow.miramcpis installed in a standard Mir CLI location such as~/.local/bin/miramcpor~/.local/bin/mira_cli, orMIRAMCP_BINpoints at the executable.
When a cliId: "mir" session starts and receives a message, BotMux best-effort starts the MCP Bridge before invoking mircli:
It first checks ~/.mira/miramcp/miramcp.pid and local port 9801, so an already-running bridge is reused instead of started twice. To inspect the bridge, run this as the same user that runs the botmux daemon:
To disable this autostart behavior, either set this in ~/.mira/config.json:
or disable it only for BotMux:
Codex-compatible distributions
BotMux separates protocol capability from distribution identity: cliId: "codex" selects the Codex protocol adapter, while cliRuntime selects the independently released executable that actually runs. A compatible fork can therefore reuse model arguments, resume, idle detection, and gated RPC without being checked against the official Codex version stream.
Use cliRuntime only for a strictly compatible fork: it must accept the arguments BotMux sends to Codex, preserve the same interaction state and rollout / resume semantics, and use a compatible authentication / home layout. If it changes arguments, the TUI state machine, session storage, or protocol, it needs a real adapter instead of a compatibility declaration.
See the bots.json Codex-compatible distributions section for the complete config and update-provider behavior. The Dashboard Bot Defaults page can also configure and preflight a runtime. Existing cliPathOverride configs remain supported, but do not automatically enable Codex RPC features that require an explicit compatibility declaration.
Wrapper / gateway integration
In many cases you don't run the native CLI directly but wrap it with a gateway / router (internal proxy + SSO, model routing, etc.), such as ccr, ttadk, aiden x claude, aiden x codex. In this case you don't need a new adapter: cliId still holds the real underlying CLI (claude-code / codex …), and you only swap the launch entry point for a wrapper script, pointing to it with cliPathOverride (the "CLI executable path override" when editing a bot in botmux setup is exactly this).
Four general steps:
- Log in to the gateway first (one-time): complete the SSO login as the same system user that runs the daemon; the token is cached in that user's home directory. An expired token will pop an interactive login that blocks the PTY, so keep the login state alive.
- Write the wrapper script in
~/.botmux/bin/, passing the arguments botmux injects through to the real CLI (note: some gateways reject the--settingsbotmux injects, so strip it in the script). chmod +xto add the executable bit (the easiest one to miss!) — botmux uses node-pty to exec the script directly; without the executable bit you getEACCES, the CLI exits immediately on launch, and the bot crashes and restarts.- Verify by executing the script directly (use
~/.botmux/bin/xxx --version, don't test withbash xxx— running via bash doesn't need the executable bit and will mask the problem in step 3). Then configurecliPathOverrideinbots.json(use an absolute path, not~), and runbotmux restartto take effect.
For the specific wrapper scripts of each gateway, use the docs published by the corresponding CLI / gateway team. This public repository intentionally does not include internal document links or copied internal content.
- aiden × claude / aiden × codex — aiden×codex needs
scriptto force a PTY - ttadk — pay attention to wrapper argument forwarding and login state
- MTR — community-contributed,
npm i -g @metamove-code/mtr-cli@latest
A general technique for troubleshooting wrapper issues: run
botmux logs, find theSpawning fresh CLI:line, copy the full command, and run it manually locally to pinpoint the problem (permissions / argument blacklist / login state).
Adding a new adapter (contributors)
- Create a new file under
src/adapters/cli/implementing theCliAdapterinterface - Add the new ID to the
CliIdunion type insrc/adapters/cli/types.ts - Add the import / switch case / export in
src/adapters/cli/registry.ts - Add the display name to
CLI_DISPLAY_NAMESinsrc/worker.tsandcliDisplayNamesincard-builder.ts - Add an option to the setup interactive menu in
src/cli.ts - Update the README
See CONTRIBUTING.md for details.
