Building CLI Backend Plugins for OpenClaw

Learn how to build a CLI backend plugin that lets OpenClaw use a local AI command-line tool as a text inference backend. This guide is for developers integrating existing local CLIs or creating fallbacks when APIs are down.

Read this when

  • 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 enable OpenClaw to treat a local AI command-line tool as a text inference backend. In model references, this backend shows up as a provider prefix:

acme-cli/acme-large

Reach for a CLI backend when the upstream integration already exists as a local command, when login state lives inside the CLI itself, or when API providers are down and you need a fallback.

Info

Should the upstream service present a standard HTTP model API, a provider plugin is the better choice. When the upstream runtime manages complete agent sessions, tool events, compaction, or background task state, opt for an agent harness.

What the plugin owns

A CLI backend plugin is built from 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

Discovery relies on the manifest, which is pure metadata: it neither runs the CLI nor registers runtime behavior. Actual runtime behavior begins once the plugin entry invokes api.registerCliBackend(...).

Minimal backend plugin

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"
  }
}

Any published package must include built JavaScript runtime files. When your source entry is ./src/index.ts, add openclaw.runtimeExtensions that points to the compiled JavaScript peer. For details, check Entry points.

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
  }
}

The runtime ownership list, cliBackends, allows OpenClaw to load the plugin automatically whenever model selection or agentRuntime.id references acme-cli.

For descriptor-first setup, use setup.cliBackends. Add it when model discovery, onboarding, or status should recognize the backend without loading the plugin runtime. Reserve requiresRuntime: false for cases where those static descriptors alone suffice for configuration.

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", "--output-format", "stream-json", "--prompt", "{prompt}"],
      resumeArgs: [
        "chat",
        "--resume",
        "{sessionId}",
        "--output-format",
        "stream-json",
        "--prompt",
        "{prompt}",
      ],
      output: "jsonl",
      resumeOutput: "jsonl",
      jsonlDialect: "gemini-stream-json",
      input: "arg",
      modelArg: "--model",
      modelAliases: {
        large: "acme-large-2026",
        fast: "acme-fast-2026",
      },
      sessionArgs: ["--session", "{sessionId}"],
      sessionMode: "existing",
      sessionIdFields: ["session_id", "conversation_id"],
      systemPromptFileArg: "--system-file",
      systemPromptWhen: "first",
      imageArg: "--image",
      imageMode: "repeat",
      imagePathScope: "workspace",
      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 align with the manifest's cliBackends entry. The registered adapter carries the authoritative plugin code; OpenClaw config picks the backend but never alters its command contract.

Config shape

CliBackendConfig specifies how OpenClaw should start the CLI and interpret its output. The worked example above deliberately covers the same command, resume, JSONL, model-alias, session, image, and watchdog fields as the built-in google-gemini-cli adapter:

FieldPurpose
commandExecutable name or full command path
argsBase argv used when starting a new run
resumeArgsAlternate argv for continued sessions; accepts {sessionId}
output / resumeOutputParser choice: json, jsonl, or text
jsonlDialectJSONL event format: claude-stream-json or gemini-stream-json
liveSessionPersistent CLI process handling (claude-stdio)
inputPrompt delivery: arg or stdin
maxPromptArgCharsPrompt size cap for arg mode before switching to stdin
env / clearEnvEnvironment variables to add, or names to remove before startup
modelArgFlag placed before the model identifier
modelAliasesTranslate OpenClaw model ids into CLI-specific ids
sessionArgsSession id passing mechanism via {sessionId}
sessionModealways, existing, or none
sessionIdFieldsJSON fields that OpenClaw extracts from CLI output
systemPromptArg / systemPromptFileArgSystem prompt delivery method
systemPromptFileConfigArg / systemPromptFileConfigKeyConfig override for a system prompt file path (e.g. -c)
systemPromptModeappend or replace
systemPromptWhenfirst, always, or never
imageArg / imageModeImage path flag and multi-image handling (repeat or list)
imagePathScopeTemporary image storage location before transfer: temp or workspace
serializeEnforce ordering for runs on the same backend
reseedFromRawTranscriptWhenUncompactedEnable bounded raw-transcript reseed before compaction for safe session restarts
reliability.watchdogOutput timeout settings, distinct for new and resumed runs

Keep the static config as minimal as possible while still matching the CLI. Only add plugin callbacks for logic that genuinely belongs to the backend.

Advanced backend hooks

Additional definitions are possible through CliBackendPlugin:

HookUse
normalizeConfig(config, context)Bring the registered static adapter in line with runtime context
resolveExecutionArgs(ctx)Attach per-request flags, for instance thinking effort or side-question isolation
prepareExecution(ctx)Stand up temporary auth, config, or environment bridges prior to launch
transformSystemPrompt(ctx)Run a final CLI-oriented system prompt transformation
textTransformsTwo-way prompt and output substitution
defaultAuthProfileIdFavor a designated OpenClaw auth profile
authEpochModeDetermine how auth changes invalidate stored CLI sessions
nativeToolModeState whether native tools are missing, always enabled, or host-selectable
toolAvailabilityEnforcementState whether exact tool caps are imposed in argv or during execution staging
sideQuestionToolModeState disabled native tools for /btw side questions
bundleMcp / bundleMcpModeChoose OpenClaw's loopback MCP tool bridge
ownsNativeCompactionBackend handles its own automatic compaction, OpenClaw stays out
manualCompactionAtomic command, transport, and positive-acknowledgement contract
subscriptionAuthDispatchOpted-in embedded runs on subscription credentials execute via this backend
runtimeArtifactTie a script launcher to its full bundled package tree
liveSessionRequirementDemand an init capability before trusting long-lived session output

These hooks stay provider-owned. Avoid adding CLI-specific branches to core when a backend hook can express the behavior.

liveSessionRequirement declares one exact capability that the CLI must advertise in its initialization record before OpenClaw trusts streamed output. It also supplies the first known compatible version, version-probe arguments, and update command used by setup and Doctor. Runtime support remains capability-based, so a compatible backport or wrapper is not rejected only because of its version string.

prepareExecution(ctx) receives ctx.contextTokenBudget, the effective token limit selected for the run. Backends that own native compaction can map that budget into their CLI-specific launch contract. It also receives the optional effective ctx.thinkingLevel: off, minimal, low, medium, high, xhigh, adaptive, or max. Use that field when the selected level must be applied through launch environment or staged configuration; the same field is available to resolveExecutionArgs(ctx) for native CLI flags.

runtimeArtifact is plugin-owned. It is consulted only when a live inference turn mints or revalidates verified setup authority; normal CLI runs do not require it. A backend without this declaration cannot mint verified CLI setup authority. A bundled-package-tree declaration names the exact package.json owner and requires the package entrypoint to be the command. OpenClaw hashes the bounded complete installed package tree, including nested dependencies, and fails closed for redirecting symlinks, launchers outside the declared package, required external dependency declarations, oversized trees, and unknown scripts. Declare this only when that tree contains the complete inference implementation; optional tool integrations do not make an external implementation graph safe.

If the same backend also ships a self-contained native executable, list its canonical basenames in nativeExecutableNames. Other native commands remain unverified.

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.

Set nativeToolMode: "selectable" only when the backend can disable every backend-native tool for an individual run. Restricted runs receive a canonical contract: ctx.toolAvailability.native is the exact backend-native list and ctx.toolAvailability.openClaw is the exact list of OpenClaw tool names. The host independently limits the generated MCP configuration and grant to that OpenClaw list; plugins must not translate it in core or add transport prefixes.

Declare how the backend enforces that contract:

  • toolAvailabilityEnforcement: "execution-args" requires resolveExecutionArgs. The hook must replace conflicting tool flags, disable customization surfaces that can execute outside the selected tools, and return enforcing argv for both fresh and resumed runs.
  • toolAvailabilityEnforcement: "prepare-execution" requires prepareExecution. The hook must stage an exact per-run policy and return toolAvailabilityEnforced: true; missing acknowledgement fails closed and OpenClaw cleans up the staged resources before launch.

Runtime caps such as cron toolsAllow are normalized and group-expanded by OpenClaw before this contract is built. Native tools are disabled, and a backend without a complete declared enforcement path fails before execution.

Plugins built against v2026.7.2-beta.1 through v2026.7.2-beta.3 may still read the deprecated ctx.toolAvailability.mcp transport-name projection and may omit toolAvailabilityEnforcement when a selectable backend implements resolveExecutionArgs. OpenClaw recognizes that shipped beta path from the plugin package's required openclaw.build.openclawVersion metadata and preserves it through the 2026.8.x line. New and updated plugins should use canonical ctx.toolAvailability.openClaw names and declare toolAvailabilityEnforcement: "execution-args" explicitly; the beta compatibility path is scheduled for removal after that window.

parseJsonlEvent: provider-specific JSONL streams

Set parseJsonlEvent when a backend emits line-delimited JSON that does not match the built-in Claude, Codex, or Gemini dialects. The hook receives one raw line plus the resolved backend id and config, and returns one normalized event, multiple events, or null to let the built-in parser try the line.

Supported events are incremental assistant text, incremental thinking, native tool start/result display, session ids, and terminal results. Terminal results may include final text, usage, an error, and a successor session id. Session ids reported by either event shape participate in resumed-session and fork persistence.

Tool events describe work the backend already performed. OpenClaw renders and summarizes them, but does not treat them as host tool execution, trusted diagnostics, loopback correlation, or message-delivery evidence.

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 - automatic CLI compaction defers to the backend and the turn proceeds. claude-cli declares it because Claude Code compacts internally with no harness endpoint. It also declares manualCompaction, so an explicit OpenClaw /compact resumes the bound Claude Code session and invokes its native /compact command without recording a conversation turn. 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 or 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 (for example --resume / --session-id);
  • it is not a native-harness compaction session - matching agentHarnessId sessions route to the harness endpoint instead.

If the backend supports an in-place manual command, declare it alongside the ownership flag:

manualCompaction: {
  buildPrompt: (instructions) =>
    instructions ? `/compact ${instructions}` : "/compact",
  input: "arg",
  validateOutput: (rawOutput) =>
    rawOutput.includes('"type":"compaction_complete"')
      ? { ok: true }
      : { ok: false, reason: "CLI did not confirm compaction." },
},

The builder receives optional /compact instructions. The validator receives the bounded raw process output and must require a backend-owned positive acknowledgement; a zero exit alone is not proof of compaction. Do not declare this capability for a command that creates a separate session or requires an ordinary model turn.

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:

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. If it can disable every native tool per run, use "selectable" with the resolveExecutionArgs contract above.

Selecting the backend

Users select a standalone backend through its model-ref prefix. A backend that declares a canonical modelProvider can instead be selected through that provider model's agentRuntime.id. Adapter mechanics remain in the plugin:

{
  agents: {
    defaults: {
      model: {
        primary: "openai/gpt-5.6-sol",
        fallbacks: ["acme-cli/large"],
      },
    },
  },
}

Put credentials in OpenClaw auth profiles or plugin-owned config. Ensure the registered command is on the gateway service's PATH; deployments that need a different path or argv should change or wrap the plugin registration.

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

Check

package.json has openclaw.extensions and built runtime entries for published packages

Check

openclaw.plugin.json declares cliBackends and intentional activation.onStartup

Check

setup.cliBackends is present when setup/model discovery should see the backend cold

Check

api.registerCliBackend(...) uses the same backend id as the manifest

Check

The backend model prefix or model-scoped agentRuntime.id selects the registration

Check

Session, system prompt, image, and output parser settings match the real CLI contract

Check

Targeted tests and at least one live CLI smoke prove the backend path

2,533 words · updated Aug 25, 2026