Files
openclaw/docs/gateway/cli-backends.md
Sally O'Malley f0b6f70053 fix(agents): honor effective exec policy for Claude live Bash (#86330)
* fix(agents): answer Claude live control_request can_use_tool via exec policy

Claude CLI emits stream-json control_request frames with subtype
can_use_tool when it wants to use a native tool. The Claude live-session
bridge previously dropped these frames, leaving Claude waiting for a
control_response until the 180/600s no-output timeout fired (see #80819).

Resolve the effective OpenClaw exec policy (per-agent tools.exec -> global
tools.exec -> allowlist/on-miss defaults) once at session-start time and
thread it through fingerprinting and the session record. When a
can_use_tool request arrives:

- Allow native Bash when the resolved policy is security=full, ask=off
  (matching the bypassPermissions semantics OpenClaw already documents).
- Otherwise deny with a message that names the resolved policy and
  points the agent at OpenClaw MCP tools.

Unsupported control_request subtypes get a structured error response
instead of a silent no-op, and stray control_response frames are
silently dropped. Adds spawn-test coverage for both allow and deny paths.

Fixes #80819

* fix(agents): align Claude live control_request policy with backend defaults

Resolve the effective exec policy through the same defaults that
extensions/anthropic/cli-shared.ts:isOpenClawRequestedYolo and
src/agents/exec-defaults.ts:resolveExecDefaults already use (security
?? "full", ask ?? "off") instead of falling back to a hand-rolled
allowlist/on-miss default that disagreed with the rest of the codebase.
Without this, a default-config OpenClaw deployment launches Claude with
--permission-mode bypassPermissions but the bridge would still deny
Bash control_requests, re-creating the #80819 stall for the very
default-config case the issue reports.

Also thread the effective Claude permission mode into the policy
decision. Prefer the operator's explicit --permission-mode in argv,
falling back to what normalizeClaudePermissionArgs would have inserted
for an un-overridden launch. Native Bash is auto-allowed only when the
effective mode is bypassPermissions AND tools.exec resolves to
full/no-ask, so explicit raw-arg overrides like --permission-mode
default or acceptEdits broaden Claude's native prompting and are
honored by routing through deny.

Adds a no-config regression test (default deployment allows Bash, no
stall) and a permission-mode-override test (tools.exec full/off plus
explicit --permission-mode default in raw args denies). Existing
allow/deny tests continue to pass via the synthesized-mode fallback.

* fix(agents): honor effective exec policy for Claude live Bash

---------

Co-authored-by: Guillaume Thirry <g.thirry@gmail.com>
2026-05-25 11:39:17 -04:00

16 KiB

summary, read_when, title
summary read_when title
CLI backends: local AI CLI fallback with optional MCP tool bridge
You want a reliable fallback when API providers fail
You are running local AI CLIs and want to reuse them
You want to understand the MCP loopback bridge for CLI backend tool access
CLI backends

OpenClaw can run local AI CLIs as a text-only fallback when API providers are down, rate-limited, or temporarily misbehaving. This is intentionally conservative:

  • OpenClaw tools are not injected directly, but backends with bundleMcp: true can receive gateway tools via a loopback MCP bridge.
  • JSONL streaming for CLIs that support it.
  • Sessions are supported (so follow-up turns stay coherent).
  • Images can be passed through if the CLI accepts image paths.

This is designed as a safety net rather than a primary path. Use it when you want "always works" text responses without relying on external APIs.

If you want a full harness runtime with ACP session controls, background tasks, thread/conversation binding, and persistent external coding sessions, use ACP Agents instead. CLI backends are not ACP.

Building a new backend plugin? Use [CLI backend plugins](/plugins/cli-backend-plugins). This page is for users configuring and operating an already registered backend.

Beginner-friendly quick start

You can use Claude Code CLI without any config (the bundled Anthropic plugin registers a default backend):

openclaw agent --message "hi" --model claude-cli/claude-sonnet-4-6

If your gateway runs under launchd/systemd and PATH is minimal, add just the command path:

{
  agents: {
    defaults: {
      cliBackends: {
        "claude-cli": {
          command: "/opt/homebrew/bin/claude",
        },
      },
    },
  },
}

That's it. No keys, no extra auth config needed beyond the CLI itself.

If you use a bundled CLI backend as the primary message provider on a gateway host, OpenClaw now auto-loads the owning bundled plugin when your config explicitly references that backend in a model ref or under agents.defaults.cliBackends.

Using it as a fallback

Add a CLI backend to your fallback list so it only runs when primary models fail:

{
  agents: {
    defaults: {
      model: {
        primary: "anthropic/claude-opus-4-6",
        fallbacks: ["claude-cli/claude-sonnet-4-6"],
      },
      models: {
        "anthropic/claude-opus-4-6": { alias: "Opus" },
        "claude-cli/claude-sonnet-4-6": {},
      },
    },
  },
}

Notes:

  • If you use agents.defaults.models (allowlist), you must include your CLI backend models there too.
  • If the primary provider fails (auth, rate limits, timeouts), OpenClaw will try the CLI backend next.

Configuration overview

All CLI backends live under:

agents.defaults.cliBackends

Each entry is keyed by a provider id (e.g. claude-cli, my-cli). The provider id becomes the left side of your model ref:

<provider>/<model>

Example configuration

{
  agents: {
    defaults: {
      cliBackends: {
        "my-cli": {
          command: "my-cli",
          args: ["--json"],
          output: "json",
          input: "arg",
          modelArg: "--model",
          modelAliases: {
            "claude-opus-4-6": "opus",
            "claude-sonnet-4-6": "sonnet",
          },
          sessionArg: "--session",
          sessionMode: "existing",
          sessionIdFields: ["session_id", "conversation_id"],
          systemPromptArg: "--system",
          // For CLIs with a dedicated prompt-file flag:
          // systemPromptFileArg: "--system-file",
          // Codex-style CLIs can point at a prompt file instead:
          // systemPromptFileConfigArg: "-c",
          // systemPromptFileConfigKey: "model_instructions_file",
          systemPromptWhen: "first",
          imageArg: "--image",
          imageMode: "repeat",
          // Opt in only if this backend may reseed safe invalidated sessions
          // from bounded raw OpenClaw transcript history before compaction.
          reseedFromRawTranscriptWhenUncompacted: true,
          serialize: true,
        },
      },
    },
  },
}

How it works

  1. Selects a backend based on the provider prefix (claude-cli/...).
  2. Builds a system prompt using the same OpenClaw prompt + workspace context.
  3. Executes the CLI with a session id (if supported) so history stays consistent. The bundled claude-cli backend keeps a Claude stdio process alive per OpenClaw session and sends follow-up turns over stream-json stdin.
  4. Parses output (JSON or plain text) and returns the final text.
  5. Persists session ids per backend, so follow-ups reuse the same CLI session.
The bundled Anthropic `claude-cli` backend is supported again. Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so OpenClaw treats `claude -p` usage as sanctioned for this integration unless Anthropic publishes a new policy.

The bundled Anthropic claude-cli backend receives the OpenClaw skills snapshot two ways: the compact OpenClaw skills catalog in the appended system prompt, and a temporary Claude Code plugin passed with --plugin-dir. The plugin contains only the eligible skills for that agent/session, so Claude Code's native skill resolver sees the same filtered set that OpenClaw would otherwise advertise in the prompt. Skill env/API key overrides are still applied by OpenClaw to the child process environment for the run.

Claude CLI also has its own noninteractive permission mode. OpenClaw maps that to the existing exec policy instead of adding Claude-specific policy config. For OpenClaw-managed Claude live sessions, the effective OpenClaw exec policy is authoritative: YOLO (tools.exec.security: "full" and tools.exec.ask: "off") launches Claude with --permission-mode bypassPermissions, while restrictive effective exec policy launches Claude with --permission-mode default. Per-agent agents.list[].tools.exec settings override global tools.exec for that agent. Raw Claude backend args may still include --permission-mode, but live Claude launches normalize that flag to match the effective OpenClaw exec policy.

The bundled Anthropic claude-cli backend also maps OpenClaw /think levels to Claude Code's native --effort flag for non-off levels. minimal and low map to low, adaptive and medium map to medium, and high, xhigh, and max map directly. Other CLI backends need their owning plugin to declare an equivalent argv mapper before /think can affect the spawned CLI.

Before OpenClaw can use the bundled claude-cli backend, Claude Code itself must already be logged in on the same host:

claude auth login
claude auth status --text
openclaw models auth login --provider anthropic --method cli --set-default

Use agents.defaults.cliBackends.claude-cli.command only when the claude binary is not already on PATH.

Sessions

  • If the CLI supports sessions, set sessionArg (e.g. --session-id) or sessionArgs (placeholder {sessionId}) when the ID needs to be inserted into multiple flags.
  • If the CLI uses a resume subcommand with different flags, set resumeArgs (replaces args when resuming) and optionally resumeOutput (for non-JSON resumes).
  • sessionMode:
    • always: always send a session id (new UUID if none stored).
    • existing: only send a session id if one was stored before.
    • none: never send a session id.
  • claude-cli defaults to liveSession: "claude-stdio", output: "jsonl", and input: "stdin" so follow-up turns reuse the live Claude process while it is active. Warm stdio is the default now, including for custom configs that omit transport fields. If the Gateway restarts or the idle process exits, OpenClaw resumes from the stored Claude session id. Stored session ids are verified against an existing readable project transcript before resume, so phantom bindings are cleared with reason=transcript-missing instead of silently starting a fresh Claude CLI session under --resume.
  • Claude live sessions keep bounded JSONL output guards. Defaults allow up to 8 MiB and 20,000 raw JSONL lines per turn. Tool-heavy Claude turns can raise them per backend with agents.defaults.cliBackends.claude-cli.reliability.outputLimits.maxTurnRawChars and maxTurnLines; OpenClaw clamps those settings to 64 MiB and 100,000 lines.
  • Stored CLI sessions are provider-owned continuity. The implicit daily session reset does not cut them; /reset and explicit session.reset policies still do.
  • Fresh CLI sessions normally reseed only from OpenClaw's compaction summary plus post-compaction tail. To recover short sessions that are invalidated before compaction, a backend can opt in with reseedFromRawTranscriptWhenUncompacted: true. OpenClaw still keeps raw transcript reseed bounded and limits it to safe invalidations such as missing CLI transcripts, system-prompt/MCP changes, or session-expired retry; auth profile or credential-epoch changes never reseed raw transcript history.

Serialization notes:

  • serialize: true keeps same-lane runs ordered.
  • Most CLIs serialize on one provider lane.
  • OpenClaw drops stored CLI session reuse when the selected auth identity changes, including a changed auth profile id, static API key, static token, or OAuth account identity when the CLI exposes one. OAuth access and refresh token rotation does not cut the stored CLI session. If a CLI does not expose a stable OAuth account id, OpenClaw lets that CLI enforce resume permissions.

Fallback prelude from claude-cli sessions

When a claude-cli attempt fails over to a non-CLI candidate in agents.defaults.model.fallbacks, OpenClaw seeds the next attempt with a context prelude harvested from Claude Code's local JSONL transcript at ~/.claude/projects/. Without this seed, the fallback provider would start cold because OpenClaw's own session transcript is empty for claude-cli runs.

  • The prelude prefers the latest /compact summary or compact_boundary marker, then appends the most recent post-boundary turns up to a char budget. Pre-boundary turns are dropped because the summary already represents them.
  • Tool blocks are coalesced to compact (tool call: name) and (tool result: …) hints to keep the prompt budget honest. The summary is labeled (truncated) if it overflows.
  • Same-provider claude-cli to claude-cli fallbacks rely on Claude's own --resume and skip the prelude.
  • The seed reuses the existing Claude session-file path validation, so arbitrary paths cannot be read.

Images (pass-through)

If your CLI accepts image paths, set imageArg:

imageArg: "--image",
imageMode: "repeat"

OpenClaw will write base64 images to temp files. If imageArg is set, those paths are passed as CLI args. If imageArg is missing, OpenClaw appends the file paths to the prompt (path injection), which is enough for CLIs that auto- load local files from plain paths.

Inputs / outputs

  • output: "json" (default) tries to parse JSON and extract text + session id.
  • For Gemini CLI JSON output, OpenClaw reads reply text from response and usage from stats when usage is missing or empty.
  • output: "jsonl" parses JSONL streams and extracts the final agent message plus session identifiers when present.
  • output: "text" treats stdout as the final response.

Input modes:

  • input: "arg" (default) passes the prompt as the last CLI arg.
  • input: "stdin" sends the prompt via stdin.
  • If the prompt is very long and maxPromptArgChars is set, stdin is used.

Defaults (plugin-owned)

Bundled CLI backend defaults live with their owning plugin. For example, Anthropic owns claude-cli and Google owns google-gemini-cli. OpenAI Codex agent runs use the Codex app-server harness through openai/*; OpenClaw no longer registers a bundled codex-cli backend.

The bundled Anthropic plugin registers a default for claude-cli:

  • command: "claude"
  • args: ["-p","--output-format","stream-json","--include-partial-messages","--verbose", ...]
  • output: "jsonl"
  • input: "stdin"
  • modelArg: "--model"
  • sessionMode: "always"

The bundled Google plugin also registers a default for google-gemini-cli:

  • command: "gemini"
  • args: ["--output-format", "json", "--prompt", "{prompt}"]
  • resumeArgs: ["--resume", "{sessionId}", "--output-format", "json", "--prompt", "{prompt}"]
  • imageArg: "@"
  • imagePathScope: "workspace"
  • modelArg: "--model"
  • sessionMode: "existing"
  • sessionIdFields: ["session_id", "sessionId"]

Prerequisite: the local Gemini CLI must be installed and available as gemini on PATH (brew install gemini-cli or npm install -g @google/gemini-cli).

Gemini CLI JSON notes:

  • Reply text is read from the JSON response field.
  • Usage falls back to stats when usage is absent or empty.
  • stats.cached is normalized into OpenClaw cacheRead.
  • If stats.input is missing, OpenClaw derives input tokens from stats.input_tokens - stats.cached.

Override only if needed (common: absolute command path).

Plugin-owned defaults

CLI backend defaults are now part of the plugin surface:

  • Plugins register them with api.registerCliBackend(...).
  • The backend id becomes the provider prefix in model refs.
  • User config in agents.defaults.cliBackends.<id> still overrides the plugin default.
  • Backend-specific config cleanup stays plugin-owned through the optional normalizeConfig hook.

Plugins that need tiny prompt/message compatibility shims can declare bidirectional text transforms without replacing a provider or CLI backend:

api.registerTextTransforms({
  input: [
    { from: /red basket/g, to: "blue basket" },
    { from: /paper ticket/g, to: "digital ticket" },
    { from: /left shelf/g, to: "right shelf" },
  ],
  output: [
    { from: /blue basket/g, to: "red basket" },
    { from: /digital ticket/g, to: "paper ticket" },
    { from: /right shelf/g, to: "left shelf" },
  ],
});

input rewrites the system prompt and user prompt passed to the CLI. output rewrites streamed assistant deltas and parsed final text before OpenClaw handles its own control markers and channel delivery.

For CLIs that emit Claude Code stream-json compatible JSONL, set jsonlDialect: "claude-stream-json" on that backend's config.

Bundle MCP overlays

CLI backends do not receive OpenClaw tool calls directly, but a backend can opt into a generated MCP config overlay with bundleMcp: true.

Current bundled behavior:

  • claude-cli: generated strict MCP config file
  • google-gemini-cli: generated Gemini system settings file

When bundle MCP is enabled, OpenClaw:

  • spawns a loopback HTTP MCP server that exposes gateway tools to the CLI process
  • authenticates the bridge with a per-session token (OPENCLAW_MCP_TOKEN)
  • scopes tool access to the current session, account, and channel context
  • loads enabled bundle-MCP servers for the current workspace
  • merges them with any existing backend MCP config/settings shape
  • rewrites the launch config using the backend-owned integration mode from the owning extension

If no MCP servers are enabled, OpenClaw still injects a strict config when a backend opts into bundle MCP so background runs stay isolated.

Session-scoped bundled MCP runtimes are cached for reuse within a session, then reaped after mcp.sessionIdleTtlMs milliseconds of idle time (default 10 minutes; set 0 to disable). One-shot embedded runs such as auth probes, slug generation, and active-memory recall request cleanup at run end so stdio children and Streamable HTTP/SSE streams do not outlive the run.

Limitations

  • No direct OpenClaw tool calls. OpenClaw does not inject tool calls into the CLI backend protocol. Backends only see gateway tools when they opt into bundleMcp: true.
  • Streaming is backend-specific. Some backends stream JSONL; others buffer until exit.
  • Structured outputs depend on the CLI's JSON format.

Troubleshooting

  • CLI not found: set command to a full path.
  • Wrong model name: use modelAliases to map provider/model → CLI model.
  • No session continuity: ensure sessionArg is set and sessionMode is not none.
  • Images ignored: set imageArg (and verify CLI supports file paths).