If the upstream service exposes a normal HTTP model API, write a
provider plugin instead. If the upstream
runtime owns complete agent sessions, tool events, compaction, or background
task state, use an agent harness.
What the plugin owns
A CLI backend plugin has three contracts:
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
1
Create package metadata
package.json
./src/index.ts, add openclaw.runtimeExtensions pointing at the
built JavaScript peer. See Entry points.2
Declare backend ownership
openclaw.plugin.json
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.3
Register the backend
index.ts
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:
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. Currently, onlysession_expireddoes so.
"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:
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.
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, separate from the ordinary prompt. 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.
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"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.
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.
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
agentHarnessIdsessions route to the harness endpoint instead.
/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:
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 canonicalmodelProvider can instead be selected through that
provider model’s agentRuntime.id. Adapter mechanics remain in the plugin:
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:Checklist
package.json has openclaw.extensions and built runtime entries for published packagesopenclaw.plugin.json declares cliBackends and intentional activation.onStartupsetup.cliBackends is present when setup/model discovery should see the backend coldapi.registerCliBackend(...) uses the same backend id as the manifestThe backend model prefix or model-scoped
agentRuntime.id selects the registrationSession, 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
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