Building plugins

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:

text
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 as a fallback when API providers are unavailable.

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 or register runtime behavior. Runtime behavior starts when the plugin entry calls api.registerCliBackend(...).

Minimal backend plugin

  • Create package metadata

    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 pointing at the built JavaScript peer. See Entry points.

  • Declare backend ownership

    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 model selection or agentRuntime.id 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.

  • Register the backend

    index.ts
    import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; function buildAcmeCliBackend(): Parameters<OpenClawPluginApi["registerCliBackend"]>[0] {  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",      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 adapter is authoritative plugin code; OpenClaw config selects the backend but does not rewrite its command contract.

  • Config shape

    CliBackendConfig describes how OpenClaw should launch and parse the CLI. The worked example above intentionally exercises the same command, resume, JSONL, model-alias, session, and image fields as the bundled google-gemini-cli adapter:

    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
    jsonlDialect JSONL event dialect: claude-stream-json or gemini-stream-json
    liveSession Long-lived CLI process mode (claude-stdio)
    input Prompt transport: arg or stdin
    maxPromptArgChars Max prompt length for arg mode before falling back to stdin
    env / clearEnv Extra env vars to inject, or names to strip before launch
    modelArg Flag used before the model id
    modelAliases Map OpenClaw model ids to CLI-native ids
    sessionArgs How to pass a session id using {sessionId}
    sessionMode always, existing, or none
    sessionIdFields JSON fields OpenClaw reads from CLI output
    systemPromptArg / systemPromptFileArg System prompt transport
    systemPromptFileConfigArg / systemPromptFileConfigKey Config-override transport for a system prompt file (for example -c)
    systemPromptMode append or replace
    systemPromptWhen first, always, or never
    imageArg / imageMode Image path flag and how to pass multiple images (repeat or list)
    imagePathScope Where staged image files live before handoff: temp or workspace
    serialize Keep same-backend runs ordered
    reseedFromRawTranscriptWhenUncompacted Opt in to bounded raw-transcript reseed before compaction for safe session resets
    freshSessionRecovery Fresh recovery policy after a recoverable resumed-session failure
    reliability.watchdog No-output timeout tuning, separate for fresh vs resumed runs

    claude-stream-json is more than a parser choice: it declares that the backend's result records carry Claude Code's terminal semantics, including terminal_reason. A reply-less result whose terminal_reason is hook_stopped, stop_hook_prevented, aborted_tools, aborted_streaming, budget_exhausted, or max_turns is a recorded turn stop: OpenClaw reports that reason to the user and does not replay the turn on a fallback model, because the backend's tool actions may already have run.

    Omit reliability.watchdog to inherit the standard profiles, including the longer resumed-run budget for cron and explicit timeouts. Set it only when a backend intentionally needs its own watchdog policy.

    freshSessionRecovery is a backend-owned compatibility contract:

    • Leave it undefined or set it to "replace-binding" to preserve the legacy clear-and-reseed behavior. OpenClaw clears the persisted binding and retries with a fresh session when the failure is eligible for recovery.
    • Set it to "invalidated-only" to suppress fresh replacement unless the canonical invalidation predicate proves the old session is dead. Only session_expired does so.

    Choose the value from the CLI or SDK session contract, not from a provider id or broad error class. The bundled Anthropic backend uses "invalidated-only"; its native session contract does not treat non-expiration failures as proof that the conversation can no longer resume.

    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) Normalize the registered static adapter with runtime context
    resolveExecutionArgs(ctx) Add request-scoped flags such as thinking effort or side-question isolation
    prepareExecution(ctx) Create temporary auth, config, or environment 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 native tools are absent, always on, or host-selectable
    toolAvailabilityEnforcement Declare whether exact tool caps are enforced in argv or execution staging
    projectNativeToolAuthority Map the observed native tool list to canonical capabilities for cron caps
    sideQuestionToolMode Declare disabled native tools for /btw side questions
    bundleMcp / bundleMcpMode Opt into OpenClaw's loopback MCP tool bridge
    ownsNativeCompaction Backend owns its own automatic compaction - OpenClaw defers
    manualCompaction Atomic command, transport, and positive-acknowledgement contract
    subscriptionAuthDispatch Opted-in embedded runs on subscription credentials execute via this backend
    runtimeArtifact Bound a script launcher to its complete bundled package tree

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

    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.

    resolveExecutionArgs(ctx) also receives optional ctx.fastMode, the effective boolean for this invocation. Core resolves automatic mode after CLI and process scope admission and backend preparation, so elapsed waits count toward its cutoff. Explicit on and off remain unchanged. The field follows the session, agent, and model fast-mode settings; backends may map it to their native arguments or ignore it. A spawned process keeps that decision for the invocation; it does not receive raw "auto".

    prepareExecution(ctx) may also return an optional execute transport when a backend owns the installed CLI's protocol or SDK integration. The transport receives the exact prepared command, arguments, optional argv0, environment, prompt, session, and tool availability; it yields the backend's existing structured stream records. Preserve the prepared command, argv0, and interpreter or script prefix in args when constructing the CLI invocation. argv0 preserves the invocation name of a PATH shim. Optional promptContext.prependContext and promptContext.appendContext are private prompt-build additions and bounded saved session notes, separate from the ordinary prompt. Saved notes are quoted reference data and may repeat on resumed turns; they do not assert that a native turn previously consumed them. Transport them through the native runtime's private context mechanism; never record them as operator-authored input. OpenClaw's policy and observation hooks still receive the complete logical prompt. Native tool actions must use the provided, run-bound requestToolPermission callback rather than creating independent approval authority. OpenClaw retains cancellation, watchdogs, session policy, and MCP grant ownership. Paired-node execution and manual compaction continue through the existing host-managed process path.

    Reusable transports receive a run-bound liveSession capability. Before creating an initial or replacement process, await liveSession.restart() unless the current process is reusable. This joins the previous process exit and its host-owned resource cleanup; waiting for the child exit alone is insufficient. The host rejects restart when the caller is revoked or an exact live generation must be preserved. register() remains a synchronous admission check and refuses replacement while cleanup is unresolved.

    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.

    Rooted runs such as Skill Workshop reviews also require isolatesInstructionsWithExactTools: true on the backend registration. Declare this optional capability only when exact-tool execution suppresses ambient instruction files, skills, hooks, and plugins for both fresh and resumed runs. The host-prepared instruction snapshot must remain authoritative. OpenClaw rejects rooted runs when this declaration is absent, even if the backend can enforce exact tools. Existing non-rooted runs do not require this field.

    The bundled Claude CLI backend declares this capability. Rooted execution disables its native tools and serves the selected OpenClaw tools through the host-owned MCP grant, which retains the root, filesystem policy, and configured sandbox. The declaration does not grant filesystem or approval authority to the backend.

    A backend whose native tools are model-callable may declare projectNativeToolAuthority(nativeTools) so that automations created from its sessions keep the creator's native capabilities. For Claude stream-JSON, the input is the parent turn's system/init.tools list, intersected with toolAvailability.native when a host selection exists. Managed native settings can remove tools after CLI argument selection, so defaults are never inferred. Each turn starts with pending authority: MCP discovery remains available, but tool calls reject visibly until initialization supplies the list. Warm turns cannot borrow a previous turn's snapshot. Return only canonical names from the core vocabulary (read, write, edit, apply_patch, exec, process, web_search, web_fetch), each derived from a native tool the host enforces through this contract. Core validates the result before updating the active loopback grant and again at final creator-cap capture; any other name fails the turn. Updating the snapshot invalidates earlier cached tool projections. Project only equivalent capabilities: Claude's Glob locates paths and NotebookEdit edits notebook cells, so neither grants general read or edit. The native list contains tool names, not permission-rule patterns. Codex native code mode projects read and exec after OpenClaw explicitly requests the shell and rejects managed requirements or legacy managed settings that disable it. The effective setting and its source are checked at each preflight; a user-local shell disable is overridden for native mode, while a managed denial rejects before capture. It never infers write, edit, apply_patch, or process. The pinned Codex registry has no shell-disabled models; a custom model that disables its shell remains an unobservable exception because Codex does not expose that model capability.

    Previously saved empty automation caps remain restricted. Recreate the job or explicitly edit its tools from a fresh authorized creator turn; an old empty cap cannot safely be distinguished from an intentional denial.

    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.

    Lifecycle events are intentionally separate from this return union so existing plugins can continue to match it exhaustively. Use parseJsonlLifecycleEvent for backend-owned lifecycle records instead.

    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.

    parseJsonlLifecycleEvent: provider-native lifecycle records

    Set parseJsonlLifecycleEvent when a backend emits JSONL records for lifecycle state that is independent of assistant text, tools, sessions, and terminal results. The hook receives the same line and context as parseJsonlEvent and is tried first. Returning a lifecycle event consumes that line; returning null lets the source-compatible parseJsonlEvent hook or built-in parser handle it.

    The current lifecycle contract supports native compaction start and end records. An end record includes completed so channels can distinguish successful and incomplete compaction without inferring an outcome from later messages.

    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:

    typescript
    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:

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

    Supported bridge modes:

    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. 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:

    json5
    {  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:

    bash
    pnpm test extensions/acme-cli

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

    bash
    openclaw plugins inspect acme-cli --runtime --jsonopenclaw 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

    Was this useful?
    On this page

    On this page