On this page

On this page

Tools

Code Mode executors

Code Mode uses Node by default when enabled. Select QuickJS when you need a hardened guest runtime. Both executors run the same plain JavaScript cells, expose the same typed tool discovery, and use the same exec and wait tools. With no global Code Mode setting, automatic per-model activation applies. Choosing an executor in Labs preserves activation. When writing an object in config, include enabled: "auto" to retain automatic activation.

An AOT standalone selects the embedded-aot runtime execution profile. Its Node executor runs VM code interpreted and returns its normal results. A one-time performance notice recommends a JIT build for full speed. Builds that disable runtime code generation, including --disallow-code-generation-from-strings=strict, still report their runtime restriction with guidance. QuickJS remains a separate guest runtime with its own embedded WASM and worker qualification; OpenClaw does not select it automatically.

Choose an executor

Executor Intended use Execution Suspended state
node (default) Trusted execution on your Gateway host Node.js node:vm in a worker thread A live worker and JavaScript context
quickjs Hardened guest isolation QuickJS-WASI in a worker thread, supplied by the bundled executor plugin A serialized VM snapshot

Bridge settlement and failure provenance are host-owned under both executors, keeping diagnostics trustworthy regardless of executor isolation.

The intended guest API does not expose Node's filesystem, networking, process, environment, or module-loading APIs. Module guards and a small set of globals help keep cells focused on tool orchestration; they do not make node:vm safe for hostile JavaScript. Use QuickJS when guest isolation is part of your threat model, and use a separate OS user, container, or host when you need a stronger host boundary.

QuickJS provides a separate WASM guest with no ambient host access. It can still call powerful tools if your tool policy grants them. Calls made through the Code Mode tool bridge retain OpenClaw's normal policy, approvals, hooks, and session ownership under either executor. These checks mediate tool calls; they cannot contain hostile code that escapes a Node VM context.

Session sandboxing does not move Code Mode cells into the sandbox. Under the Node executor, a cell from a sandboxed session, including one created by a role that requires sandboxing, still runs in the Gateway process. Use QuickJS on Gateways where untrusted people can start sessions on the built-in runtime.

Set the executor

In the web interface, open Settings → Agents & Tools → Labs and use Code Mode executor to choose Node.js (default) or QuickJS (isolated). Activation and executor selection are separate: choosing an executor does not enable Code Mode.

To enable Code Mode with the default Node executor:

json5
{  tools: {    codeMode: {      enabled: true,      executor: "node",    },  },}

For hardened guest isolation, set executor: "quickjs" in the same object. The bundled plugin ID is code-mode-quickjs; selecting its executor needs no separate plugin enable step. Explicit executor selection activates this bundled runtime even when plugins.enabled is false or a restrictive plugins.allow list omits it. It does not enable other plugins. An explicit plugins.deny entry or plugins.entries.code-mode-quickjs.enabled: false still blocks it. An external plugin can supply the quickjs executor choice and remains subject to the full plugin activation policy. Enable exactly one owner for that choice. The selectable executor IDs are node and quickjs; node is owned by core.

The selected executor must be available. If it is missing, disabled, denied, or cannot load, the run fails with runtime_unavailable; OpenClaw never silently switches to Node or broad direct tool exposure. Check plugin policy when a configured QuickJS executor is unavailable.

Set agents.entries.<agent>.tools.codeMode.executor to override the global selection for one agent. An agent that omits it inherits the global choice. See Code Mode configuration for activation precedence and limits.

Understand waits and limits

Each cell keeps its selected executor for its entire lifetime, including every wait. Changing configuration affects new cells; it does not move suspended JavaScript between engines or replay completed tool calls.

Node retains its live worker context while suspended. QuickJS can release its worker after serializing the guest and restore the snapshot for wait. Both continuations are transient: completion, failure, cancellation, expiry, or Gateway shutdown releases them. Neither survives a Gateway restart.

timeoutMs, output limits, pending-call limits, suspended-run capacity, and snapshotTtlSeconds apply to both executors. The historical snapshotTtlSeconds name also controls Node's suspended-context lifetime. Suspended runs expire after elapsed time, unaffected by system-clock changes. The host enforces Node's execution deadline by terminating an overdue worker, including synchronous loops and promise continuations. Output emitted before the timeout remains available within the configured output limit. QuickJS additionally checks serialized VM state against maxSnapshotBytes. Node has no serialized VM snapshot, so that setting does not bound its live heap. Saved JSON results retain their shared memory/snapshot data allowance.

memoryLimitBytes limits QuickJS guest memory. For Node, it configures a best-effort V8 worker heap budget across the old and young generations, subject to engine minimums. That budget includes worker runtime allocations and excludes external buffers; it is not a per-context heap measurement or security boundary. Node stops an execution with runtime_unavailable when sustained major garbage collections leave the worker near its heap limit, then retires that worker, even when individual collections are cheap. The same guard retires pressured suspended or warm workers. Output emitted before heap exhaustion or GC retirement remains available within the configured output limit. Synchronous code that prevents GC notifications remains bounded by the host execution deadline. Paginate or narrow large tool results and avoid retaining large arrays; inspect completed actions before retrying. Captured Node output retains only a bounded prefix, including when a cell emits many entries without yielding. Fresh cells get fresh globals even when a worker is reused. Settled input and guest errors release their cell context and keep healthy workers available for reuse. Native failures, interrupted execution, and cancellation still retire the worker.

Node keeps at most eight completed workers warm. Their combined nominal heap budget is 512 MiB at the default 64 MiB per worker, or 8 GiB when every worker uses the maximum 1 GiB configuration. This is not an RSS limit. Surplus workers expire three minutes after their last completion. This cooldown keeps recent burst capacity warm; serial traffic reuses the newest worker without extending the surplus workers' deadlines. Within that eight-worker budget, the most recently used idle worker for each compatible host and heap limit remains warm until pressure, health, runtime rotation, or host shutdown retires it. Workers start only when a cell needs one; unused Code Mode creates no warm workers. Critical memory pressure retires all completed idle workers without discarding live continuations. GC storms above 600 major collections per minute across two consecutive windows of at least 10 seconds also retire warm workers. The pool admits at most one storm retirement per minute. Active cells and parked continuations drain through completion or their existing timeout or expiry before retirement; draining workers never return to the warm pool. The next cell acquires a healthy worker on demand. Diagnostic memory samples and stability logs expose the cumulative workerLifecycle counters. Prometheus exports openclaw_worker_started_total{script="code-mode-node.worker.js"} and openclaw_worker_retired_total with bounded retirement reasons: eviction for the warm-cache limit, idle-expiry for idle deadlines, dispose for explicit continuation or standalone cleanup, shutdown for host closure, crash for native worker errors, gc-thrash for the near-limit GC watchdog, and gc-storm for sustained storm containment. Memory pressure and runtime replacement retain memory_pressure and rotation. Retirement counts advance only after native exit; cleanup retries keep the first recorded reason. Start counts remain grouped by script, since a new worker has no retirement reason yet.

Node Code Mode leaves garbage collection to V8 instead of requesting inspector full collections between cells or while a cell is suspended. Its heap limit, execution deadline, GC-pressure detection, and worker retirement still apply. Neither executor's setting caps total Gateway RSS. Worker overhead and host-side tool results also consume memory. A retained Node context can use more total host memory than a size-limited QuickJS snapshot. The shared limit of 64 suspended cells and reserved resume slots still applies; there is no separate global memory quota. See Code Mode internals for state ownership and retention.

Upgrade an existing configuration

Doctor and eligible Gateway startup migrations replace an explicit runtime: "quickjs-wasi" with executor: "quickjs", preserving that explicit choice. An existing executor setting takes precedence when both fields are present. Activation and other limits remain unchanged.

The migrated explicit QuickJS choice also works when generic plugins are disabled or allowlisted, without enabling unrelated plugins. An explicit denial of code-mode-quickjs still prevents execution.

Configurations that did not explicitly select a runtime use the new Node default. Set executor: "quickjs" explicitly to retain hardened guest isolation. The retired runtime field is not a runtime alias; use openclaw doctor --fix for an older configuration.

Was this useful?