Nodes: Pairing, Capabilities, Permissions, and CLI Helpers

Learn how nodes pair with the Gateway, their capabilities across canvas, camera, device, notifications, and system, and how to manage permissions and CLI helpers. Essential for developers integrating companion devices.

Read this when

  • Pairing iOS/watchOS/Android nodes to a gateway
  • Using node canvas/camera for agent context
  • Adding new node commands or CLI helpers

A node is a companion device (macOS, iOS, watchOS, Android, or headless) that links to the Gateway using role: "node" and provides a command surface (for example, canvas.*, camera.*, device.*, notifications.*, system.*) through node.invoke. The majority of nodes communicate over the Gateway WebSocket on the operator port. The optional direct Apple Watch node uses signed HTTPS polling on that same port, since watchOS blocks generic low-level networking for standard apps. Protocol specifics: Gateway protocol.

Legacy transport: Bridge protocol (TCP JSONL; historical only for current nodes).

macOS can also function in node mode: the menu bar app connects to the Gateway's WS server as one node, so openclaw nodes … targets this Mac. The app adds native Canvas, camera, screen, notification, and computer-control commands to the same node-host command surface used by openclaw node run. Do not launch a second CLI node on that Mac; the app runs the matching CLI node-host runtime as an internal worker and stays as the single Gateway connection and node identity.

Nodes are peripherals, not gateways: they do not run the gateway service, and channel messages (Telegram, WhatsApp, etc.) arrive on the gateway, not on nodes.

Troubleshooting runbook: /nodes/troubleshooting

Pairing + status

Nodes rely on device pairing. A node presents a signed device identity when connecting; the Gateway creates a device pairing request for role: node. Approve through the devices CLI (or UI). The direct Apple Watch setup uses an admin-minted, short-lived node-only setup code to approve its fixed low-risk command surface; later capability expansion still requires standard approval.

openclaw devices list
openclaw devices approve <requestId>
openclaw devices reject <requestId>
openclaw nodes status
openclaw nodes describe --node <idOrNameOrIp>

Pending pairing requests expire 5 minutes after the device's last retry. A device that repeatedly reconnects keeps its one pending request (and requestId) alive instead of generating a new prompt every few minutes. See Node pairing for the full request/approve lifecycle. If a node retries with changed auth details (role, scopes, public key), the prior pending request is superseded and a new requestId is created. Clients receive a device.pair.resolved event for the superseded request, and you should re-run openclaw devices list before approving.

  • nodes status marks a node as paired when its device pairing role includes node.
  • A connected native Mac can opt in to coalesced physical-input activity from Settings -> Permissions -> Active computer detection. Accessibility is also required. The Gateway marks the freshest eligible Mac as active, gives the agent a stable node-id hint, and routes node connection alerts there before a delayed fallback. See Active computer presence for setup, privacy, timing, and troubleshooting.
  • The device pairing record serves as the durable approved-role contract. Token rotation stays inside that contract; it cannot elevate a paired node into a role that pairing approval never granted.
  • node.pair.* (CLI: openclaw nodes pending/approve/reject/remove/rename) is a separate, gateway-owned node pairing store that tracks the node's approved command and capability surface across reconnects. It does not gate transport authentication; device pairing handles that.
  • openclaw nodes remove --node <id|name|ip> removes a node pairing. For a device-backed node, it revokes the device's node role in the paired-device store and disconnects that device's node-role sessions. A mixed-role device keeps its row and only loses the node role, while a node-only device row is deleted. It also clears any matching entry from the separate node pairing store. operator.pairing may remove non-operator node rows on other devices; a device-token caller revoking its own node role on a mixed-role device additionally needs operator.admin.
  • Approval scope follows the pending request's declared commands:
    • commandless request: operator.pairing
    • non-exec node commands: operator.pairing + operator.write
    • system.run / system.run.prepare / system.which: operator.pairing + operator.admin

Version skew and upgrade order

The Gateway WebSocket accepts authenticated node clients across an N-1 protocol window. The current v4 Gateway therefore accepts v3 nodes when the connection declares both role: "node" and client.mode: "node". Operator and UI sessions must still use the current protocol.

For staged fleet upgrades, upgrade the Gateway first, then upgrade each node. An N-1 node remains visible and manageable while it is upgraded; the Gateway logs legacy node protocol accepted with an upgrade recommendation. Pairing, device authentication, command allowlists, and exec approvals still apply. Plugin-owned capabilities and commands stay hidden until the node upgrades to the current protocol. Nodes older than N-1 require an out-of-band upgrade before reconnecting.

The direct watchOS HTTPS transport requires the current protocol version; update the watch app with the Gateway before enabling direct mode.

Remote node host (system.run)

Use 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.

RoleResponsibility
Gateway hostReceives messages, runs the model, routes tool calls.
Node hostExecutes system.run/system.which on the node machine.
ApprovalsEnforced on the node host via ~/.openclaw/exec-approvals.json.

Approval note:

  • Approval-backed node runs bind exact request context. The exec path prepares a canonical systemRunPlan before approval; once granted, the gateway forwards that stored plan, not any later caller-edited command/cwd/session fields, and re-validates the working directory before running.
  • For direct shell/runtime file executions, OpenClaw also best-effort binds one concrete local file operand and denies the run if that file changes before execution.
  • If OpenClaw cannot identify exactly one concrete local file for an interpreter/runtime command, approval-backed execution is denied instead of pretending full runtime coverage. Use sandboxing, separate hosts, or an explicit trusted allowlist/full workflow for broader interpreter semantics.

Start a node host (foreground)

On the node machine:

openclaw node run --host <gateway-host> --port 18789 --display-name "Build Node"

node run also accepts --context-path (Gateway WS context path), --tls, --tls-fingerprint <sha256>, and --node-id (override the legacy client instance ID; this does not reset pairing). On macOS, pass --share-installed-apps to advertise device.apps; sharing is off by default. Use --no-share-installed-apps to disable a previously saved opt-in.

Remote gateway via SSH tunnel (loopback bind)

When the Gateway binds to loopback (gateway.bind=loopback, the default for local mode), direct connections from remote node hosts are not possible. Set up an SSH tunnel and direct the node host to the tunnel's local endpoint.

Example (node host to 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:

  • Authentication via token or password is supported by openclaw node run.
  • Environment variables are the recommended approach: OPENCLAW_GATEWAY_TOKEN and OPENCLAW_GATEWAY_PASSWORD.
  • Configuration falls back to gateway.auth.token and gateway.auth.password.
  • In local mode, gateway.remote.token and gateway.remote.password are deliberately ignored by the node host.
  • For remote mode, gateway.remote.token and gateway.remote.password are considered according to remote precedence rules.
  • When active local gateway.auth.* SecretRefs are set but cannot be resolved, node host authentication fails in a closed state.
  • Only OPENCLAW_GATEWAY_* environment variables are considered during node host authentication resolution.

Start a node host (service)

openclaw node install --host <gateway-host> --port 18789 --display-name "Build Node"
openclaw node start
openclaw node restart

node install additionally accepts --context-path, --tls, --tls-fingerprint, --node-id (limited to legacy client instance ID), --share-installed-apps and --no-share-installed-apps, --runtime <node> (node is the default), and --force for reinstallation. node status, node stop, and node uninstall are also provided.

Pair + name

On the gateway machine:

openclaw devices list
openclaw devices approve <requestId>
openclaw nodes status

If the node retries after changing its authentication details, execute openclaw devices list again and approve the current requestId.

Naming choices:

  • --display-name on openclaw node run or openclaw node install (stored in the shared node_host_config SQLite row alongside the client instance ID and Gateway connection metadata).
  • openclaw nodes rename --node <id|name|ip> --name "Build Node" (overrides the gateway).

Node-hosted MCP servers

Place MCP server configuration in openclaw.json on the node machine, not on the Gateway:

{
  nodeHost: {
    mcp: {
      servers: {
        localDocs: {
          command: "npx",
          args: ["-y", "@modelcontextprotocol/server-filesystem", "/srv/docs"],
          toolFilter: {
            include: ["read_*", "search"],
          },
        },
        internalApi: {
          url: "https://mcp.internal.example/mcp",
          transport: "streamable-http",
          headers: {
            Authorization: "Bearer ${INTERNAL_MCP_TOKEN}",
          },
        },
      },
    },
  },
}

The headless node host starts these servers, enumerates their tools, and broadcasts the descriptors once connected. Tool calls are routed back to that node through mcp.tools.call.v1; the Gateway does not require matching MCP configuration or a JS plugin. OAuth MCP servers are not compatible with this node hosted v1 path.

Current node hosts declare the built-in mcp.tools.call.v1 command family during their initial pairing, even without any MCP server configured. A node paired under an older OpenClaw version might request a one time command surface upgrade after the node host is updated. Adding, removing, or filtering servers afterward does not necessitate re pairing, as the approved command family stays the same. Restart openclaw node run or openclaw node restart to apply changes to node MCP configuration; the node host does not monitor this configuration file.

Gateway operators can suppress all agent visible tools published by paired nodes, including node hosted MCP tools, using gateway.nodes.pluginTools.enabled: false. Specific command denials like gateway.nodes.commands.deny: ["mcp.tools.call.v1"] also prevent execution.

Node-hosted skills

Install skills in the node machine's active OpenClaw skills directory, which defaults to ~/.openclaw/skills. OPENCLAW_HOME, OPENCLAW_STATE_DIR, and OPENCLAW_CONFIG_PATH shift that active profile. OPENCLAW_STATE_DIR takes priority for skills; otherwise, skills/ sits next to the path output by openclaw config file. The headless node host publishes valid SKILL.md files after connecting, and the Gateway adds them to agent skill snapshots only while that node remains connected. Each skill directory name must match the name frontmatter field so the abstract node locator maps to a single entry without introducing another protocol field.

The initial node role pairing authorizes skill publication. Adding, removing, or modifying skills does not require additional pairing or Gateway configuration changes. Restart openclaw node run or openclaw node restart after modifying node skill files; the node host does not watch the skills directory.

Node skill entries that run on a host specify both the node itself and where execution takes place. Skill files, relative paths, and binaries all reside on that particular node. The agent reads the published node://.../SKILL.md location using the standard read tool. file_fetch only accepts operator-approved absolute node paths, not node skill locators; runtimes lacking the normal read tool can instead execute cat SKILL.md through exec host=node node=<node-id> with the advertised node://.../skills/<name> directory as workdir. Referenced files and binaries share the same execution target and working directory. The node host resolves that locator against its current OpenClaw state directory, so relative paths are resolved on the node rather than on the Gateway machine. The publishing node must have system.run approved, and the agent's exec policy must permit host=node; otherwise the skill is excluded from that agent's snapshot.

To stop publication, set nodeHost.skills.enabled: false on the node. Gateway operators can ignore skills from all paired nodes using gateway.nodes.allowSkills: false.

Headless identity state

The headless node maintains three separate state records in a shared SQLite database:

  • ~/.openclaw/state/openclaw.sqlite (node_host_config): the client instance ID, display name, and Gateway connection metadata.
  • ~/.openclaw/state/openclaw.sqlite (device_identities, key primary): the signed device keypair and derived cryptographic device ID.
  • ~/.openclaw/state/openclaw.sqlite (device_auth_tokens): paired device auth tokens keyed by cryptographic device ID and role.

For a signed node, the Gateway uses the cryptographic device ID for pairing and node routing. The client instance ID serves only as connection metadata. Changing --node-id or migrating a retired node.json therefore does not reset pairing. Refer to Identity and pairing state for the supported revoke-and-re-pair workflow and upgrade notes.

Retired identity/device.json and identity/device-auth.json files are Doctor-owned migration inputs. Stop the node host and run openclaw doctor --fix; Doctor imports and verifies their rows in SQLite before removing the old files.

Allowlist the commands

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 are stored on the node host at ~/.openclaw/exec-approvals.json.

Point exec at the node

Configure defaults in the gateway config:

openclaw config set tools.exec.host node
openclaw config set tools.exec.mode allowlist
openclaw config set tools.exec.node "<id-or-name>"

Or per session:

/exec host=node security=allowlist node=<id-or-name>

Once configured, any exec call with host=node runs on the node host (subject to the node allowlist or approvals).

host=auto will not automatically select the node on its own, but an explicit per-call host=node request is permitted from auto. To make node exec the session default, explicitly set tools.exec.host=node or /exec host=node ....

Related:

Local model inference

A desktop or server node can expose chat-capable models from an Ollama server running locally on that node. Agents use the Ollama plugin's node_inference tool to find installed models and run a bounded prompt remotely; the Gateway does not require direct network access to Ollama. See Ollama node-local inference for setup, model filtering, and direct verification commands.

Codex sessions and transcripts

The official codex plugin can expose non-archived Codex sessions on a headless node host or native macOS node. Catalog registration no longer depends on supervision.enabled; that option controls the agent-facing supervision tools. Set sessionCatalog.enabled: false in the Codex plugin config to disable the operator catalog and paired-node catalog commands without disabling the provider or harness. The plugin must still be active on both computers, and the node setting remains local consent: enabling only the Gateway cannot read another computer's Codex state.

The node advertises the versioned read-only codex.appServer.threads.list.v1 and codex.appServer.thread.turns.list.v1 commands. A native node host with the Codex CLI available also advertises codex.terminal.resume.v1. Approve the node pairing upgrade when those commands first appear. The Gateway invokes them through the normal plugin node policy and isolates failures by host.

Paired-node rows appear as a Codex group in the normal sessions sidebar. Within each host, rows group by project folder by default; a working directory under .claude/worktrees/<name> folds into its origin repository, and project groups collapse like other sidebar sections. Use the folder icon in the catalog header to flatten or restore the project groups. The same grouping applies to the Claude sessions catalog. By default, selecting a row opens the normal Chat pane and reads its persisted transcript through bounded, cursor-paginated thread/turns/list calls with full item projection. Use the row menu, the viewer header, or the Open Codex/Claude sessions in preference to start codex resume <thread-id> in the operator terminal on the computer that owns the session. The paired-node terminal path is an allowlisted PTY relay owned by the Codex plugin, not arbitrary node command execution.

The relay does not provide the full OpenClaw harness continuation and archive ownership contracts. Continue and Archive are therefore unavailable for remote rows. On the Gateway computer, stored and idle rows can start a distinct model-locked Chat branch. Either can be archived only after the operator confirms that no other Codex client is using it; a stored row's live activity remains unknown. Active rows cannot branch or archive.

See Supervise Codex sessions for setup, pagination, local continuation, and the metadata security boundary.

Claude sessions and transcripts

The non-archived Claude CLI and Claude Desktop sessions on the Gateway and paired nodes are discovered by the bundled anthropic plugin by default. To turn off the operator catalog and paired-node catalog commands without disabling Anthropic models or the Claude CLI backend, set plugins.entries.anthropic.config.sessionCatalog.enabled: false.

When the Anthropic plugin is active and ~/.claude/projects/ exists, a remote macOS app node broadcasts anthropic.claude.sessions.list.v1 and anthropic.claude.sessions.read.v1. When those commands appear for the first time, approve the node pairing upgrade.

If a native node host has the Claude CLI available, it also broadcasts anthropic.claude.terminal.resume.v1. Eligible CLI and Desktop rows can launch claude --resume <session-id> in the operator terminal on the host that owns them. This action takes over the native session; unlike OpenClaw adoption, it does not fork the Claude session beforehand.

The catalog merges valid Claude CLI project-index records with a bounded metadata fallback for unindexed JSONL transcripts. That fallback identifies concurrent non-sidechain interactive (cli) and headless Agent SDK CLI (sdk-cli) sessions. Desktop titles and archive state come from Claude Desktop's local metadata. When both sources point to the same Claude Code session ID, Desktop metadata takes precedence; CLI-only transcripts stay visible because the CLI lacks an archive flag. Transcript reads rely on opaque byte-offset cursors and bounded backward file reads, so selecting a large session or loading an older page does not load the entire JSONL history into a single Gateway response.

The list and read commands are read-only. They expose catalog metadata and transcript content solely through the generic sessions.catalog.list and sessions.catalog.read methods to an authenticated operator connection with operator.write. A Gateway-local Claude CLI row can be adopted from the normal Chat composer: OpenClaw imports bounded visible history, resumes with --fork-session on the first turn, and leaves the source transcript unchanged.

A headless node host can opt into the same continuation flow:

{
  nodeHost: {
    agentRuns: {
      claude: { enabled: true },
    },
  },
}

The node broadcasts agent.cli.claude.run.v1 only when this node-local setting is enabled and the claude executable resolves on that node. The Gateway cannot enable it remotely. The command also passes through the node's existing exec approval policy. When all three Claude commands are broadcast and allowed by the Gateway's node command policy, a Claude CLI row on that node becomes continuable: OpenClaw imports bounded history, ties the adopted session to the node and its catalog-reported working directory, and runs each one-shot claude -p turn there. The first turn still uses --fork-session, preserving the source transcript.

Node-placed turns use the node's Claude defaults. In v1 they do not receive the Gateway loopback MCP config or Gateway skills plugin, cannot reseed from a Gateway transcript, and reject attachments and images. Claude Desktop rows and nodes that do not broadcast the run command remain view-only. The macOS app node does not yet broadcast this command, so its rows remain view-only.

For the Control UI behavior and storage sources, see Anthropic: Claude sessions across computers.

OpenCode and Pi sessions

The bundled OpenCode and ACPX plugins also discover read-only native session catalogs on the Gateway and paired nodes. A node broadcasts opencode.sessions.list.v1 / opencode.sessions.read.v1 when the opencode CLI is installed, and acpx.pi.sessions.list.v1 / acpx.pi.sessions.read.v1 when Pi's session directory exists. Approve the node pairing upgrade when new commands first appear. When the matching CLI is also available, the node adds opencode.terminal.resume.v1 or acpx.pi.terminal.resume.v1; the existing row menu and viewer header can then reopen the selected session in its owning terminal with opencode --session <id> or pi --session <id>.

OpenCode reads through its official CLI JSON/export surface. Pi reads its documented JSONL session store, including project and global settings.json session directories plus PI_CODING_AGENT_DIR and PI_CODING_AGENT_SESSION_DIR overrides. Both catalogs are enabled by default; turn them off in the Web UI under Config > Plugins.

Terminal resume uses the stored session working directory and the same allowlisted duplex PTY relay as Codex and Claude. It does not expose arbitrary node command execution.

Terminal file uploads

The Control UI can drag files into an open paired-node terminal. The native node host broadcasts the admin-only terminal.upload command; approve the pairing upgrade when it first appears. Each file is limited to 16 MiB, staged in a private temporary directory on that node, and returned to the terminal as a shell-quoted path without executing it.

Path insertion supports PowerShell, cmd.exe, and recognized POSIX shells (sh, Bash, Dash, Ash, Ksh, Zsh, and Fish), including Git Bash on Windows. Other shell overrides are refused because their quoting rules cannot be inferred safely; run the node host inside WSL for native WSL paths. cmd.exe paths containing % or ! are also refused because that shell expands those characters even inside double quotes.

Invoking commands

Low-level (raw RPC):

openclaw nodes invoke --node <idOrNameOrIp> --command canvas.eval --params '{"javaScript":"location.href"}'

nodes invoke blocks system.run and system.run.prepare; those commands only run through the exec tool with host=node (see above). Higher-level helpers exist for the common "give the agent a MEDIA attachment" workflows (canvas, camera, screen, location, below).

Long-running streaming node commands use additive node.invoke.progress events. Each event carries the invoke ID, a zero-based sequence number, and a bounded UTF-8 text chunk; the Gateway orders chunks before delivering them to the caller. The existing node.invoke.result remains the single terminal response. Streaming callers can set an inactivity deadline that starts with the first progress event and resets after later progress while retaining the invoke's separate hard timeout during approval and execution. Result, hard timeout, inactivity timeout, and node disconnect all discard pending stream state. Caller cancellation emits node.invoke.cancel; the node host then terminates the matching process tree. Existing request/response commands are unchanged.

Command policy

Node commands must pass two gates before they can be invoked:

  1. The node must declare the command in its authenticated connect metadata (connect.commands).
  2. The gateway's platform-and-approval-derived allowlist must include the declared command.

Default allowlists by platform (before plugin defaults and commands.allow/commands.deny overrides):

PlatformDefault permitted commands
iOScamera.list, location.get, device.info, device.status, contacts.search, calendar.events, reminders.list, photos.latest, motion.activity, motion.pedometer, system.notify
watchOSdevice.info, device.status, system.notify
Androidcamera.list, location.get, notifications.list, notifications.actions, system.notify, device.info, device.status, device.permissions, device.health, device.apps, contacts.search, calendar.events, callLog.search, reminders.list, photos.latest, motion.activity, motion.pedometer
macOScamera.list, location.get, device.info, device.status, device.apps, contacts.search, calendar.events, reminders.list, photos.latest, motion.activity, motion.pedometer, system.notify
Windowscamera.list, location.get, device.info, device.status, system.notify
Linuxsystem.notify (node host commands such as system.run require approval, see below)

The table above defines the Gateway policy ceiling, not the full set of commands each node app implements. A command can only be used if the connected node also declares it. For example, the current macOS app does not declare the device or personal-data families that appear in the macOS policy row.

On iOS, Android, macOS, Windows, Linux, and unknown platforms, canvas.* commands (canvas.present, canvas.hide, canvas.navigate, canvas.eval, canvas.snapshot, canvas.a2ui.*) are enabled as a plugin default. Linux nodes only declare these commands when the desktop app's local Canvas socket is active. On iOS, all Canvas commands are restricted to the foreground.

talk.ptt.start, talk.ptt.stop, talk.ptt.cancel, and talk.ptt.once are permitted by default on any node that either advertises the talk capability or declares talk.* commands, regardless of the platform label.

Desktop host commands (system.run, system.run.prepare, system.which, browser.proxy, mcp.tools.call.v1, and screen.snapshot on macOS, Windows, and Linux) do not appear in the static platform-default table above. They become accessible only after an operator approves a pairing request that declares them; from that point onward, the node's approved command set retains them across reconnections.

Commands that are dangerous or privacy-sensitive still require an explicit opt-in using gateway.nodes.commands.allow, even if a node declares them: camera.snap, camera.clip, screen.record, computer.act, contacts.add, calendar.add, reminders.add, health.summary, sms.send, sms.search. gateway.nodes.commands.deny always takes precedence over defaults and any additional allowlist entries. Refer to HealthKit summaries for the iPhone consent gate and Computer use for the extra capability, tool-policy, arming, and platform-fulfiller gates surrounding desktop input.

Commands owned by a plugin node can include a Gateway node-invoke policy. This policy executes after the allowlist check and before the command is forwarded to the node, so raw node.invoke, CLI helpers, and dedicated agent tools all operate within the same plugin permission boundary. Dangerous plugin node commands still require an explicit gateway.nodes.commands.allow opt-in.

When a node modifies its declared command list, reject the old device pairing and approve the new request so the gateway stores the updated command snapshot.

Config (openclaw.json)

Settings related to nodes are located under gateway.nodes and tools.exec:

{
  gateway: {
    nodes: {
      // Auto-approve first-time node pairing from trusted networks (CIDR list).
      // Disabled when unset. Only applies to first-time role:node requests
      // with no requested scopes; does not auto-approve upgrades.
      pairing: {
        autoApproveCidrs: ["192.168.1.0/24"],
        // SSH-verified auto-approval (default: enabled). Approves first-time
        // node pairing on an exact device-key match read back over SSH.
        sshVerify: true,
      },
      // Trust agent-visible plugin tools published by paired nodes (default: true).
      pluginTools: {
        enabled: true,
      },
      // Opt into dangerous/privacy-heavy node commands (camera.snap, etc.).
      commands: {
        allow: ["camera.snap", "screen.record"],
        // Block exact command names even if defaults or commands.allow include them.
        deny: ["camera.clip"],
      },
    },
  },
  tools: {
    exec: {
      // Default exec host: "node" routes all exec calls to a paired node.
      host: "node",
      // Security mode for node exec: allow only approved/allowlisted commands.
      security: "allowlist",
      // Pin exec to a specific node (id or name). Omit to allow any node.
      node: "build-node",
    },
  },
}

Use exact node command names. commands.deny removes a command even when a platform default or an commands.allow entry would otherwise permit it. Paired nodes can publish agent-visible plugin tool descriptors by default, but each descriptor's command must still fall within the node's approved command surface. Set gateway.nodes.pluginTools.enabled: false to ignore all such descriptors. See Gateway configuration reference for details on gateway node pairing and command-policy fields.

Per-agent exec node override:

{
  agents: {
    list: [
      {
        id: "main",
        tools: { exec: { node: "build-node" } },
      },
    ],
  },
}

Screenshots (canvas snapshots)

If the node is displaying the Canvas (WebView), canvas.snapshot returns { format, base64 }.

CLI helper (writes to a temporary file and outputs the saved path):

openclaw nodes canvas snapshot --node <idOrNameOrIp> --format png
openclaw nodes canvas snapshot --node <idOrNameOrIp> --format jpg --max-width 1200 --quality 0.9

Canvas controls

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) on nodes that support local paths, along with optional --x/--y/--width/--height for positioning. On Linux, Canvas accepts HTTP(S) URLs or its bundled A2UI renderer.
  • canvas eval accepts inline JavaScript (--js) or a positional argument.

A2UI (Canvas)

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:

  • Mobile and Linux desktop nodes use a bundled app-owned A2UI page for action-capable rendering.
  • Only A2UI v0.8 JSONL is supported (v0.9/createSurface is rejected).
  • iOS and Android render remote Gateway Canvas pages, but A2UI button actions are dispatched only from the bundled app-owned A2UI page. Gateway-hosted HTTP/HTTPS A2UI pages are render-only on those mobile clients.
  • macOS can dispatch actions from the exact capability-scoped Gateway A2UI page selected by the app. Other HTTP/HTTPS pages remain render-only.
  • Linux dispatches actions only from the bundled A2UI page. Other HTTP/HTTPS pages remain render-only, and a headless Linux node without the desktop app does not advertise Canvas.

Photos + videos (node camera)

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
openclaw nodes camera snap --node <idOrNameOrIp> --device-id <id> --max-width 1200 --quality 0.9 --delay-ms 2000

Video clips (mp4):

openclaw nodes camera clip --node <idOrNameOrIp> --duration 10s
openclaw nodes camera clip --node <idOrNameOrIp> --duration 3000 --no-audio

Notes:

  • The node must run in the foreground for canvas.* and camera.* to work (background calls return NODE_BACKGROUND_UNAVAILABLE).
  • To keep the base64 payload size manageable, nodes clamp clip duration (see Camera capture for platform-specific limits). The nodes agent tool further restricts the requested durationMs to 300000 (5 minutes) before passing the call along; the node itself enforces whichever limit is tighter.
  • When possible, Android will ask for CAMERA/RECORD_AUDIO permissions; if denied, the call fails with *_PERMISSION_REQUIRED.

Screen recordings (nodes)

Supported nodes provide 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:

  • Whether screen.record is available depends on the node's platform.
  • The nodes agent tool limits the requested durationMs to 300000 (5 minutes); the node may apply a stricter cap to keep the returned payload within bounds.
  • On supported platforms, --no-audio turns off microphone capture.
  • When multiple displays are connected, use --screen <index> to pick one (0 = primary).

Location (nodes)

Nodes expose location.get when Location is turned on 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:

  • Location is disabled by default.
  • "Always" requires a system permission; background fetching is done on a best-effort basis.
  • The response contains lat/lon, accuracy in meters, and a timestamp.
  • For the full parameter and response structure plus error codes, see Location command.

SMS (Android nodes)

On Android, nodes can expose sms.send and sms.search once the user grants SMS permission and the device supports telephony. Both commands are classified as dangerous by default: the gateway operator must also add them to gateway.nodes.commands.allow before they can be used (see Command policy).

To opt in explicitly for read-only SMS search, set this in openclaw.json:

{
  gateway: {
    nodes: {
      commands: { allow: ["sms.search"] },
    },
  },
}

Add sms.send separately only if the node should also be able to send messages. Android permission and Gateway command authorization are independent; granting the phone permission does not modify Gateway policy.

Low-level invoke:

openclaw nodes invoke --node <idOrNameOrIp> --command sms.send --params '{"to":"+15555550123","message":"Hello from OpenClaw"}'

Notes:

  • sms.search can be declared before READ_SMS is granted, allowing an invocation to return a permission diagnostic; reading messages still requires the Android permission.
  • Devices that are Wi-Fi only and lack telephony will not advertise sms.send.
  • A requires explicit gateway.nodes.commands.allow opt-in error indicates the phone declared the command, but the Gateway operator has not authorized it.

Device and personal data commands

iOS and Android nodes advertise several read-only data commands by default (see the Command policy table); Android additionally exposes a larger set gated by its own in-app settings. A macOS or headless-mac TypeScript node host advertises device.apps only after the operator enables installed-app sharing with --share-installed-apps.

Available families:

  • device.status, device.info work on iOS, Android, and Windows.
  • device.permissions and device.health are supported exclusively on Android.
  • device.apps applies to Android, macOS, and headless-mac nodes. On Android, Installed Apps sharing must be enabled in Settings, and only launcher-visible apps are returned by default. TypeScript node hosts keep sharing disabled by default and accept query, limit, and includeSystem; macOS results include label, bundleId, path, and system.
  • notifications.list and notifications.actions are Android-only.
  • photos.latest is available on iOS and Android.
  • contacts.search works on iOS and Android, with read-only as the default; contacts.add is dangerous and requires gateway.nodes.commands.allow.
  • calendar.events works on iOS and Android, with read-only as the default; calendar.add is dangerous and requires gateway.nodes.commands.allow.
  • reminders.list works on iOS and Android, with read-only as the default; reminders.add is dangerous and requires gateway.nodes.commands.allow.
  • callLog.search is Android-only.
  • motion.activity and motion.pedometer work on iOS and Android; their availability depends on which sensors the device has.

Example invokes:

openclaw nodes invoke --node <idOrNameOrIp> --command device.status --params '{}'
openclaw nodes invoke --node <idOrNameOrIp> --command device.apps --params '{"limit":10}'
openclaw nodes invoke --node <idOrNameOrIp> --command notifications.list --params '{}'
openclaw nodes invoke --node <idOrNameOrIp> --command photos.latest --params '{"limit":1}'

System commands (node host / mac node)

The macOS node provides system.run, system.which, system.notify, and system.execApprovals.get/set. The headless node host provides system.run.prepare, 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 '{"bins":["git"]}'

Notes:

  • system.run provides stdout, stderr, and the exit code inside the payload.
  • Shell execution now uses the 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 are only available on the exec path.
  • The exec path generates a canonical systemRunPlan before approval. Once approved, the gateway forwards that stored plan, ignoring any later caller modifications to command, cwd, or session fields.
  • system.notify respects notification permission state on the macOS app and supports --priority <passive|active|timeSensitive> and --delivery <system|overlay|auto>.
  • Unrecognized node platform / deviceFamily metadata uses a conservative default allowlist that excludes system.run and system.which. To intentionally use those commands on an unknown platform, add them explicitly through gateway.nodes.commands.allow.
  • system.run supports --cwd, --env KEY=VAL, --command-timeout, and --needs-screen-recording.
  • For shell wrappers (bash|sh|zsh ... -c/-lc), request-scoped --env values are reduced to an explicit allowlist (TERM, LANG, LC_*, COLORTERM, NO_COLOR, FORCE_COLOR).
  • For allow-always decisions in allowlist mode, known dispatch wrappers (env, flock, nice, nohup, stdbuf, timeout) persist inner executable paths instead of wrapper paths. If unwrapping is unsafe, no allowlist entry is persisted automatically.
  • On Windows node hosts in allowlist mode, shell-wrapper runs via cmd.exe /c require approval (an allowlist entry alone does not auto-allow the wrapper form).
  • Node hosts ignore PATH overrides in --env and strip a large, maintained set of interpreter and shell startup variables (for example NODE_OPTIONS, PYTHONPATH, BASH_ENV, DYLD_*, LD_*) before running a command. To add extra PATH entries, configure the node host service environment (or install tools in standard locations) instead of passing PATH via --env.
  • On macOS node mode, system.run is gated by exec approvals in the macOS app (Settings → Exec approvals). Ask, allowlist, and full behave identically to the headless node host; denied prompts return SYSTEM_RUN_DENIED.
  • On headless node host, system.run is gated by exec approvals (~/.openclaw/exec-approvals.json); on macOS specifically, see the exec-host routing env vars under Headless node host below.

Exec node binding

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.entries
openclaw config set 'agents.entries.main.tools.exec.node' "node-id-or-name"

Unset to allow any node:

openclaw config unset tools.exec.node
openclaw config unset 'agents.entries.main.tools.exec.node'

Permissions map

Nodes can carry a permissions map inside node.list / node.describe, indexed by permission name (for example screenRecording, accessibility, location) and holding boolean values (true means permission is granted).

Headless node host (cross-platform)

OpenClaw supports a headless node host (no graphical interface) that links to the Gateway WebSocket and makes system.run / system.which available. This works well on Linux/Windows or when a minimal node is needed beside a server.

Launch it:

openclaw node run --host <gateway-host> --port 18789

Points to keep in mind:

  • Pairing remains necessary (the Gateway will display a device pairing prompt).
  • Separate state records manage client instance metadata, signed device identity, and pairing authentication; refer to Headless identity state.
  • Local enforcement of exec approvals happens through ~/.openclaw/exec-approvals.json (check Exec approvals).
  • On macOS, the headless node host runs system.run locally as the default. Use OPENCLAW_NODE_EXEC_HOST=app to send system.run through the companion app exec host; add OPENCLAW_NODE_EXEC_FALLBACK=0 to mandate the app host and fail closed if it is unavailable.
  • Include --tls / --tls-fingerprint when the Gateway WS uses TLS.

Mac node mode

  • The macOS menubar app links to the Gateway WS server as a node (so openclaw nodes … operates against this Mac).
  • In remote mode, the app creates an SSH tunnel for the Gateway port and connects to localhost.