OpenClaw Code Mode: Tool Discovery and Workflows
Learn how OpenClaw Code Mode lets agents discover, call, and combine large tool catalogs in compact JavaScript or TypeScript workflows. Essential for developers using the OpenClaw agent runtime.
Read this when
- You want to enable OpenClaw code mode for an agent run
- You need to explain why Code Mode is different from Codex Code Mode
- You are reviewing the compact tool contract, QuickJS-WASI sandbox, TypeScript transform, or hidden tool-catalog bridge
- You are reviewing the MCP namespace bridge or virtual API declarations
Code mode is an experimental feature in the OpenClaw agent runtime. By default it operates at the "auto" tier, which activates only for models that the catalog flags as preferred code-mode performers; all other models retain their standard tool access. Once engaged, the model no longer receives every enabled tool schema. Instead, it gets exec, wait, and any direct-only tool whose structured output cannot pass through the JSON-only guest bridge. The model then writes a small JavaScript or TypeScript program that searches, describes, and invokes the hidden tool catalog.
This page covers OpenClaw code mode, not Codex Code Mode. Although they share a name and identical control-tool names (exec, wait), they are distinct implementations:
- Codex Code Mode operates within the Codex coding harness. Its
exectool follows a freeform grammar: the model produces raw JavaScript source, optionally preceded by a// @exec: {...}pragma line for execution options, which runs in Codex's in-process V8 Code Mode runtime. - OpenClaw code mode operates in the generic OpenClaw agent runtime, controlled by
tools.codeMode.enabled(default"auto", activated per model). Itsexectool accepts a JSON{ code, language }payload, executed in a QuickJS-WASI worker.
Both are JavaScript execution surfaces rather than shell-command surfaces. Treat them as separate, differently-built features that coincidentally expose tools named exec/wait.
In OpenClaw code mode, command serves as a JavaScript or TypeScript alias for code, not a shell command. For shell or file operations, invoke the appropriate catalog tool from guest JavaScript using tools.callValue. Recognizable shell commands get rejected before the QuickJS worker launches, with actionable invalid_input guidance.
What it does
- The model-visible tool list narrows to
exec,wait, plus any direct-only tool likecomputeror the native-visionimageloader whose image result cannot cross the guest bridge. execruns model-generated JavaScript or TypeScript in an isolated QuickJS-WASI worker thread.- Every catalog-eligible enabled tool (OpenClaw core, plugin, MCP, client) is concealed as a standalone model tool and made available inside the guest program through
ALL_TOOLSandtools. - The
execdescription includes a bounded quick index of exact OpenClaw/plugin catalog ids, compact input hints, and compact declared output hints when a trusted tool supplies an output schema. It leaves out descriptions, full schemas, MCP entries, and overflow entries; guest-side catalog lookup remains the fallback. - Guest code searches the hidden catalog, describes a tool's schema, and calls a tool through the same execution path as normal agent turns (policy, approvals, hooks, telemetry all still apply).
- MCP tools are collected under the
MCPnamespace; in code mode this is the only supported way to call them. waitresumes a suspended code-mode run when nested tool calls are still pending.
Code mode alters only the model-facing orchestration surface. It does not replace tools, plugin tools, MCP tools, auth, approval policy, channel behavior, or model selection.
Why use it
- Reduced prompt footprint: providers receive two control tools, a bounded native-tool index, and only the few required direct tools instead of dozens or hundreds of full tool schemas.
- Enhanced orchestration: the model can employ loops, joins, small transforms, conditional logic, and parallel nested tool calls within a single code cell.
- Fewer model round trips: a declared output contract lets the model call and transform a tool result in one
exec; unknown outputs stay raw-first. - Provider neutral: functions for OpenClaw, plugin, MCP, and client tools without relying on provider-native code execution.
- Fails closed: if code mode is enabled but the QuickJS-WASI runtime is unavailable, the run fails rather than silently falling back to broad direct tool exposure.
Best suited for agents with a large enabled tool catalog, or workflows where the model must search, combine, and call several tools before answering.
Keep direct tool exposure for a small catalog or a model that does not reliably write short programs. Use Tool Search when you want a compact catalog but prefer structured search/describe/call controls instead of the QuickJS-WASI guest.
Quickstart
Defaults and overrides
Code mode ships enabled in the "auto" tier: it engages only when the run's model is flagged as a preferred code-mode performer in its provider catalog, and every other model retains normal tool exposure. No configuration is required. See Automatic per-model activation for the exact semantics and the shipped model list.
To opt out for every run:
{
tools: {
codeMode: false,
},
}
To force code mode on for every tool-capable run, regardless of model:
{
tools: {
codeMode: true,
},
}
Object form also works: tools.codeMode.enabled accepts the same false, true, and "auto" values. An object without enabled keeps the "auto" default.
If you use sandboxed agents with configured MCP servers, also allow the bundled MCP plugin in the sandbox tool policy, for example tools.sandbox.tools.alsoAllow: ["bundle-mcp"]. See Configuration - tools and custom providers.
Set explicit limits for tighter bounds:
{
tools: {
codeMode: {
enabled: true,
timeoutMs: 10000,
memoryLimitBytes: 67108864,
maxOutputBytes: 65536,
maxSnapshotBytes: 10485760,
maxPendingToolCalls: 16,
snapshotTtlSeconds: 900,
searchDefaultLimit: 8,
maxSearchLimit: 50,
},
},
}
What the model does
For a tool with a declared output such as Array<{ id: string; paid: boolean; tons: number }>, one guest program can select, call, and transform it:
const [shipmentTool] = await tools.search("list shipments");
const shipments = await tools.callValue(shipmentTool.id, {});
return shipments.filter((shipment) => !shipment.paid && shipment.tons > 10);
When a quick-index line ends in -> ?, the output shape is unknown. The first exec must return await tools.callValue(...) unchanged. A later exec can transform the observed value. This adds an extra model turn but prevents the model from guessing field names.
Verify the active surface
To confirm the model payload shape while debugging, run the Gateway with targeted logging:
OPENCLAW_DEBUG_CODE_MODE=1 \
OPENCLAW_DEBUG_MODEL_TRANSPORT=1 \
OPENCLAW_DEBUG_MODEL_PAYLOAD=tools \
openclaw gateway
With code mode active, the logged model-facing tool names should be exec and wait. For the full redacted provider payload, add OPENCLAW_DEBUG_MODEL_PAYLOAD=full-redacted for a short debugging session.
Use Swarm for agent fan-out
Swarm introduces three guest globals, agents.run(), phase(), and log(), to coordinate concurrent sub-agents within Code Mode scripts. With both tools.codeMode and tools.swarm enabled, standard JavaScript control flow handles fan-out, decision gates, and structured aggregation. Swarm functions as its own opt-in gate; turning on Code Mode by itself does not make the agents.* API available.
Technical tour
For maintainers, plugin authors debugging tool exposure, and operators checking high-risk deployments, the remainder of this page details the runtime contract and implementation specifics.
Runtime status
| Runtime | quickjs-wasi |
| Default state | "auto" (engages only catalog-preferred models) |
| Stability | experimental OpenClaw surface (Codex Code Mode is a separate, stable Codex harness surface) |
| Target surface | generic OpenClaw agent runs |
| Security posture | model code is hostile |
| User-facing promise | enabling code mode never silently falls back to broad direct tool exposure |
Scope
For a prepared run, code mode shapes the model-facing orchestration. Model selection, channel behavior, auth, tool policy, and tool implementations fall outside its control.
In scope: model-visible control/direct tool definitions, hidden tool catalog construction, JavaScript/TypeScript guest execution, the QuickJS-WASI worker runtime, host callbacks for search/describe/call, resumable state for suspended guest programs, output/timeout/memory/pending-call/snapshot limits, and telemetry/trajectory projection for nested tool calls.
Out of scope: provider-native remote code execution, shell execution semantics, changing existing tool authorization, persistent user-authored scripts, package manager/file/network/module access in guest code, and direct reuse of Codex Code Mode internals.
Provider-owned tools, such as remote Python sandboxes, count as separate tools. Refer to Code execution.
Terms
- Code mode: the OpenClaw runtime mode that hides catalog-compatible model tools and exposes
exec,wait, plus required direct-only tools. - Guest runtime: the QuickJS-WASI JavaScript VM that evaluates model code.
- Host bridge: the narrow JSON-compatible callback surface from guest code back into OpenClaw.
- Catalog: the run-scoped list of effective tools after normal tool policy, plugin, MCP, and client-tool resolution.
- Nested tool call: a tool call made from guest code through the host bridge.
- Snapshot: serialized QuickJS-WASI VM state saved so
waitcan continue a suspended code-mode run.
Configuration
The activation gate is tools.codeMode.enabled; configuring other fields alone will not turn on the feature.
| Field | Default | Clamp |
|---|---|---|
enabled | "auto" | false, true, or "auto" (per-model) |
runtime | "quickjs-wasi" | only supported value |
mode | "only" | exposes control/direct tools, catalogs the rest |
languages | ["javascript", "typescript"] | any subset of the two |
timeoutMs | 10000 | 100-60000 |
memoryLimitBytes | 67108864 | 1048576-1073741824 |
maxOutputBytes | 65536 | 1024-10485760 |
maxSnapshotBytes | 10485760 | 1024-268435456 |
maxPendingToolCalls | 16 | 1-128 |
snapshotTtlSeconds | 900 | 1-86400 |
searchDefaultLimit | 8 | clamped to maxSearchLimit |
maxSearchLimit | 50 | 1-50 |
When code mode is active but QuickJS-WASI fails to load, OpenClaw halts that run rather than falling back to standard tools. This applies to true and to "auto" runs where the model is deemed preferred: an active run will never quietly revert to broad direct tool access.
Automatic per-model activation
tools.codeMode.enabled takes three options:
"auto"(default): code mode activates only when the run's model is marked as a preferred code-mode performer in its provider catalog.false: code mode stays disabled for all runs.true: code mode activates for every tool-capable run, no matter the model.
false and true serve as absolute overrides and work just as they did before the "auto" tier was introduced.
The compat.codeMode catalog flag
Provider catalogs can tier a model by adding compat.codeMode to its model entry, alongside flags such as compat.supportsTools:
"preferred": the model consistently produces short orchestration programs and gains from the reduced code-mode interface;"auto"turns on code mode."capable"(or omitted): the model can operate in code mode when forced viaenabled: true, but"auto"maintains standard tool exposure.
Models lacking tool support have no access to code mode whatsoever, and no separate "unsupported" category exists. The flag itself is capability metadata owned by the provider plugin's catalog; core only reads the generic compat field.
Shipped preferred models
The bundled provider catalogs currently mark these models with "preferred":
| Provider | Models |
|---|---|
| anthropic | claude-fable-5, claude-opus-5, claude-sonnet-5, claude-mythos-5, claude-opus-4-8, claude-haiku-4-5 |
| deepseek | deepseek-v4-pro, deepseek-v4-flash |
gemini-3-flash-preview, gemini-3.1-pro-preview, gemini-3.1-flash-lite, gemini-3.5-flash, gemini-3.5-flash-lite, gemini-3.6-flash | |
| kimi | k3, k3-256k |
| minimax | MiniMax-M3 |
| moonshot | kimi-k3 |
| openai | gpt-5.6, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.5-pro |
| xiaomi | mimo-v2.5 |
| zai | glm-5.2, glm-5.1 |
All remaining models, including every local model served through Ollama, stay unflagged and retain standard tool exposure under "auto".
Models shipped by more than one provider
Some vendors can be reached through multiple provider ids: a subscription endpoint alongside an API endpoint, or a gateway that resells another vendor's model. Since "auto" derives the tier from whichever catalog handled the run, two catalogs describing the same upstream model must not conflict unintentionally.
Consequently, every catalog row for a shared model states its tier explicitly once any sibling row does so. Rows are matched using the vendor's own name for the weights, so a catalog that republishes a model under a namespaced id or altered casing is matched automatically: novita/moonshotai/kimi-k3, nvidia/z-ai/glm-5.2, and together/deepseek-ai/DeepSeek-V4-Pro all group with the first-party rows without any explicit declaration. Only names that are genuinely distinct require the manifest's upstreamModel marker, as the kimi catalog uses for moonshot/kimi-k3.
Reseller and aggregator catalogs such as baseten, deepinfra, github-copilot, gmi, novita, nvidia, ollama-cloud, opencode, opencode-go, qianfan, together, venice, and volcengine-plan currently declare "capable" for the models first-party catalogs flag "preferred": the preferred tier came from evaluations on the first-party endpoints, and those runs have not been repeated per reseller. Promoting one of those rows is a deliberate, evidence-backed change rather than an oversight.
For OpenAI models, the flag only matters when the run resolves to the OpenClaw embedded agent runtime. Default OpenAI routing uses the Codex-style harness surface, where OpenClaw code mode does not apply; the catalog flag never changes that routing decision.
Choosing when to enable
In A/B evaluations on the preferred models above, code mode reduced total token usage by roughly 30-50% at equal-or-better task pass rates, mostly by replacing many full tool schemas and per-tool round trips with one compact program surface. Models below the preferred tier showed no consistent win and sometimes regressed, which is why "auto" leaves them on direct tools.
The default "auto" fits agents that switch between models: strong models get the compact surface, weaker or local ones keep the exposure they handle best. Use true when you have verified a specific unflagged model performs well with code mode; global force-on is most predictable for single-model deployments. For open-weight or uncached serving where every prompt token is billed or recomputed, prefer enabling per model (via "auto" or a per-agent override) rather than globally, since the token savings depend on the model actually using the program surface well.
Activation
Code mode is evaluated after the effective tool policy is known and before the final model request is assembled:
- Resolve the agent, model, provider, sandbox, channel, sender, and run policy.
- Build the effective OpenClaw tool list, adding eligible plugin, MCP, and client tools.
- Apply allow/deny policy.
- If
tools.codeMode.enabledisfalse, or is"auto"and the run's model is not catalog-preferred, continue with normal tool exposure. - If enabled and tools are active for the run, retain required direct-only tools and register every catalog-eligible effective tool in the code-mode catalog.
- Remove the cataloged tools from the model-visible list; add
execandwaitalongside the retained direct-only tools.
Runs that intentionally have no tools (raw model calls, disableTools: true,
or an empty tools.allow list) do not activate the code-mode surface even
when tools.codeMode.enabled: true is configured. Code mode and OpenClaw Tool
Search are mutually exclusive for a run; if code mode activates, Tool Search's
compaction does not.
The code-mode catalog is run-scoped and must not leak tools from another agent, session, sender, or run.
Model-visible tools
When code mode is active, the model sees exec, wait, and any required
direct-only tool. Every other enabled tool is hidden from the model-facing
tool list and registered in the code-mode catalog.
Use exec for tool orchestration, data joining, loops, parallel nested calls,
and structured transforms. Use wait only when exec returns a resumable
waiting result.
exec
exec starts a code-mode cell and returns one result. Input code is model
generated and must be treated as hostile.
Input:
type CodeModeExecInput = {
code?: string;
command?: string;
language?: "javascript" | "typescript";
};
Rules:
- One of
codeorcommandmust be non-empty. codeis the documented model-facing field.commandis accepted as an exec-compatible alias for hook policies and trusted rewrites (the normal OpenClaw shell exec tool also uses acommandfield); when both are present, the values must match.languagedefaults to"javascript"; the schema exposes it as a flat string enum ("javascript" | "typescript"), not aoneOf/anyOfunion, since some providers reject those shapes.- If
languageis"typescript", OpenClaw transpiles before evaluation. execrejectsimport,require, dynamic import, and module-loader patterns.execnever exposes the normal shellexecimplementation recursively.- Outer code-mode
exechook events carrytoolKind: "code_mode_exec"andtoolInputKind: "javascript" | "typescript"(when known), so policies can distinguish code-mode cells from shell-styleexeccalls that share the same tool name.
Result:
type CodeModeResult = CodeModeCompletedResult | CodeModeWaitingResult | CodeModeFailedResult;
type CodeModeCompletedResult = {
status: "completed";
value: unknown;
output?: CodeModeOutput[];
telemetry: CodeModeTelemetry;
};
type CodeModeWaitingResult = {
status: "waiting";
runId: string;
reason: "pending_tools" | "yield";
pendingToolCalls?: CodeModePendingToolCall[];
output?: CodeModeOutput[];
telemetry: CodeModeTelemetry;
};
type CodeModeFailedResult = {
status: "failed";
error: string;
code?: CodeModeErrorCode;
output?: CodeModeOutput[];
telemetry: CodeModeTelemetry;
};
exec returns waiting when the guest suspends with resumable state that still
needs a model-visible continuation, an explicit yield_control(...), or a
bridge tool call that has not resolved within the exec deadline. The result
includes a runId for wait. Bridge tool calls, tools.search/describe/
call and namespace calls, including MCP namespace calls, are auto-drained
inside the same exec/wait call while they resolve within the deadline, so a
compact code block that awaits several tools runs to completion in one model
turn instead of forcing one model tool call per await. Restart-safe runs never
auto-drain; their pending work still goes through the replay-safe checks.
exec returns completed only when the guest VM has no pending work and the
final value is JSON-compatible after OpenClaw's output adapter runs.
wait
wait continues a suspended code-mode VM.
Input:
type CodeModeWaitInput = {
runId: string;
};
Output is the same CodeModeResult union returned by exec.
wait exists because nested OpenClaw tools can be slow, interactive, approval
gated, or stream partial updates; the model should not need to keep one long
exec call open while the host waits for external work.
QuickJS-WASI snapshot/restore is the resume mechanism:
exec runs code until it finishes, hits an error, or gets suspended.
When a suspension occurs, OpenClaw takes a snapshot of the QuickJS VM and logs any host work that is still pending.
After that pending work wraps up, wait brings the VM snapshot back and re-registers host callbacks using their stable names.
Nested tool results are fed into the restored VM by OpenClaw, and any pending QuickJS jobs are then flushed.
wait produces either completed, failed, or some other waiting result.
Snapshots count as runtime state, not user artifacts: they exist solely in an in-process map (nothing touches the database or disk), come with size caps, expire over time, and are limited to the run and session that created them.
wait returns a failed result when any of these conditions hold:
runIdis not recognized, or its snapshot has already expired.- The caller's run/session scope does not match the suspended run's scope.
- A
waitis already active for thatrunId. - The QuickJS-WASI restore operation fails.
- Resuming would push past
maxOutputBytesormaxSnapshotBytes.
Guest runtime API
declare const ALL_TOOLS: ToolCatalogEntry[];
declare const tools: ToolCatalog;
declare const MCP: Record<string, unknown>;
declare const namespaces: Record<string, unknown>;
declare function text(value: unknown): void;
declare function json(value: unknown): void;
declare function yield_control(reason?: string): Promise<void>;
ALL_TOOLS serves as compact metadata for the run-scoped catalog, and by default it omits full schemas. The model-visible exec description also carries a bounded, deterministic slice of exact OpenClaw/plugin ids, brief input hints, and trusted declared output hints. Descriptions stay deferred so that adversarial catalog prose cannot influence the model. When a tool is missing from that index, read ALL_TOOLS or invoke tools.search(...) from within the guest program.
In each quick-index line, the arrow points to the tools.callValue(...) value.
-> Array<{ id: string }> marks a declared output hint, while -> ? indicates the output is unknown.
Unknown outputs stay raw-first: hand back the value unchanged, inspect it, then filter or map it in a later exec instead of guessing field names. The same rule applies when a declared-output read feeds a final -> ? call: return that call's raw value without wrapping it in the requested answer shape.
type ToolCatalogEntry = {
id: string;
name: string;
label?: string;
description: string;
source: "openclaw" | "mcp" | "client";
sourceName?: string;
input: string;
output?: string;
};
input is a bounded TypeScript-style signature meant for the typical case. When the exact full schema is still required, turn to tools.describe(...). Remote MCP and client entries rely on input: "unknown" so their untrusted schemas remain deferred until describe. output appears only when a complete compact hint comes from a trusted OpenClaw core or plugin outputSchema. Output-schema claims from MCP and client sources are never promoted into this trusted catalog hint.
Plugin tools use source: "openclaw" with sourceName set to the owning plugin id, and no separate "plugin" source value exists. source: "mcp" applies only to MCP entries in sourceName/mcp metadata (and gets filtered out of ALL_TOOLS/tools.*, as noted below).
Full schema loads only on demand:
type ToolCatalogEntryWithSchema = ToolCatalogEntry & {
parameters: unknown;
outputSchema?: unknown;
};
Catalog helpers:
type ToolCatalog = {
search(query: string, options?: { limit?: number }): Promise<ToolCatalogEntry[]>;
describe(id: string): Promise<ToolCatalogEntryWithSchema>;
callValue(id: string, input?: unknown): Promise<unknown>;
call(id: string, input?: unknown): Promise<unknown>;
[safeToolName: string]: unknown;
};
Paired Gateway nodes show up through the nodes global:
const available = await nodes.list();
const node = await nodes.get(available[0].id);
const status = await node.invoke("device.status");
nodes.list() gives back paired node ids, names, platforms, connection state, and advertised commands. nodes.get(idOrName) matches an exact id before a display name and returns a handle carrying id, name, and invoke(command, params?). Invocation follows the standard nodes tool path, so pairing, command policy, scopes, approvals, timeouts, hooks, and telemetry all stay the same. A handle includes listDir(path) only when the node advertises fs.listDir. It leaves out exec: the generic nodes surface keeps system.run reserved for the normal shell exec tool with a node host.
Convenience tool functions get installed only for unambiguous safe names:
const files = await tools.search("read local file");
const fileRead = await tools.describe(files[0].id);
const content = await tools.callValue(fileRead.id, { path: "README.md" });
// If the hidden catalog has an unambiguous `web_search` entry:
const hits = await tools.web_search({ query: "OpenClaw code mode" });
tools.callValue(...) returns a normal tool's JSON details value directly.
tools.call(...) keeps the raw { tool, result } envelope intact for callers that need content blocks or other result metadata.
Declared output contracts
OpenClaw tools have the option to define outputSchema for the structured value that gets placed into AgentToolResult.details. This capability matters for Code Mode and Tool Search, though it is not a provider-native tool response schema, nor does it alter how tools are directly exposed.
When building a tool with defineToolPlugin, position the schema right next to parameters:
import { Type } from "typebox";
import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
const Shipment = Type.Object(
{
id: Type.String(),
paid: Type.Boolean(),
tons: Type.Number(),
},
{ additionalProperties: false },
);
export default defineToolPlugin({
id: "shipping",
name: "Shipping",
description: "Shipment tools.",
tools: (tool) => [
tool({
name: "shipping_list",
description: "List shipments.",
parameters: Type.Object({}),
outputSchema: Type.Array(Shipment),
execute: async () => loadShipments(),
}),
],
});
For api.registerTool(...) or a factory tool, apply the same outputSchema property to the AnyAgentTool object that gets returned.
Among the built-in contracts currently available are agents_list, apply_patch, conversations_list, conversations_send, conversations_turn, edit, openclaw, read, screen, sessions_history, sessions_list, sessions_search, sessions_send, session_status, spawn_task, terminal, web_fetch, and web_search. When a passthrough is exact, it can borrow the owning protocol's schema rather than duplicating a model-only contract. As an example, the conversation tools surface the same Gateway result schemas that conversations.list, conversations.send, and conversations.turn rely on; web_fetch maintains a tool-local schema whose hint exposes stable metadata, text, cache state, and nested spill metadata; web_search spells out its exact normalized results/answer/error/raw union as a complete quick-index hint. Filesystem contracts deliver structured read text, image, truncation, and optional-not-found outcomes; explicit edit change state plus diff/patch data; and apply-patch path summaries. When the quick index lists the fields, a single cell can handle both discovery and delivery without needing a separate inspection turn:
const listed = await tools.conversations_list({ query: "build bot" });
const target = listed.conversations.find((item) => item.label === "Build bot");
if (!target) throw new Error("conversation not found");
return await tools.conversations_send({
conversationRef: target.conversationRef,
message: "Build finished.",
});
Nested calls still go through the normal tool policy, hooks, and approvals. If a full contract is exact but exceeds the bounded quick index size, it stays reachable via tools.describe(...) and the arrow remains -> ?.
The contract rules are strict:
- Spell out the exact JSON-compatible
detailsvalue, not renderedcontentblocks or a provider envelope. - Cover every non-throwing success or error variant. Leave out
outputSchemawhen the tool has no stable structured result. - Close object layers with
{ additionalProperties: false }for a complete quick-index hint. Open, oversized, or otherwise partial schemas stay reachable throughtools.describe(...)but do not permit one-turn field use. - OpenClaw compiles the schema before running the tool, then validates the final
detailsafter normal tool hooks and before a catalog call returns. An invalid schema blocks tool execution; a mismatch fails without printing the value. - Compact hints are deterministic and bounded.
tools.describe(...)exposes the full trusted schema when the compact hint is not enough. - Installed plugin code counts as already trusted local code. Remote MCP and client metadata remains untrusted and cannot opt into these quick-index hints.
Plugin authoring details are covered in Tool plugins.
MCP catalog entries cannot be invoked through tools.callValue(...), tools.call(...), or convenience functions in code mode; they appear only via the generated MCP namespace. TypeScript-style declaration files are accessible through the read-only API virtual file surface, letting agents review MCP signatures without pushing MCP schemas into the prompt:
const files = await API.list("mcp");
const githubApi = await API.read("mcp/github.d.ts");
const issue = await MCP.github.createIssue({
owner: "openclaw",
repo: "openclaw",
title: "Investigate gateway logs",
});
const snapshot = await MCP.chromeDevtools.takeSnapshot({ output: "markdown" });
const resource = await MCP.docs.resources.read({ uri: "memo://one" });
const prompt = await MCP.docs.prompts.get({
name: "brief",
arguments: { topic: "release" },
});
API.read("mcp/<server>.d.ts") yields compact declarations derived from MCP tool metadata:
type McpToolResult = {
content?: unknown[];
structuredContent?: unknown;
isError?: boolean;
[key: string]: unknown;
};
declare namespace MCP.github {
/** Return this TypeScript-style API header. */
function $api(toolName?: string, options?: { schema?: boolean }): Promise<McpApiHeader>;
/**
* Create a GitHub issue.
* @param owner Repository owner
* @param repo Repository name
* @param title Issue title
*/
function createIssue(input: {
owner: string;
repo: string;
title: string;
body?: string;
}): Promise<McpToolResult>;
}
Declaration files are virtual, never written under the workspace or state directory. For each code-mode exec call, OpenClaw builds the run-scoped tool catalog, keeps the visible MCP entries, renders mcp/index.d.ts plus one mcp/<server>.d.ts per visible server, and injects that small read-only table into the QuickJS worker. Guest code sees only the API object: API.list(prefix?) returns file metadata and API.read(path) returns the selected declaration content. Unknown paths and ./.. segments are rejected.
This approach keeps large MCP schemas out of the model prompt: the agent learns the virtual API exists from the exec tool description, reads only the needed declaration file, then calls MCP.<server>.<tool>() with one object argument. MCP.<server>.$api() remains available as an inline fallback for a single-tool schema response inside the program.
The guest runtime is never given direct access to host objects. Instead, all input and output crosses the bridge as JSON-compatible values, each subject to explicit size limits.
Output API
text(value)adds human-readable output to theoutputarray.json(value)adds a structured output item once JSON-compatible serialization is complete.- Whatever value the guest code finally returns is placed into
valuewithin acompletedresult.
type CodeModeOutput = { type: "text"; text: string } | { type: "json"; value: unknown };
The rules are as follows: output appears in the same order as guest calls; the cap for output is maxOutputBytes; values that cannot be serialized get converted to plain strings or errors; binary values are not supported. Images and files move through standard OpenClaw tools, not via the code-mode bridge.
Tool catalog
Once effective policy filtering has been applied, the hidden catalog lists tools in this sequence: OpenClaw core tools, bundled plugin tools, external plugin tools, MCP tools, and finally client-provided tools for the current run.
Within a single run, catalog ids remain stable, and across equivalent tool sets they are deterministic whenever possible. The concrete shape is:
<source>:<owner>:<tool-name>
Here <source> can be openclaw, mcp, or client (plugin tools use openclaw with the plugin id serving as <owner>; core tools rely on openclaw:core:*). Examples:
openclaw:core:message
openclaw:browser:browser_request
mcp:github:create_issue
client:app:select_file
The catalog leaves out code-mode control tools (exec, wait, tool_search_code, tool_search, tool_describe, tool_call) as well as direct-only tools. Controls must not recurse through the catalog; direct-only tools stay visible to the model because their structured results cannot traverse the QuickJS bridge.
MCP entries remain in the run-scoped catalog so that policy, approvals, hooks, telemetry, transcript projection, and exact tool ids all stay aligned with normal tool execution. The guest-facing ALL_TOOLS, tools.search(...), tools.describe(...), tools.callValue(...), and tools.call(...) views do not include MCP entries. The generated MCP.<server>.<tool>({ ...input }) namespace maps back to the exact catalog id and routes through the same executor path.
Tool Search interaction
For runs where code mode is active, it replaces the OpenClaw Tool Search model surface.
When tools.codeMode.enabled is true and code mode is activated:
- OpenClaw stops exposing
tool_search_code,tool_search,tool_describe, ortool_callas model-visible tools. - The same cataloging concept is moved inside the guest runtime.
- The guest runtime gets compact
ALL_TOOLSmetadata plus search/describe/call helpers for non-MCP tools. - MCP calls go through the generated
MCPnamespace and its$api()headers rather thantools.call(...). - Nested calls use the same OpenClaw executor path that Tool Search relies on.
For the OpenClaw compact catalog bridge that code mode supersedes on active runs, see Tool Search.
Tool names and collisions
The model-visible exec tool is the code-mode tool. If the standard OpenClaw shell exec tool is enabled, it becomes hidden from the model and is cataloged just like any other tool.
Within the guest runtime:
tools.call("openclaw:core:exec", input)may invoke the shell exec tool when policy permits.tools.exec(...)gets installed only when the shell exec catalog entry has a clearly unambiguous safe name.- the code-mode
exectool is never made recursively available throughtools.
When two tools normalize to the same safe convenience name, OpenClaw drops the convenience function and requires tools.call(id, input) instead.
Nested tool execution
Every nested tool call crosses the host bridge and re-enters OpenClaw, carrying along: active agent id, session id and key, sender and channel context, sandbox policy, approval policy, plugin before_tool_call hooks, abort signal, streaming updates where they exist, and trajectory/audit events.
Nested calls appear in the transcript as genuine tool calls so support bundles reveal what happened, with the projection marking both the parent code-mode tool call and the nested tool id.
Nested calls may run in parallel, up to a limit of maxPendingToolCalls.
Run and snapshot lifecycle
Each code-mode run gets tracked in an in-process map keyed by runId (nothing is persisted to disk or a database). exec/wait report one of three result statuses: completed, waiting, or failed.
- A
waitingresult holds the QuickJS snapshot, any bridge requests still waiting, and scoping details such as agent run id and session id/key, untilwaiteither resumes it or the data expires. - Cases like expiry, mismatched session or run, and unknown or already-resuming
runIdvalues do not map to a unique terminal status. Instead, they appear as afailedresult (code: "invalid_input") carrying a message, for examplecode mode run is unavailable or expired.orcode mode run belongs to a different session.. - Once a run's snapshot reaches
completedorfailed, it is cleared from the map, and it is also discarded when the Gateway shuts down. Nothing persists across a restart, since this is only transient runtime state. - For read-only scenarios,
execmay enablerestartSafe: true. Under that setting, OpenClaw blocks side-effecting catalog and namespace tool calls before they execute and flags suspended results as replay-safe. Should a restart interruptwait, restart recovery rebuilds the turn from the transcript rather than pulling from the process-local snapshot. That recovery turn stays confined to audited read-only core tools and plugin tools that are explicitly replay-safe. - OpenClaw limits concurrent suspended runs per process to 64, and any new suspension beyond that limit is refused with
too many suspended code mode runs..
Per-run snapshot storage is capped by maxSnapshotBytes, the per-process suspended-run limit just mentioned, and snapshotTtlSeconds.
QuickJS-WASI runtime
OpenClaw brings in quickjs-wasi as a direct dependency within the owning package, rather than depending on a transitive copy that some unrelated dependency happens to install.
Runtime duties include: compiling and loading the QuickJS-WASI WebAssembly module, spinning up a separate VM per code-mode run or resume, registering host callbacks under stable names, applying memory and interrupt limits, executing JavaScript, draining queued jobs, snapshotting suspended VM state, restoring snapshots for wait, and releasing VM handles and snapshots once terminal states are reached.
Execution happens in a Node.js worker thread, away from OpenClaw's primary event loop. A guest infinite loop cannot hold the Gateway process hostage forever; the worker's interrupt handler enforces the wall-clock timeout regardless of whether guest code cooperates.
TypeScript
TypeScript support is purely a source transform: the accepted input is one TypeScript code string, and the output is a JavaScript string that QuickJS-WASI evaluates. There is no typechecking, no module resolution, and no import or require. Diagnostics come back as failed results.
The TypeScript compiler loads lazily, and only for TypeScript cells. Plain JavaScript cells and disabled code mode never trigger its loading.
Security boundary
Model code is not trusted. The runtime applies layered defenses:
- QuickJS-WASI runs in a worker thread, outside the main event loop
quickjs-wasiis loaded as a direct dependency, not via Codex or a transitive package- the guest gets no filesystem, network, subprocess, module import, environment variables, or host global objects
- QuickJS memory and interrupt limits are paired with a parent-process wall-clock timeout
- output, snapshot, log, and pending-call caps are enforced
- host bridge values pass through a narrow JSON adapter for serialization
- host errors become plain guest errors, never host realm objects
- snapshots are dropped on timeout, abort, session end, or expiry
- recursive access to
exec,wait, and Tool Search control tools is rejected - convenience-name collisions cannot shadow catalog helpers
The sandbox is one layer of protection; operators may still need OS-level hardening for high-risk deployments.
Error codes
type CodeModeErrorCode =
| "invalid_input"
| "runtime_unavailable"
| "timeout"
| "output_limit_exceeded"
| "snapshot_limit_exceeded"
| "internal_error";
invalid_input handles bad exec or wait arguments, disabled languages, rejected module access, TypeScript transform failures, unknown/expired/wrong-scope runId values, and an excess of suspended runs. runtime_unavailable deals with a QuickJS worker that fails to start or exits with a non-zero code.
Errors delivered to the guest are plain data; host Error instances, stack objects, prototypes, and host functions never cross into QuickJS.
Telemetry
Each result's telemetry field reports: hidden catalog size plus a source breakdown with openclaw/mcp/client counts, cumulative search/describe/call totals for the run's catalog, and the model-visible tool names, namely exec, wait, and retained direct-only tools.
The counterScope marks a single counter lifetime, which shifts when a catalog is replaced or restored but stays constant when tools are appended or prompt policy narrows that catalog.
The run metadata, found in meta.agentMeta within openclaw agent --json and mirrored on the agent exec --json envelope, adds per-run statistics:
codeModeEngaged: set totrueonly when code mode actually controlled the model tool surface. This is the dependable engagement indicator; do not infer engagement from config or tool names, since the shell tool also goes byexec, and the"auto"tier engages based on model capability. Harnesses that bridge OpenClaw's tool surface, like Copilot, report their resolved gate, socodeModeEngaged: falsewithtools.codeMode.enabled=truemakes a silent no-op observable. Harnesses running their own native tool surface, such as Codex, never engage OpenClaw code mode and therefore always readfalse; an attempt that reports nothing is normalized tofalsefor the same reason. Codex's owncodeModeOnlyis a separate native feature outside this field's tracking.assistantTurns: completed assistant/provider round trips over the run.bridgeCalls: the run's cumulative inner bridge counts ({ search, describe, call }). These calls never reach the provider; provider-visible outer tool calls stay inmeta.toolSummary.calls.costUsd: estimated USD cost derived from the run's accumulated usage and the model's cost config, including cache read/write tiers; omitted when the model has no cost data.
Telemetry must never include secrets, raw environment values, or tool inputs that have not been redacted, beyond what the existing OpenClaw trajectory policy already allows.
Debugging
When code mode behaves differently from a standard tool run, enable targeted model transport logging:
OPENCLAW_DEBUG_CODE_MODE=1 \
OPENCLAW_DEBUG_MODEL_TRANSPORT=1 \
OPENCLAW_DEBUG_MODEL_PAYLOAD=tools \
OPENCLAW_DEBUG_SSE=events \
openclaw gateway
For debugging payload shapes, turn on OPENCLAW_DEBUG_MODEL_PAYLOAD=full-redacted.
It captures a size-limited, redacted JSON snapshot of the model request; activate it only during debugging, because prompts and message text may still show up.
For stream debugging, turn on OPENCLAW_DEBUG_SSE=peek to capture the first five
redacted SSE events. Code mode also fails closed when, after the code-mode surface activates, the final provider payload does not contain exactly one exec, one wait, and only approved direct-only tools.
Implementation layout
- config contract:
tools.codeMode - catalog builder: effective tools to compact entries and id map
- model-surface adapter: replace visible tools with control/direct tools
- QuickJS-WASI runtime adapter: load, eval, snapshot, restore, dispose
- worker supervisor: timeout, abort, crash isolation
- bridge adapter: JSON-safe host callbacks and result delivery
- TypeScript transform adapter
- snapshot store: TTL, size caps, run/session scoping
- trajectory projection for nested tool calls
- telemetry counters and diagnostics
The implementation reuses catalog and executor concepts from Tool Search, but
does not use a node:vm child as the sandbox.
Validation checklist
Code mode coverage should prove:
- disabled config leaves existing tool exposure unchanged
- object config without
enabled: trueleaves code mode disabled - enabled config exposes
exec,wait, and only required direct-only tools to the model when tools are active for the run - raw no-tool runs,
disableTools, and empty allowlists do not trigger code-mode payload enforcement - all catalog-eligible effective non-MCP tools appear in
ALL_TOOLS - direct-only tools stay model-visible and do not appear in
ALL_TOOLS - denied tools do not appear in
ALL_TOOLS tools.search,tools.describe,tools.callValue, andtools.callwork for OpenClaw toolsAPI.list("mcp")andAPI.read("mcp/<server>.d.ts")expose TypeScript-style MCP declarations without a bridge/tool call- MCP namespace
$api()remains available as an inline fallback for schemas - MCP namespace calls work for visible MCP tools with one object input, while
direct MCP catalog entries are absent from
tools.* - Tool Search control tools are hidden from both the model surface and the hidden catalog
- nested calls preserve approval and hook behavior
- shell
execis hidden from the model but callable by catalog id when allowed - recursive code-mode
execandwaitare not callable from guest code - TypeScript input is transformed and evaluated without loading TypeScript on disabled or JavaScript-only paths
import,require, filesystem, network, and environment access fail- infinite loops time out and cannot block the Gateway
- memory cap failures terminate the guest VM
- output and snapshot caps are enforced for completed and suspended calls
waitresumes a suspended snapshot and returns the final value- expired, aborted, wrong-session, and unknown
runIdvalues fail - transcript replay and persistence preserve code-mode control calls
- transcript and telemetry show nested tool calls clearly
E2E test plan
Run these as integration or end-to-end tests when changing the runtime:
- Start a Gateway with
tools.codeMode.enabled: false. - Send an agent turn with a small direct tool set.
- Assert the model-visible tools are unchanged.
- Restart with
tools.codeMode.enabled: true. - Send an agent turn with OpenClaw, plugin, MCP, and client test tools.
- Assert the model-visible tool list is
exec,wait, plus only configured direct-only tools. - In
exec, readALL_TOOLSand assert the catalog-eligible effective test tools are present while direct-only tools are absent. - In
exec, call OpenClaw/plugin/client tools throughtools.search,tools.describe, andtools.callValue(or rawtools.call). - In
exec, callAPI.list("mcp")andAPI.read("mcp/<server>.d.ts")and assert the declaration files describe visible MCP tools. - In
exec, call MCP tools throughMCP.<server>.<tool>({ ...input })and assert direct MCP catalog entries are absent fromALL_TOOLSandtools.*. - Assert denied tools are absent and cannot be called by guessed id.
- Start a nested tool call that resolves after
execreturnswaiting. - Call
waitand assert the restored VM receives the tool result. - Assert the final answer contains output produced after restore.
- Assert timeout, abort, and snapshot expiry clean up runtime state.
- Export trajectory and assert nested calls are visible under the parent code-mode call.
Docs-only changes to this page should still run pnpm check:docs.
Related
- Swarm lets you fan out agent orchestration from Code Mode scripts
- Tool Search
- Agent runtimes
- Exec tool
- Code execution