Files
openclaw/docs/plugins/cli-backend-plugins.md
Peter Steinberger 42ecd5d95e fix(acpx): harden session lifecycle cleanup
Harden ACPX process cleanup with lease-backed ownership verification, startup orphan reaping, reusable cancel semantics, and spawned-session visibility fixes.
2026-05-07 07:30:37 +01:00

12 KiB

summary, title, sidebarTitle, read_when
summary title sidebarTitle read_when
Build a plugin that registers a local AI CLI backend Building CLI backend plugins CLI backend plugins
You are building a local AI CLI backend plugin
You want to register a backend for model refs such as acme-cli/model
You need to map a third-party CLI into OpenClaw's text fallback runner

CLI backend plugins let OpenClaw call a local AI CLI as a text inference backend. The backend appears as a provider prefix in model refs:

acme-cli/acme-large

Use a CLI backend when the upstream integration is already exposed as a local command, when the CLI owns local login state, or when the CLI is a useful fallback if API providers are unavailable.

If the upstream service exposes a normal HTTP model API, write a [provider plugin](/plugins/sdk-provider-plugins) instead. If the upstream runtime owns complete agent sessions, tool events, compaction, or background task state, use an [agent harness](/plugins/sdk-agent-harness).

What the plugin owns

A CLI backend plugin has three contracts:

Contract File Purpose
Package entry package.json Points OpenClaw at the plugin runtime module
Manifest ownership openclaw.plugin.json Declares the backend id before runtime loads
Runtime registration index.ts Calls api.registerCliBackend(...) with command defaults

The manifest is discovery metadata. It does not execute the CLI and does not register runtime behavior. Runtime behavior starts when the plugin entry calls api.registerCliBackend(...).

Minimal backend plugin

```json package.json { "name": "@acme/openclaw-acme-cli", "version": "1.0.0", "type": "module", "openclaw": { "extensions": ["./index.ts"], "compat": { "pluginApi": ">=2026.3.24-beta.2", "minGatewayVersion": "2026.3.24-beta.2" }, "build": { "openclawVersion": "2026.3.24-beta.2", "pluginSdkVersion": "2026.3.24-beta.2" } }, "dependencies": { "openclaw": "^2026.3.24" }, "devDependencies": { "typescript": "^5.9.0" } } ```
Published packages must ship built JavaScript runtime files. If your source
entry is `./src/index.ts`, add `openclaw.runtimeExtensions` that points at
the built JavaScript peer. See [Entry points](/plugins/sdk-entrypoints).
```json openclaw.plugin.json { "id": "acme-cli", "name": "Acme CLI", "description": "Run Acme's local AI CLI through OpenClaw", "cliBackends": ["acme-cli"], "setup": { "cliBackends": ["acme-cli"], "requiresRuntime": false }, "activation": { "onStartup": false }, "configSchema": { "type": "object", "additionalProperties": false } } ```
`cliBackends` is the runtime ownership list. It lets OpenClaw auto-load the
plugin when config or model selection mentions `acme-cli/...`.

`setup.cliBackends` is the descriptor-first setup surface. Add it when
model discovery, onboarding, or status should recognize the backend without
loading plugin runtime. Use `requiresRuntime: false` only when those static
descriptors are enough for setup.
```typescript index.ts import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { CLI_FRESH_WATCHDOG_DEFAULTS, CLI_RESUME_WATCHDOG_DEFAULTS, type CliBackendPlugin, } from "openclaw/plugin-sdk/cli-backend";
function buildAcmeCliBackend(): CliBackendPlugin {
  return {
    id: "acme-cli",
    liveTest: {
      defaultModelRef: "acme-cli/acme-large",
      defaultImageProbe: false,
      defaultMcpProbe: false,
      docker: {
        npmPackage: "@acme/acme-cli",
        binaryName: "acme",
      },
    },
    config: {
      command: "acme",
      args: ["chat", "--json"],
      output: "json",
      input: "stdin",
      modelArg: "--model",
      sessionArg: "--session",
      sessionMode: "existing",
      sessionIdFields: ["session_id", "conversation_id"],
      systemPromptFileArg: "--system-file",
      systemPromptWhen: "first",
      imageArg: "--image",
      imageMode: "repeat",
      reliability: {
        watchdog: {
          fresh: { ...CLI_FRESH_WATCHDOG_DEFAULTS },
          resume: { ...CLI_RESUME_WATCHDOG_DEFAULTS },
        },
      },
      serialize: true,
    },
  };
}

export default definePluginEntry({
  id: "acme-cli",
  name: "Acme CLI",
  description: "Run Acme's local AI CLI through OpenClaw",
  register(api) {
    api.registerCliBackend(buildAcmeCliBackend());
  },
});
```

The backend id must match the manifest `cliBackends` entry. The registered
`config` is only the default; user config under
`agents.defaults.cliBackends.acme-cli` is merged over it at runtime.

Config shape

CliBackendConfig describes how OpenClaw should launch and parse the CLI:

Field Use
command Binary name or absolute command path
args Base argv for fresh runs
resumeArgs Alternate argv for resumed sessions; supports {sessionId}
output / resumeOutput Parser: json, jsonl, or text
input Prompt transport: arg or stdin
modelArg Flag used before the model id
modelAliases Map OpenClaw model ids to CLI-native ids
sessionArg / sessionArgs How to pass a session id
sessionMode always, existing, or none
sessionIdFields JSON fields OpenClaw reads from CLI output
systemPromptArg / systemPromptFileArg System prompt transport
systemPromptWhen first, always, or never
imageArg / imageMode Image path support
serialize Keep same-backend runs ordered
reliability.watchdog No-output timeout tuning

Prefer the smallest static config that matches the CLI. Add plugin callbacks only for behavior that really belongs to the backend.

Advanced backend hooks

CliBackendPlugin can also define:

Hook Use
normalizeConfig(config, context) Rewrite legacy user config after merge
resolveExecutionArgs(ctx) Add request-scoped flags such as thinking effort
prepareExecution(ctx) Create temporary auth or config bridges before launch
transformSystemPrompt(ctx) Apply a final CLI-specific system prompt transform
textTransforms Bidirectional prompt/output replacements
defaultAuthProfileId Prefer a specific OpenClaw auth profile
authEpochMode Decide how auth changes invalidate stored CLI sessions
nativeToolMode Declare whether the CLI has always-on native tools
bundleMcp / bundleMcpMode Opt into OpenClaw's loopback MCP tool bridge

Keep these hooks provider-owned. Do not add CLI-specific branches to core when a backend hook can express the behavior.

MCP tool bridge

CLI backends do not receive OpenClaw tools by default. If the CLI can consume an MCP configuration, opt in explicitly:

return {
  id: "acme-cli",
  bundleMcp: true,
  bundleMcpMode: "codex-config-overrides",
  config: {
    command: "acme",
    args: ["chat", "--json"],
    output: "json",
  },
};

Supported bridge modes are:

Mode Use
claude-config-file CLIs that accept an MCP config file
codex-config-overrides CLIs that accept config overrides on argv
gemini-system-settings CLIs that read MCP settings from their system settings directory

Only enable the bridge when the CLI can actually consume it. If the CLI has its own built-in tool layer that cannot be disabled, set nativeToolMode: "always-on" so OpenClaw can fail closed when a caller requires no native tools.

User configuration

Users can override any backend default:

{
  agents: {
    defaults: {
      cliBackends: {
        "acme-cli": {
          command: "/opt/acme/bin/acme",
          args: ["chat", "--json", "--profile", "work"],
          modelAliases: {
            large: "acme-large-2026",
          },
        },
      },
      model: {
        primary: "openai/gpt-5.5",
        fallbacks: ["acme-cli/large"],
      },
    },
  },
}

Document the minimum override users are likely to need. Usually that is only command when the binary is outside PATH.

Verification

For bundled plugins, add a focused test around the builder and setup registration, then run the plugin's targeted test lane:

pnpm test extensions/acme-cli

For local or installed plugins, verify discovery and one real model run:

openclaw plugins inspect acme-cli --runtime --json
openclaw agent --message "reply exactly: backend ok" --model acme-cli/acme-large

If the backend supports images or MCP, add a live smoke that proves those paths with the real CLI. Do not rely on static inspection for prompt, image, MCP, or session-resume behavior.

Checklist

package.json has openclaw.extensions and built runtime entries for published packages openclaw.plugin.json declares cliBackends and intentional activation.onStartup setup.cliBackends is present when setup/model discovery should see the backend cold api.registerCliBackend(...) uses the same backend id as the manifest User overrides under agents.defaults.cliBackends.<id> still win Session, system prompt, image, and output parser settings match the real CLI contract Targeted tests and at least one live CLI smoke prove the backend path