Skip to content

Building CLI backend plugins

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.

A CLI backend plugin has three contracts:

ContractFilePurpose
Package entrypackage.jsonPoints OpenClaw at the plugin runtime module
Manifest ownershipopenclaw.plugin.jsonDeclares the backend id before runtime loads
Runtime registrationindex.tsCalls 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(...).

  1. Create package metadata

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

  2. Declare backend ownership

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

  3. Register the backend

    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.

CliBackendConfig describes how OpenClaw should launch and parse the CLI:

FieldUse
commandBinary name or absolute command path
argsBase argv for fresh runs
resumeArgsAlternate argv for resumed sessions; supports {sessionId}
output / resumeOutputParser: json, jsonl, or text
inputPrompt transport: arg or stdin
modelArgFlag used before the model id
modelAliasesMap OpenClaw model ids to CLI-native ids
sessionArg / sessionArgsHow to pass a session id
sessionModealways, existing, or none
sessionIdFieldsJSON fields OpenClaw reads from CLI output
systemPromptArg / systemPromptFileArgSystem prompt transport
systemPromptWhenfirst, always, or never
imageArg / imageModeImage path support
serializeKeep same-backend runs ordered
reliability.watchdogNo-output timeout tuning

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

CliBackendPlugin can also define:

HookUse
normalizeConfig(config, context)Rewrite legacy user config after merge
resolveExecutionArgs(ctx)Add request-scoped flags such as thinking effort or side-question isolation
prepareExecution(ctx)Create temporary auth or config bridges before launch
transformSystemPrompt(ctx)Apply a final CLI-specific system prompt transform
textTransformsBidirectional prompt/output replacements
defaultAuthProfileIdPrefer a specific OpenClaw auth profile
authEpochModeDecide how auth changes invalidate stored CLI sessions
nativeToolModeDeclare whether the CLI has always-on native tools
sideQuestionToolModeDeclare disabled native tools for /btw side questions
bundleMcp / bundleMcpModeOpt into OpenClaw’s loopback MCP tool bridge
ownsNativeCompactionBackend owns its own compaction - OpenClaw defers

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

ctx.executionMode is "agent" for normal turns and "side-question" for ephemeral /btw calls. Use it when the CLI needs different one-shot flags, such as disabling native tools, session persistence, or resume behavior for BTW. If a backend normally has nativeToolMode: "always-on" but its side-question argv reliably disables those tools, also set sideQuestionToolMode: "disabled"; otherwise OpenClaw fails closed when BTW requires a no-tools CLI run.

ownsNativeCompaction: opting out of OpenClaw compaction

Section titled “ownsNativeCompaction: opting out of OpenClaw compaction”

If your backend runs an agent that compacts its own transcript, set ownsNativeCompaction: true so OpenClaw’s safeguard summarizer never runs against its sessions - the CLI compaction lifecycle returns a no-op and the turn proceeds. claude-cli declares it because Claude Code compacts internally with no harness endpoint. Native-harness sessions such as Codex keep routing to their harness compaction endpoint instead.

Only declare it when all of the following hold, or a deferred over-budget session can stay over budget / go stale (OpenClaw no longer rescues it):

  • the backend reliably compacts or bounds its own transcript as it nears its window;
  • it persists a resumable session so the compacted state survives turns (e.g. --resume / --session-id);
  • it is not a native-harness compaction session - matching agentHarnessId sessions route to the harness endpoint instead.

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:

ModeUse
claude-config-fileCLIs that accept an MCP config file
codex-config-overridesCLIs that accept config overrides on argv
gemini-system-settingsCLIs 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.

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.

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

Terminal window
pnpm test extensions/acme-cli

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

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