Building CLI Backend Plugins for Local AI Inference
Learn how to build a plugin that registers a local AI CLI as a text inference backend for OpenClaw. This guide is for developers integrating local command-line tools as fallback or primary backends.
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 allow OpenClaw to invoke a local AI CLI as a text inference backend. This backend shows up as a provider prefix inside model references:
acme-cli/acme-large
Choose a CLI backend when the upstream integration already runs as a local command, when the CLI manages local login state, or as a fallback option when API providers are unreachable.
Info
If the upstream service provides a standard HTTP model API, build a provider plugin instead. When the upstream runtime controls full agent sessions, tool events, compaction, or background task state, use an agent harness.
What the plugin owns
A CLI backend plugin consists of three contracts:
| Contract | File | Purpose |
|---|---|---|
| Package entry | package.json | Directs OpenClaw to the plugin runtime module |
| Manifest ownership | openclaw.plugin.json | Announces the backend id before runtime loads |
| Runtime registration | index.ts | Invokes api.registerCliBackend(...) with command defaults |
The manifest acts as discovery metadata: it neither runs the CLI nor registers runtime behavior. Runtime behavior begins only when the plugin entry calls 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"
}
}
Published packages must include compiled JavaScript runtime files. If your source entry is ./src/index.ts, add openclaw.runtimeExtensions pointing to the compiled JavaScript peer. Refer to 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
}
}
cliBackends holds the runtime ownership list; it enables OpenClaw to auto-load the plugin whenever model selection or agentRuntime.id references acme-cli.
setup.cliBackends provides the descriptor-first setup surface. Include it when model discovery, onboarding, or status should detect the backend without loading the plugin runtime. Use requiresRuntime: false only when those static descriptors are sufficient 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 match the manifest's cliBackends entry. The registered adapter is the authoritative plugin code; OpenClaw configuration selects the backend but does not alter its command contract.
Config shape
CliBackendConfig specifies how OpenClaw should start and interpret the CLI. The example above intentionally exercises the same command, resume, JSONL, model-alias, session, image, and watchdog fields as the bundled google-gemini-cli adapter.
| Field | Use |
|---|---|
command | Name of the binary or a full command path |
args | Default argv for initial launches |
resumeArgs | Alternative argv when resuming a session; {sessionId} is available here |
output / resumeOutput | Parsing engine: choose json, jsonl, or text |
jsonlDialect | JSONL event format: either claude-stream-json or gemini-stream-json |
liveSession | Mode for a persistent CLI process (claude-stdio) |
input | How prompts are delivered: arg or stdin |
maxPromptArgChars | Prompt character limit in arg mode before the system reverts to stdin |
env / clearEnv | Environment variables to add or remove before execution |
modelArg | Flag placed ahead of the model identifier |
modelAliases | Translation table from OpenClaw model IDs to the CLI's own IDs |
sessionArgs | Method for sending a session identifier through {sessionId} |
sessionMode | One of always, existing, or none |
sessionIdFields | JSON fields that OpenClaw extracts from the CLI's output |
systemPromptArg / systemPromptFileArg | Mechanism for passing the system prompt |
systemPromptFileConfigArg / systemPromptFileConfigKey | How to override the system prompt file path via config (e.g. -c) |
systemPromptMode | Either append or replace |
systemPromptWhen | Options: first, always, or never |
imageArg / imageMode | Flag for the image path and the method for multiple images (repeat or list) |
imagePathScope | Temporary location for staged images before transfer: temp or workspace |
serialize | Preserve execution order for runs using the same backend |
reseedFromRawTranscriptWhenUncompacted | Enable bounded raw transcript reseed before compaction to allow safe session resets |
reliability.watchdog | Separate no output timeouts for initial and resumed sessions |
Aim for the smallest static configuration that fits the CLI. Only introduce plugin callbacks for functionality that genuinely belongs to the backend.
Advanced backend hooks
Additional definitions available in CliBackendPlugin:
| Hook | Use |
|---|---|
normalizeConfig(config, context) | Apply runtime context to normalize the registered static adapter |
resolveExecutionArgs(ctx) | Attach request-level flags like thinking effort or side-question isolation |
prepareExecution(ctx) | Build temporary auth, config, or environment bridges ahead of startup |
transformSystemPrompt(ctx) | Perform a final CLI-tailored system prompt transformation |
textTransforms | Two-way replacements for prompt and output content |
defaultAuthProfileId | Favor a designated OpenClaw authentication profile |
authEpochMode | Determine how auth changes invalidate stored CLI sessions |
nativeToolMode | Indicate whether native tools are missing, always active, or host-selectable |
toolAvailabilityEnforcement | Specify whether exact tool caps are enforced at argv or execution staging |
sideQuestionToolMode | List disabled native tools for /btw side questions |
bundleMcp / bundleMcpMode | Choose OpenClaw's loopback MCP tool bridge |
ownsNativeCompaction | Backend manages its own compaction while OpenClaw steps aside |
subscriptionAuthDispatch | Embedded runs activated on subscription credentials execute via this backend |
runtimeArtifact | Attach a script launcher to its full bundled package tree |
These hooks should remain under provider ownership. Avoid adding CLI-specific logic to core when a backend hook can capture the behavior.
prepareExecution(ctx) receives ctx.contextTokenBudget, the effective token
limit chosen for the run. Backends that handle their own compaction can map that
budget into their CLI-specific launch contract.
runtimeArtifact belongs to the plugin. It is consulted
only when a live inference turn creates or refreshes verified setup authority;
normal CLI runs do not need it. A backend without this declaration cannot
create verified CLI setup authority. A bundled-package-tree declaration specifies
the exact package.json owner and requires the package entrypoint to be the
command. OpenClaw hashes the 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 full inference implementation; optional tool integrations
do not make an external implementation graph safe.
If the same backend also ships a self-contained native executable, include its
canonical basenames in nativeExecutableNames. Other native commands stay
unverified.
ctx.executionMode is "agent" for standard 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 uses 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"requiresresolveExecutionArgs. 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"requiresprepareExecution. The hook must stage an exact per-run policy and returntoolAvailabilityEnforced: 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.
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 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
agentHarnessIdsessions route to the harness endpoint instead.
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:
| 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
A standalone backend is chosen by users through its model-ref prefix. When a backend declares a canonical modelProvider, it can alternatively be selected via that provider model's agentRuntime.id. The adapter logic stays inside the plugin:
{
agents: {
defaults: {
model: {
primary: "openai/gpt-5.6-sol",
fallbacks: ["acme-cli/large"],
},
},
},
}
Store credentials in OpenClaw auth profiles or in configuration owned by the plugin. Make sure the registered command appears on the gateway service's PATH; deployments that require a different path or argv should modify or wrap the plugin registration.
Verification
For bundled plugins, write a focused test around the builder and setup registration, then execute the plugin's targeted test lane:
pnpm test extensions/acme-cli
For local or installed plugins, verify discovery along with 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, include a live smoke test that exercises those paths with the actual CLI. Do not rely on static inspection for prompt, image, MCP, or session-resume behavior.
Checklist
Check
package.jsoncontainsopenclaw.extensionsand built runtime entries for published packages
Check
openclaw.plugin.jsondefinescliBackendsand deliberateactivation.onStartup
Check
setup.cliBackendsis present when setup or 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.idpicks 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 confirm the backend path
Related
- CLI backends - runtime selection and behavior
- Building plugins - package and manifest basics
- Plugin SDK overview - registration API reference
- Plugin manifest -
cliBackendsand setup descriptors - Agent harness - full external agent runtimes