Nodes: Pairing, Capabilities, and CLI Helpers

Learn how companion devices link to the Gateway as nodes, offering command surfaces for camera, screen, notifications, and more. Includes macOS node mode and CLI usage for operators.

Read this when

  • Pairing iOS/watchOS/Android nodes to a gateway
  • Enabling isolated OpenClaw session hosting on a paired node
  • Using node camera or screen capture for agent context
  • Presenting a hosted widget on a Mac
  • Adding new node commands or CLI helpers

A node is a companion device (macOS/iOS/watchOS/Android/headless) that links to the Gateway using role: "node" and offers a command surface (for example camera.*, device.*, notifications.*, system.*) through node.invoke. The majority of nodes rely on 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 ordinary apps. Protocol details: Gateway protocol.

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

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

Nodes are peripherals, not gateways: they don't run the gateway service, and channel messages (Telegram, WhatsApp, etc.) land on the gateway, not on nodes.

Troubleshooting runbook: /nodes/troubleshooting

Pairing + status

Nodes use device pairing. A node presents a signed device identity during connect; the Gateway creates a device pairing request for role: node. Approve via 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 normal 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 keeps reconnecting keeps its one pending request (and requestId) alive instead of minting 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 get 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 is the durable approved-role contract. Token rotation stays inside that contract; it cannot upgrade a paired node into a role that pairing approval never granted.
  • node.pair.* (CLI: openclaw nodes pending/approve/reject/remove/rename) manages the node's approved command/capability surface on its canonical paired-device record. Device pairing owns both transport authentication and the durable node surface; there is no separate node pairing store.
  • openclaw nodes remove --node <id|name|ip> 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. 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/state/openclaw.sqlite#exec_approvals_config.

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.

Gateway deployments that cannot host nodes

A Gateway can remain healthy for browser users while node hosting is unavailable. Run openclaw doctor on the Gateway before onboarding nodes, and check these preconditions:

  • Machine authentication: Tailscale identity headers do not authenticate node-role connections. In gateway.auth.mode: "trusted-proxy", a new node also cannot supply the proxy's user identity headers. To use a shared token, switch to token mode and configure gateway.auth.token with a SecretRef; trusted-proxy mode rejects mixed token configuration. A trusted-proxy Gateway can use gateway.auth.password only for clean loopback/direct callers. See trusted-proxy mixed token configuration.
  • Node onboarding URL: With gateway.bind: "loopback", configure Tailscale Serve, gateway.remote.url, or plugins.entries.device-pair.config.publicUrl before minting a join code. Otherwise openclaw devices join-code reports: Gateway is only bound to loopback. Set gateway.bind=lan, enable tailscale serve, or configure plugins.entries.device-pair.config.publicUrl.
  • Node onboarding plugin: Join codes and openclaw connect require the bundled device-pair plugin. If it is disabled or excluded by plugin policy, set plugins.entries.device-pair.enabled: true, make sure device-pair is allowed, and restart the Gateway.
  • Device session runtime: Paired-device runners support the embedded OpenClaw runtime and explicitly authorized Codex remote-exec; ACPX routes cannot dispatch to a paired device. Codex requires codex.exec-server.stdio.v1 in gateway.nodes.commands.allow plus its normal pairing and invocation approvals. Runtime policy belongs on provider/model routes, not the ignored whole-agent runtime keys. Multi-agent rosters must also set agents.ownership: "explicit". See Codex paired-device placement and runtime policy.
  • Edge routing: When a reverse proxy or access edge fronts the Gateway, the node must satisfy edge auth on the join request, its main Gateway WebSocket, and the worker WebSocket. Keep WebSocket upgrade enabled for /__openclaw__/worker. You can instead exempt /j/* and /__openclaw__/worker from edge identity auth because both routes enforce their own short-lived credentials. See worker protocol.

For a Cloudflare Access-fronted Gateway:

  1. In Cloudflare Zero Trust, create an Access service token. Copy its Client ID and Client Secret when Cloudflare displays them.

  2. Add a Service Auth policy that accepts the token on the Access application protecting the Gateway. If /j/* and /__openclaw__/worker are separate Access applications, add the same policy to both.

  3. On the node, provide the conventional environment fallback and connect:

    export CF_ACCESS_CLIENT_ID="<client-id>"
    export CF_ACCESS_CLIENT_SECRET="<client-secret>"
    openclaw connect https://gateway.example/j/<code> --service
    

The canonical node connection keys are gateway.cloudflareAccess.clientId and gateway.cloudflareAccess.clientSecret; both accept SecretInput values. The environment fallback above persists those keys as env SecretRefs, not copied plaintext. For installed nodes, OpenClaw stores the environment values in the managed service environment file rather than inline in launchd, systemd, or Task Scheduler definitions. Resolved values are bound to the configured Gateway origin and are not followed across redirects. OpenClaw rejects the pair before resolution on plaintext http:// or ws:// routes; credential-free loopback and private-network plaintext behavior is unchanged.

Start a node host (foreground)

On the node machine:

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

For one-paste setup, create a Node host setup link from the Control UI Devices page, then run its copyable command on the node machine:

openclaw node run --pair "oc-pair://<setup-code>"

The link is single-use and expires after 10 minutes. It supplies the endpoint, bootstrap token, TLS mode, and certificate pin when available. Explicit gateway flags override the corresponding --pair values. Pairing does not pre-approve command execution; the first system.run request still follows the normal pending-approval or SSH-verification path. See Node pairing.

node run also accepts --pair, --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)

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.
  • Env vars are preferred: OPENCLAW_GATEWAY_TOKEN / OPENCLAW_GATEWAY_PASSWORD.
  • Config fallback is gateway.auth.token / gateway.auth.password.
  • In local mode, node host intentionally ignores gateway.remote.token / gateway.remote.password.
  • In remote mode, gateway.remote.token / gateway.remote.password are eligible per remote precedence rules.
  • If active local gateway.auth.* SecretRefs are configured but unresolved, node-host auth fails closed.
  • Node-host auth resolution only honors OPENCLAW_GATEWAY_* env vars.

Start a node host (service)

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

Alongside node install, the following are supported: --context-path, --tls, --tls-fingerprint, --node-id (legacy client instance ID only), --share-installed-apps / --no-share-installed-apps, --runtime <node> (defaults to node), and --force for reinstalling. Additional options include node status, node stop, and node uninstall.

Pair + name

On the machine acting as the gateway:

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

If the node retries using updated authentication details, execute openclaw devices list again and approve the current requestId.

Choices for naming:

  • --display-name on openclaw node run / openclaw node install (this persists in the shared node_host_config SQLite row, together with the client instance ID and Gateway connection metadata).
  • openclaw nodes rename --node <id|name|ip> --name "Build Node" (applies as a gateway-level override).

Node-hosted MCP servers

Set up MCP servers in openclaw.json on the node's 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 pushes the descriptors once connected. Tool calls route back to that node via mcp.tools.call.v1; the Gateway requires neither matching MCP configuration nor a JS plugin. This node-hosted v1 path does not support OAuth MCP servers.

During initial pairing, current node hosts declare the built-in mcp.tools.call.v1 command family even if no MCP server is set up. A node paired with an older OpenClaw version may ask for a one-time command-surface upgrade after the node host gets updated. Since the approved command family stays unchanged, adding, removing, or filtering servers afterward does not call for re-pairing. Apply node MCP configuration changes by restarting openclaw node run or openclaw node restart; the node host does not monitor this config.

Tool-list changes announced by servers take effect immediately and swap out the published node catalog. When an MCP transport shuts down or a stateful Streamable HTTP session lapses, the node pulls that server's stale tools and reconnects using bounded backoff. The call that failed due to the expired session is not replayed; a subsequent call can use the replacement connection once its tools are republished.

Gateway operators can disregard every agent-visible tool published by paired nodes, including node-hosted MCP tools, using gateway.nodes.pluginTools.enabled: false. Precise command denials like gateway.nodes.commands.deny: ["mcp.tools.call.v1"] also stop execution.

Node-hosted skills

Place skills in the active OpenClaw skills directory on the node machine, which defaults to ~/.openclaw/skills. The active profile is relocated by OPENCLAW_HOME, OPENCLAW_STATE_DIR, and OPENCLAW_CONFIG_PATH. For skills, OPENCLAW_STATE_DIR takes priority; otherwise, skills/ sits beside the path shown by openclaw config file. Once connected, the headless node host publishes valid SKILL.md files, and the Gateway includes them in agent skill snapshots only while that node stays connected. Each skill directory name must match the name frontmatter field so the abstract node locator resolves to a single entry without introducing another protocol field.

Skill publication is approved during the initial node-role pairing. Modifying, adding, or removing skills does not demand another pairing or Gateway configuration update. After changing node skill files, restart openclaw node run or openclaw node restart; the node host does not watch the skills directory.

Skill entries hosted on a node identify that node and carry their execution location. Skill files, relative paths they reference, and binaries all remain on that node. The agent reads the advertised node://.../SKILL.md location with the standard read tool. Operator-approved absolute node paths are accepted by file_fetch, not node skill locators; runtimes lacking the normal read tool can instead run cat SKILL.md through exec host=node node=<node-id>, using the advertised node://.../skills/<name> directory as workdir. Referenced files and binaries share the same exec target and workdir. The node host resolves that locator against its active OpenClaw state directory, so relative paths resolve on the node rather than the Gateway machine. The publishing node must have approved system.run, and the agent's exec policy must permit host=node; otherwise the skill stays out of that agent's snapshot.

Set nodeHost.skills.enabled: false on the node to halt publication. Gateway operators can ignore skills from all paired nodes with gateway.nodes.allowSkills: false.

Headless identity state

Within shared SQLite, the headless node maintains three distinct state records:

  • ~/.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 relies on the cryptographic device ID for both pairing and node routing. The client instance ID serves purely as connection metadata. Consequently, altering --node-id or retiring a node.json does not reset pairing. Refer to Identity and pairing state for the supported revoke-and-re-pair flow and upgrade notes.

Retired identity/device.json and identity/device-auth.json files serve as Doctor-owned migration inputs. Halt the node host and execute openclaw doctor --fix; Doctor imports and validates their rows in SQLite before deleting 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 live on the node host in ~/.openclaw/state/openclaw.sqlite#exec_approvals_config.

Point exec at the node

Configure defaults (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 set, any exec call with host=node executes on the node host (subject to the node allowlist/approvals).

host=auto will not implicitly 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 on that node. Agents use the Ollama plugin's node_inference tool to discover installed models and run a bounded prompt remotely; the Gateway does not need 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 gates 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 bundled anthropic plugin discovers non-archived Claude CLI and Claude Desktop sessions on the Gateway and paired nodes by default. Set plugins.entries.anthropic.config.sessionCatalog.enabled: false to disable the operator catalog and paired-node catalog commands without disabling Anthropic models or the Claude CLI backend. A remote macOS app node advertises anthropic.claude.sessions.list.v1 and anthropic.claude.sessions.read.v1 when the Anthropic plugin is enabled and ~/.claude/projects/ exists. Approve the node pairing upgrade when those commands first appear.

A native node host with the Claude CLI available also advertises anthropic.claude.terminal.resume.v1. Eligible CLI and Desktop rows can open claude --resume <session-id> in the operator terminal on their owning host. This is a takeover of the native session; unlike OpenClaw adoption, it does not fork the Claude session first.

The catalog combines valid Claude CLI project-index records with a bounded metadata fallback for unindexed JSONL transcripts. That fallback recognizes concurrent non-sidechain interactive (cli) and headless Agent SDK CLI (sdk-cli) sessions. Claude Desktop's local metadata supplies Desktop titles and archive state. Desktop metadata wins when both sources refer to the same Claude Code session ID; CLI-only transcripts remain visible because the CLI has no archive flag. Transcript reads use opaque byte-offset cursors and bounded backward file reads, so selecting a large session or loading an older page does not read the whole JSONL history into one Gateway response.

Catalog RPCs keep their normal method scopes: sessions.catalog.list and sessions.catalog.read require operator.read; sessions.catalog.continue and sessions.catalog.archive require operator.write.

Catalog visibility is tied to the authenticated caller as well. An operator.admin connection observes every row that discovery returns. When the Gateway holds durable profiles for fewer than two people, catalog visibility stays the same and rows remain unfiltered. On a multi-user Gateway, a non-admin connection can view, resume, or archive only rows whose recorded createdActor.id matches the caller's Gateway profile. Unattributed host CLI or desktop sessions are hidden from such callers. This acts as a privacy and coordination boundary inside a single trusted Gateway domain, not as isolation against hostile users; when people must not share access to files, credentials, or tools, use separate agents or Gateway/host trust boundaries. See Multi-user mode.

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 advertises 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 advertised and permitted by the Gateway's node command policy, a Claude CLI row on that node becomes continuable: OpenClaw imports bounded history, binds 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 advertise the run command remain view-only. The macOS app node does not advertise this command yet, so its rows remain view-only.

Host OpenClaw sessions

A headless node host can separately opt into full OpenClaw session hosting:

{
  nodeHost: {
    workerRuns: { enabled: true },
  },
}

Restart the node host after enabling this setting. On the first session dispatch for a Gateway build, the node downloads one sealed worker artifact from that paired Gateway, verifies its exact content hash, and publishes it atomically under the Gateway-namespaced node-host bundle root. The artifact already contains its complete JavaScript dependency closure; the node does not install packages or execute lifecycle scripts. Later turns reuse the immutable artifact while its receipt still matches the Gateway's current build.

You can also enroll and enable a service host in one step with openclaw connect --service --session-host. In Control UI New Session, a write-scoped operator selects a Gateway project or folder and then the paired device. OpenClaw creates a session-owned managed worktree on the Gateway, dispatches it with the exact deviceId, and sends the first turn only after the device placement becomes active. New Session does not bind execNode or browse the device filesystem.

The Devices page shows the validated Gateway-owned worker version in the node's metadata. If the retained artifact is missing or fails validation, Devices shows a worker missing warning; start a new session on that device to reinstall the current bundle. This status is observational and reconnect-scoped: launch still requires the exact durable receipt and current node authority.

Node hosts must support the current private worker-supervisor dialect before they can host sessions. An older connected host remains visible but disabled in the session picker. Update OpenClaw on that device and reconnect it; for a headless node, run openclaw update followed by openclaw node restart. The Gateway does not fall back to the node's local OpenClaw package or an older supervisor dialect.

This setting enables supervised session turns on the paired device, including Gateway-owned workspace transfer and result reconciliation. Each node runs at most two worker processes by default. A third launch waits up to 10 seconds for a durable slot; while both slots are occupied, the node remains available for status and cancellation but is not selected for a new session turn.

The picker derives every device row from environments.list. Every selected runtime requires an available, connected paired session host. OpenClaw worker turns additionally require valid exact worker slots with at least one free slot. Codex paired-device execution launches its exec-server directly, so it does not consume or require a worker slot; instead, its required command must appear in the node's effective invocableCommands, not merely its declared capabilities. A declared command is usable only when the approved pairing and Gateway command allowlist both authorize it. Connected non-hosts, ineligible or saturated hosts, update-required devices, and unavailable hosts remain visible but disabled with an actionable reason. Enable hosting with openclaw connect --service --session-host or the nodeHost.workerRuns setting, then restart the node host. Update-required hosts must be upgraded and restarted before selection.

When a known session host disconnects, its paired-device record preserves only the last accepted current-v6 hosting consent. The offline row remains visible and disabled with status unavailable. A current disabled or empty v6 publication records false; older v1-v5 and update-required dialects do not overwrite the last current fact. Connected inventory always wins over stored history, a missing stored value means false, and exact worker slots are never persisted or shown as offline capacity.

If the device is offline, its active placement remains active: availability is process-current, not a terminal placement state. sessions.list and sessions.describe project runner: { kind: "device", status: "offline" } until that exact current-v6 node runner reconnects. Gateway restart therefore shows an active device placement as offline until reconnect; current inventory then changes the projection to available and emits a session refresh. Exact worker slots gate only new placements whose runtime consumes a worker slot; they do not affect Codex remote execution or an existing session's availability.

Control UI shows Device offline and waits by default without giving up the placement, workspace, or authority. Retry the next turn after the device returns. Continue on Gateway… is a separate destructive choice: it fences the device owner and continues from the last Gateway-synced workspace without replaying the interrupted turn. Unsynced device files and in-flight work may be lost. A paired node remains dormant for 14 days after its exact recorded disconnect; at that boundary its old worker environment is treated as gone and the session placement reconciles normally. Pairing itself remains, so a later reconnect can provision a fresh environment. Legacy pairings without exact node disconnect history are retained fail-safe rather than expired from unrelated device activity. Removing the device pairing, silently pruning a superseded pairing, or removing only its node role invalidates clients first, then runs targeted environment and placement reconciliation; explicit removal waits for the credential fence before returning success, and the periodic sweep retries failed provider or placement cleanup.

See Anthropic: Claude sessions across computers for the Control UI behavior and storage sources.

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 advertises 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 relies on the session's stored working directory and the same allowlisted duplex PTY relay used by Codex and Claude. Arbitrary command execution on the node is not exposed.

Terminal file uploads

Through the Control UI, files can be dragged into a terminal that is paired with an open node. The native node host advertises the admin-only terminal.upload command; approve the pairing upgrade when it first appears. Each file is capped at 16 MiB, placed in a private temporary directory on that node, and handed back to the terminal as a shell-quoted path without being executed.

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

Invoking commands

Low-level (raw RPC):

openclaw nodes invoke --node <idOrNameOrIp> --command device.info --params '{}'

nodes invoke blocks system.run and system.run.prepare; those commands only execute through the exec tool with host=node (see above). Higher-level helpers exist for the common "give the agent a MEDIA attachment" workflows (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

Before a node command can be invoked, it must clear two gates:

  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, mobile.ui.observe, mobile.ui.act
macOScamera.list, camera.ptz.status, location.get, device.info, device.status, device.apps, contacts.search, calendar.events, reminders.list, photos.latest, motion.activity, motion.pedometer, system.notify, computer.act
Windowscamera.list, location.get, device.info, device.status, system.notify, computer.act
Linuxsystem.notify, computer.act (node host commands such as system.run need approval, details below)

What these rows define is the upper bound set by the Gateway policy, not the full set of commands each node application actually implements. For a command to be available, the connected node must also declare it. Android, for instance, only exposes its mobile UI commands when Accessibility Control is turned on, while desktop nodes only advertise computer.act when their local Computer Control fulfiller is active. The current macOS application does not declare the device or personal-data command families that appear in the macOS policy row.

Plugin-owned defaults add to the platform table, but only for the surface that the plugin supports:

PluginPlatformDefault permitted commands
CanvasmacOScanvas.present, canvas.hide, canvas.navigate

Canvas commands surface hosted widget documents inside the macOS app's native panel. Plugin defaults for Canvas are not delivered to iOS, Android, Windows, Linux, or unknown platforms.

talk.ptt.start, talk.ptt.stop, talk.ptt.cancel, and talk.ptt.once are enabled by default for any node that advertises the talk capability or declares talk.* commands, no matter what platform label it carries.

Desktop host commands (system.run, system.run.prepare, system.which, browser.proxy, browser.proxy.upload.v1, mcp.tools.call.v1, and screen.snapshot on macOS/Windows/Linux) sit outside the static platform-default table shown above. They only become available after the operator approves a pairing request that declares them; once that happens, the node's approved command set keeps them across reconnects.

Commands that are dangerous or privacy-heavy need a one-time persistent opt-in via gateway.nodes.commands.allow, even if the node declares them: camera.snap, camera.clip, camera.ptz.control, desktop.stream, screen.record, contacts.add, calendar.add, reminders.add, health.summary, sms.send, sms.search. gateway.nodes.commands.deny always takes precedence over defaults and extra allowlist entries. For the local enablement, pairing, capability, and tool-policy gates around desktop access, see Paired node desktops, HealthKit summaries, and Computer use.

A Gateway node-invoke policy can be added by plugin-owned node commands. That policy executes after the allowlist check and before forwarding to the node, so raw node.invoke, CLI helpers, and dedicated agent tools all share the same plugin permission boundary. Explicit gateway.nodes.commands.allow opt-in is still required for dangerous plugin node commands.

After a node updates its declared command list, reconnect it, check openclaw nodes pending, and approve the expanded surface with openclaw nodes approve <requestId> so the Gateway stores the refreshed command snapshot.

Config (openclaw.json)

Settings for 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,
      },
      // Persistently enable dangerous/privacy-heavy node commands.
      commands: {
        allow: ["camera.snap", "desktop.stream", "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 commands.allow entry would otherwise permit it. Paired nodes may 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 disregard all such descriptors. For gateway node pairing and command-policy field details, see Gateway configuration reference.

Per-agent exec node override:

{
  agents: {
    entries: {
      main: {
        default: true,
        tools: { exec: { node: "build-node" } },
      },
    },
  },
}

macOS widget panel

openclaw nodes canvas present --node <idOrNameOrIp>
openclaw nodes canvas hide --node <idOrNameOrIp>
openclaw nodes canvas navigate "/__openclaw__/canvas/documents/<document-id>/index.html" --node <idOrNameOrIp>

Notes:

  • canvas present takes the existing optional target plus --x/--y/--width/--height placement arguments.
  • canvas navigate accepts a hosted widget-document path or an app-local Canvas URL. The macOS app resolves hosted paths through its current scoped Canvas capability URL.
  • The agent-facing path is show_widget with presentation.target: "node_panel"; use the CLI helpers for direct operator control.
  • A2UI renders on session dashboards, not through node Canvas commands.

Photos + videos (node camera)

Photos (jpg):

openclaw nodes camera list --node <idOrNameOrIp>
openclaw nodes camera snap --node <idOrNameOrIp>            # default: one node-selected photo
openclaw nodes camera snap --node <idOrNameOrIp> --facing front
openclaw nodes camera snap --node <idOrNameOrIp> --facing both # front then back (2 saved paths)
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:

  • For camera.* to work, the node must run in the foreground; otherwise background calls come back as NODE_BACKGROUND_UNAVAILABLE.
  • Clip duration is clamped by nodes so the base64 payload stays reasonable; per-platform caps are listed under Camera capture. The nodes agent tool also limits any requested durationMs to 300000 (5 minutes) before passing the call along, while the node applies its own stricter ceiling.
  • When possible, Android asks for CAMERA/RECORD_AUDIO permissions; if denied, the call fails with *_PERMISSION_REQUIRED.

Screen recordings (nodes)

Nodes that support it expose screen.record (mp4). For 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 platform.
  • The nodes agent tool restricts requested durationMs to 300000 (5 minutes); the node may impose a tighter cap to keep the returned payload bounded.
  • Microphone capture can be turned off via --no-audio on platforms that support it.
  • When more than one display is present, use --screen <index> to pick one (0 = primary).

Location (nodes)

With Location enabled in settings, nodes expose location.get.

CLI helper:

openclaw nodes location get --node <idOrNameOrIp>
openclaw nodes location get --node <idOrNameOrIp> --accuracy precise --max-age 15000 --location-timeout 10000

Notes:

  • Location starts disabled by default.
  • "Always" needs system permission; background fetch is only best-effort.
  • The reply carries lat/lon, accuracy in meters, and a timestamp.
  • Full parameter/response shape and error codes are in Location command.

SMS (Android nodes)

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

For read-only SMS search, opt in explicitly via openclaw.json:

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

Add sms.send separately only if the node should also send messages. Android permission and Gateway command authorization are independent: granting the phone permission does not alter 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 so an invocation returns a permission diagnostic; reading messages still needs that 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

By default, iOS and Android nodes advertise several read-only data commands (see the Command policy table); Android also exposes a larger family 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, supported on iOS, Android, and Windows.
  • device.permissions, device.health, limited to Android.
  • device.apps, works with Android, macOS, and headless-mac nodes. On Android, Installed Apps sharing must be enabled in Settings, and by default only launcher-visible apps are returned. TypeScript node hosts keep sharing disabled unless configured, accepting query, limit, and includeSystem; macOS output includes label, bundleId, path, and system.
  • notifications.list, notifications.actions, Android is the only platform.
  • photos.latest, available on iOS and Android.
  • contacts.search, iOS and Android, with read-only as the default; contacts.add carries risk and requires gateway.nodes.commands.allow.
  • calendar.events, iOS and Android, with read-only as the default; calendar.add carries risk and requires gateway.nodes.commands.allow.
  • reminders.list, iOS and Android, with read-only as the default; reminders.add carries risk and requires gateway.nodes.commands.allow.
  • callLog.search, Android only.
  • motion.activity, motion.pedometer, iOS and Android; 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)

From the macOS node, system.run, system.which, system.notify, and system.execApprovals.get/set are exposed. 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:

  • The system.run payload carries stdout, stderr, and the exit code.
  • Shell execution now routes through the exec tool using host=node; nodes stays as the direct-RPC interface for explicit node commands.
  • Neither system.run nor system.run.prepare is exposed by nodes invoke; both remain confined to the exec path.
  • Before approval, the exec path builds a canonical systemRunPlan. Once approved, the gateway forwards that stored plan, ignoring any later caller modifications to command, cwd, or session fields.
  • On the macOS app, system.notify honors the notification permission state and supports --priority <passive|active|timeSensitive> and --delivery <system|overlay|auto>.
  • For unrecognized node platform / deviceFamily metadata, a conservative default allowlist applies, excluding system.run and system.which. If those commands are needed for an unknown platform, add them explicitly with gateway.nodes.commands.allow.
  • A system.run request carries cwd, an env map, timeoutMs, and needsScreenRecording as payload fields on the exec path (see above), not as nodes invoke CLI flags.
  • For shell wrappers (bash|sh|zsh ... -c/-lc), request-scoped env values are trimmed to an explicit allowlist: TERM, LANG, LC_*, COLORTERM, NO_COLOR, FORCE_COLOR.
  • In allowlist mode, known dispatch wrappers (env, flock, nice, nohup, stdbuf, timeout) persist inner executable paths rather than wrapper paths for allow-always decisions. If unwrapping is unsafe, no allowlist entry is saved automatically.
  • On Windows node hosts in allowlist mode, shell-wrapper executions via cmd.exe /c need approval; an allowlist entry alone does not auto-allow the wrapper form.
  • Node hosts disregard PATH overrides in the env object and remove a broad, maintained set of interpreter/shell startup variables (such as NODE_OPTIONS, PYTHONPATH, BASH_ENV, DYLD_*, LD_*) before running a command. To add PATH entries, configure the node host service environment (or install tools in standard locations) rather than passing PATH through env.
  • In macOS node mode, system.run is controlled 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 the headless node host, system.run is controlled by the local SQLite exec approvals row; for macOS specifically, check the exec-host routing env vars under Headless node host below.

Exec node binding

When several nodes are present, exec can be bound to a particular node. That node becomes the default for exec host=node, with per-agent overrides possible.

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 permit 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, organized by permission name (for instance screenRecording, accessibility, location) with boolean entries (true = granted).

Headless node host (cross-platform)

A headless node host (no UI) can be run by OpenClaw, which links to the Gateway WebSocket and makes system.run / system.which available. This suits Linux/Windows environments or a minimal node running beside a server.

Launch it:

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

Notes:

  • Pairing remains mandatory (the Gateway presents a device pairing prompt).
  • Separate state records hold client instance metadata, signed device identity, and pairing auth; refer to Headless identity state.
  • Exec approvals are checked locally through ~/.openclaw/state/openclaw.sqlite#exec_approvals_config (see Exec approvals).
  • On macOS, the headless node host runs system.run locally by default. Set OPENCLAW_NODE_EXEC_HOST=app to send system.run via the companion app exec host; add OPENCLAW_NODE_EXEC_FALLBACK=0 to demand the app host and fail closed if it is not reachable.
  • Include --tls / --tls-fingerprint when TLS is used by the Gateway WS.

Mac node mode

  • The macOS menubar app acts as a node connected to the Gateway WS server (so openclaw nodes … operates against this Mac).
  • In remote mode, the app opens an SSH tunnel for the Gateway port and links to localhost.
7,233 words · updated Aug 25, 2026