docs/nodes/index.md
A node is a companion device (macOS/iOS/Android/headless) that connects to the Gateway WebSocket (same port as operators) with role: "node" and exposes a command surface (e.g. canvas.*, camera.*, device.*, notifications.*, system.*) via node.invoke. Protocol details: Gateway protocol.
Legacy transport: Bridge protocol (TCP JSONL; historical only for current nodes).
macOS can also run in node mode: the menubar app connects to the Gateway’s
WS server and exposes its local canvas/camera commands as a node (so
openclaw nodes … works against this Mac). In remote gateway mode, browser
automation is handled by the CLI node host (openclaw node run or the
installed node service), not by the native app node.
Notes:
WS nodes use device pairing. Nodes present a device identity during connect; the Gateway
creates a device pairing request for role: node. Approve via the devices CLI (or UI).
Quick CLI:
openclaw devices list
openclaw devices approve <requestId>
openclaw devices reject <requestId>
openclaw nodes status
openclaw nodes describe --node <idOrNameOrIp>
If a node retries with changed auth details (role/scopes/public key), the prior
pending request is superseded and a new requestId is created. Re-run
openclaw devices list before approving.
Notes:
nodes status marks a node as paired when its device pairing role includes node.node.pair.* (CLI: openclaw nodes pending/approve/reject/remove/rename) is a separate gateway-owned
node pairing store; it does not gate the WS connect handshake.openclaw nodes remove --node <id|name|ip> deletes stale entries from that
separate gateway-owned node pairing store.operator.pairingoperator.pairing + operator.writesystem.run / system.run.prepare / system.which: operator.pairing + operator.adminUse a node host when your Gateway runs on one machine and you want commands
to execute on another. The model still talks to the gateway; the gateway
forwards exec calls to the node host when host=node is selected.
system.run/system.which on the node machine.~/.openclaw/exec-approvals.json.Approval note:
On the node machine:
openclaw node run --host <gateway-host> --port 18789 --display-name "Build Node"
If the Gateway binds to loopback (gateway.bind=loopback, default in local mode),
remote node hosts cannot connect directly. Create an SSH tunnel and point the
node host at the local end of the tunnel.
Example (node host -> gateway host):
# Terminal A (keep running): forward local 18790 -> gateway 127.0.0.1:18789
ssh -N -L 18790:127.0.0.1:18789 user@gateway-host
# Terminal B: export the gateway token and connect through the tunnel
export OPENCLAW_GATEWAY_TOKEN="<gateway-token>"
openclaw node run --host 127.0.0.1 --port 18790 --display-name "Build Node"
Notes:
openclaw node run supports token or password auth.OPENCLAW_GATEWAY_TOKEN / OPENCLAW_GATEWAY_PASSWORD.gateway.auth.token / gateway.auth.password.gateway.remote.token / gateway.remote.password.gateway.remote.token / gateway.remote.password are eligible per remote precedence rules.gateway.auth.* SecretRefs are configured but unresolved, node-host auth fails closed.OPENCLAW_GATEWAY_* env vars.openclaw node install --host <gateway-host> --port 18789 --display-name "Build Node"
openclaw node start
openclaw node restart
On the gateway host:
openclaw devices list
openclaw devices approve <requestId>
openclaw nodes status
If the node retries with changed auth details, re-run openclaw devices list
and approve the current requestId.
Naming options:
--display-name on openclaw node run / openclaw node install (persists in ~/.openclaw/node.json on the node).openclaw nodes rename --node <id|name|ip> --name "Build Node" (gateway override).Exec approvals are per node host. Add allowlist entries from the gateway:
openclaw approvals allowlist add --node <id|name|ip> "/usr/bin/uname"
openclaw approvals allowlist add --node <id|name|ip> "/usr/bin/sw_vers"
Approvals live on the node host at ~/.openclaw/exec-approvals.json.
Configure defaults (gateway config):
openclaw config set tools.exec.host node
openclaw config set tools.exec.security allowlist
openclaw config set tools.exec.node "<id-or-name>"
Or per session:
/exec host=node security=allowlist node=<id-or-name>
Once set, any exec call with host=node runs on the node host (subject to the
node allowlist/approvals).
host=auto will not implicitly choose the node on its own, but an explicit per-call host=node request is allowed from auto. If you want node exec to be the default for the session, set tools.exec.host=node or /exec host=node ... explicitly.
Related:
Low-level (raw RPC):
openclaw nodes invoke --node <idOrNameOrIp> --command canvas.eval --params '{"javaScript":"location.href"}'
Higher-level helpers exist for the common “give the agent a MEDIA attachment” workflows.
Node commands must pass two gates before they can be invoked:
connect.commands list.Windows and macOS companion nodes allow safe declared commands such as
canvas.*, camera.list, location.get, and screen.snapshot by default.
Dangerous or privacy-heavy commands such as camera.snap, camera.clip, and
screen.record still require explicit opt-in with
gateway.nodes.allowCommands. gateway.nodes.denyCommands always wins over
defaults and extra allowlist entries.
Plugin-owned node commands can add a Gateway node-invoke policy. That policy
runs after the allowlist check and before forwarding to the node, so raw
node.invoke, CLI helpers, and dedicated agent tools share the same plugin
permission boundary. Dangerous plugin node commands still require explicit
gateway.nodes.allowCommands opt-in.
After a node changes its declared command list, reject the old device pairing and approve the new request so the gateway stores the updated command snapshot.
If the node is showing the Canvas (WebView), canvas.snapshot returns { format, base64 }.
CLI helper (writes to a temp file and prints MEDIA:<path>):
openclaw nodes canvas snapshot --node <idOrNameOrIp> --format png
openclaw nodes canvas snapshot --node <idOrNameOrIp> --format jpg --max-width 1200 --quality 0.9
openclaw nodes canvas present --node <idOrNameOrIp> --target https://example.com
openclaw nodes canvas hide --node <idOrNameOrIp>
openclaw nodes canvas navigate https://example.com --node <idOrNameOrIp>
openclaw nodes canvas eval --node <idOrNameOrIp> --js "document.title"
Notes:
canvas present accepts URLs or local file paths (--target), plus optional --x/--y/--width/--height for positioning.canvas eval accepts inline JS (--js) or a positional arg.openclaw nodes canvas a2ui push --node <idOrNameOrIp> --text "Hello"
openclaw nodes canvas a2ui push --node <idOrNameOrIp> --jsonl ./payload.jsonl
openclaw nodes canvas a2ui reset --node <idOrNameOrIp>
Notes:
Photos (jpg):
openclaw nodes camera list --node <idOrNameOrIp>
openclaw nodes camera snap --node <idOrNameOrIp> # default: both facings (2 MEDIA lines)
openclaw nodes camera snap --node <idOrNameOrIp> --facing front
Video clips (mp4):
openclaw nodes camera clip --node <idOrNameOrIp> --duration 10s
openclaw nodes camera clip --node <idOrNameOrIp> --duration 3000 --no-audio
Notes:
canvas.* and camera.* (background calls return NODE_BACKGROUND_UNAVAILABLE).<= 60s) to avoid oversized base64 payloads.CAMERA/RECORD_AUDIO permissions when possible; denied permissions fail with *_PERMISSION_REQUIRED.Supported nodes expose screen.record (mp4). Example:
openclaw nodes screen record --node <idOrNameOrIp> --duration 10s --fps 10
openclaw nodes screen record --node <idOrNameOrIp> --duration 10s --fps 10 --no-audio
Notes:
screen.record availability depends on node platform.<= 60s.--no-audio disables microphone capture on supported platforms.--screen <index> to select a display when multiple screens are available.Nodes expose location.get when Location is enabled in settings.
CLI helper:
openclaw nodes location get --node <idOrNameOrIp>
openclaw nodes location get --node <idOrNameOrIp> --accuracy precise --max-age 15000 --location-timeout 10000
Notes:
Android nodes can expose sms.send when the user grants SMS permission and the device supports telephony.
Low-level invoke:
openclaw nodes invoke --node <idOrNameOrIp> --command sms.send --params '{"to":"+15555550123","message":"Hello from OpenClaw"}'
Notes:
sms.send.Android nodes can advertise additional command families when the corresponding capabilities are enabled.
Available families:
device.status, device.info, device.permissions, device.healthnotifications.list, notifications.actionsphotos.latestcontacts.search, contacts.addcalendar.events, calendar.addcallLog.searchsms.searchmotion.activity, motion.pedometerExample invokes:
openclaw nodes invoke --node <idOrNameOrIp> --command device.status --params '{}'
openclaw nodes invoke --node <idOrNameOrIp> --command notifications.list --params '{}'
openclaw nodes invoke --node <idOrNameOrIp> --command photos.latest --params '{"limit":1}'
Notes:
The macOS node exposes system.run, system.notify, and system.execApprovals.get/set.
The headless node host exposes system.run, system.which, and system.execApprovals.get/set.
Examples:
openclaw nodes notify --node <idOrNameOrIp> --title "Ping" --body "Gateway ready"
openclaw nodes invoke --node <idOrNameOrIp> --command system.which --params '{"name":"git"}'
Notes:
system.run returns stdout/stderr/exit code in the payload.exec tool with host=node; nodes remains the direct-RPC surface for explicit node commands.nodes invoke does not expose system.run or system.run.prepare; those stay on the exec path only.systemRunPlan before approval. Once an
approval is granted, the gateway forwards that stored plan, not any later
caller-edited command/cwd/session fields.system.notify respects notification permission state on the macOS app.platform / deviceFamily metadata uses a conservative default allowlist that excludes system.run and system.which. If you intentionally need those commands for an unknown platform, add them explicitly via gateway.nodes.allowCommands.system.run supports --cwd, --env KEY=VAL, --command-timeout, and --needs-screen-recording.bash|sh|zsh ... -c/-lc), request-scoped --env values are reduced to an explicit allowlist (TERM, LANG, LC_*, COLORTERM, NO_COLOR, FORCE_COLOR).env, nice, nohup, stdbuf, timeout) persist inner executable paths instead of wrapper paths. If unwrapping is not safe, no allowlist entry is persisted automatically.cmd.exe /c require approval (allowlist entry alone does not auto-allow the wrapper form).system.notify supports --priority <passive|active|timeSensitive> and --delivery .PATH overrides and strip dangerous startup/shell keys (DYLD_*, LD_*, NODE_OPTIONS, PYTHON*, PERL*, RUBYOPT, SHELLOPTS, PS4). If you need extra PATH entries, configure the node host service environment (or install tools in standard locations) instead of passing PATH via --env.system.run is gated by exec approvals in the macOS app (Settings → Exec approvals).
Ask/allowlist/full behave the same as the headless node host; denied prompts return SYSTEM_RUN_DENIED.system.run is gated by exec approvals (~/.openclaw/exec-approvals.json).When multiple nodes are available, you can bind exec to a specific node.
This sets the default node for exec host=node (and can be overridden per agent).
Global default:
openclaw config set tools.exec.node "node-id-or-name"
Per-agent override:
openclaw config get agents.list
openclaw config set agents.list[0].tools.exec.node "node-id-or-name"
Unset to allow any node:
openclaw config unset tools.exec.node
openclaw config unset agents.list[0].tools.exec.node
Nodes may include a permissions map in node.list / node.describe, keyed by permission name (e.g. screenRecording, accessibility) with boolean values (true = granted).
OpenClaw can run a headless node host (no UI) that connects to the Gateway
WebSocket and exposes system.run / system.which. This is useful on Linux/Windows
or for running a minimal node alongside a server.
Start it:
openclaw node run --host <gateway-host> --port 18789
Notes:
~/.openclaw/node.json.~/.openclaw/exec-approvals.json
(see Exec approvals).system.run locally by default. Set
OPENCLAW_NODE_EXEC_HOST=app to route system.run through the companion app exec host; add
OPENCLAW_NODE_EXEC_FALLBACK=0 to require the app host and fail closed if it is unavailable.--tls / --tls-fingerprint when the Gateway WS uses TLS.openclaw nodes … works against this Mac).localhost.