- name
- winnode
- description
- Invoke and troubleshoot OpenClaw Windows-node commands through the local winnode CLI and MCP endpoint, including command argument schemas and A2UI JSONL calls.
<!--
REGENERATE-ME-WHEN-CAPABILITIES-CHANGE
The list of supported commands below is checked at CI time against the live
capability surface (see SkillMdDriftTests). When a capability is added,
removed, or renamed in src/OpenClaw.Shared/Mcp/McpToolBridge.cs
(CommandDescriptions), update this document so the drift test stays green -
the test compares command identifiers, so prose can still be tweaked by hand.
-->
# winnode skill reference
`winnode.exe` invokes OpenClaw Windows-node commands on the local tray over a
loopback MCP HTTP endpoint (default `http://127.0.0.1:8765/`). Enable
**Local MCP Server** in the tray's Settings → Advanced before calling.
This document is the agent-facing reference: every supported command, its
argument shape, and the A2UI v0.8 JSONL grammar. It is shipped alongside
`winnode.exe` so an agent can read it once and emit token-efficient calls.
---
## Invocation shape
```
winnode --command <name> [--params '<json-object>'] [--invoke-timeout <ms>] [--identity release|dev]
winnode --list-tools [--mcp-url <url>|--mcp-port <port>] [--identity release|dev]
```
- `--command` (required) - node command (e.g. `system.which`, `canvas.a2ui.push`).
- `--list-tools` - query the live MCP server's `tools/list` method and print the
advertised tools. Useful when settings-gated capabilities differ from this
static reference.
- `--params` - single JSON **object** string, default `{}`. Must be a JSON object,
not an array or scalar. **`--params @<path>`** loads the JSON object from a
file on disk (useful for big A2UI payloads / `canvas.eval` scripts).
- `--invoke-timeout` - milliseconds, default 15000, max 600000 (10 min). HTTP
timeout adds a 5s buffer.
- `--node` - accepted for parity with `openclaw nodes invoke`; **ignored**
locally. Safe to copy/paste from gateway-side commands.
- `--idempotency-key` - accepted for parity; **ignored**, and the CLI emits a
`[winnode] WARN` to stderr because local MCP does *not* dedupe retries -
re-running a command after a transient failure can double-execute side
effects. If you need idempotency, target the gateway, not winnode.
- `--mcp-url <url>` / `--mcp-port <port>` - override the endpoint. Falls back to
`OPENCLAW_MCP_PORT` env var, then port 8765. `--mcp-port` must be in
`[1, 65535]`; out of range fails with exit code 2.
- `--mcp-token <token>` - bearer token override (testing / explicit only). The
literal value is **visible to other same-user processes via the OS process
listing** (`Get-CimInstance Win32_Process | Select CommandLine`,
Process Explorer, etc.). The CLI emits a stderr warning when this flag is
used. **Prefer `OPENCLAW_MCP_TOKEN` (env var) or the on-disk
`%APPDATA%\OpenClawTray\mcp-token.txt`** which the release tray writes when
MCP is enabled. Both `OPENCLAW_MCP_TOKEN` and the on-disk file should
themselves be treated as sensitive operational secrets.
- `--identity release|dev` - selects which tray profile supplies the default
on-disk MCP token. Defaults to `OPENCLAW_APP_IDENTITY`, then `release`.
Use `--identity dev` for a side-by-side dev tray; its default token path is
`%APPDATA%\OpenClawTray-Dev\mcp-token.txt`. `OPENCLAW_TRAY_DATA_DIR` still
wins for isolated runs and points directly at the data folder.
- `--verbose` - log endpoint + ignored flags to stderr. Without `--verbose`,
HTTP error bodies are emitted only as the first line; with `--verbose`, the
full body is shown (after sanitization + token-shape redaction).
**Output contract:** stdout receives the capability payload as pretty-printed
JSON (matches `openclaw nodes invoke`). stderr receives errors. Exit code:
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Tool error, JSON-RPC error, transport failure, or HTTP non-2xx |
| 2 | Argument error (missing/invalid flags, bad `--params` JSON, out-of-range port/timeout, non-http URL) |
**Off-loopback safety:** when `--mcp-url` points at a non-loopback host, the
CLI **refuses to send the auto-loaded local MCP token** (and warns on stderr).
An explicitly supplied `--mcp-token` is honored with a warning. This preserves
the loopback-only threat model the tray's MCP server relies on.
---
## Commands
### system.notify
Show a Windows toast notification.
```
{"title": "OpenClaw", "body": "string", "subtitle": "string", "sound": true}
```
Returns `{ "sent": true }`. All fields optional except `body` in practice.
### system.run
Execute canonical argv. Subject to the local exec approval policy at
`%APPDATA%\OpenClawTray\exec-approvals.json`.
```
{
"command": ["executable", "arg", ...], // required
"rawCommand": "string", // optional display metadata
"cwd": "string",
"timeoutMs": 30000
}
```
Shell behavior must be explicit in the argv, for example
`{"command":["cmd.exe","/d","/s","/c","echo hello"],"rawCommand":"echo hello"}`.
The gateway's `exec host=node` path performs this wrapping automatically.
Non-empty custom `env` is rejected until environment values can be identity-bound
and shown safely during approval.
#### Migration from the pre-V2 low-level contract
The V2 node boundary intentionally rejects string-form `command` with
`command-array-required`. Update raw MCP, `node.invoke`, plugin, and direct
`winnode` callers to send the shell transport explicitly:
```json
// Before
{"command":"echo hello","shell":"cmd"}
// V2
{"command":["cmd.exe","/d","/s","/c","echo hello"],"rawCommand":"echo hello"}
```
PowerShell callers must likewise name `powershell.exe` or `pwsh.exe` and its
`-Command` arguments explicitly. Remove custom `env` from the request; V2 rejects
non-empty environments rather than approving an executable under hidden process
configuration.
Returns `{ stdout, stderr, exitCode, timedOut, durationMs }`.
### system.run.prepare
Pre-flight a `system.run` invocation. Same args as `system.run`. Returns the
parsed plan (`argv`, `cwd`, `rawCommand`, `agentId`, `sessionKey`) without
executing.
### system.which
Resolve binary names to absolute paths.
```
{"bins": ["git", "node", "powershell"]}
```
Returns `{ "bins": { "git": "C:\\...", ... } }`. Names not found are omitted.
### system.execApprovals.get
No params. Returns the active V2 snapshot:
`{ path, exists, hash, file: { version, defaults: { security, ask, askFallback, autoAllowSkills }, agents: { "<agentId>": { security, ask, askFallback, autoAllowSkills, allowlist: [{ id, pattern, lastUsedAt?, lastResolvedPath? }] } } } }`.
`security` is `deny|allowlist|full`; `ask` is `off|on-miss|always|deny`;
`askFallback` is `deny|allowlist|full`. Socket credentials are never returned.
Pass `hash` as `baseHash` when calling `system.execApprovals.set`.
### system.execApprovals.set
Replace the full V2 file using compare-and-swap. `baseHash` is required and must
equal the latest hash returned by `system.execApprovals.get`.
```
{
"baseHash": "<hash-from-get>",
"file": {
"version": 1,
"defaults": {
"security": "allowlist",
"ask": "on-miss",
"askFallback": "deny",
"autoAllowSkills": false
},
"agents": {
"main": {
"security": "allowlist",
"ask": "on-miss",
"askFallback": "deny",
"autoAllowSkills": false,
"allowlist": []
}
}
}
}
```
Returns the updated snapshot. A remote update may preserve or remove existing
allowlist grants, but it cannot add or change grants or set full access. Missing or
stale hashes are rejected. This command changes future `system.run` authorization
decisions. The `winnode` path is local loopback MCP, so no gateway scope applies.
Remote operators must use the gateway `exec.approvals.node.*` control-plane
methods, which are separately gated by gateway admin scope.
### canvas.present
Open the WebView2 canvas window.
```
{
"url": "string", // OR "html": "string"
"html": "string",
"width": 800, "height": 600,
"x": -1, "y": -1, // -1 centers
"title": "Canvas",
"alwaysOnTop": false
}
```
Returns `{ "presented": true }`.
### canvas.hide
No params. Hides the canvas without destroying state.
### canvas.navigate
```
{"url": "https://..."} // also accepts file:// or local canvas paths
```
### canvas.eval
```
{"script": "document.title"} // also accepts "javaScript" or "javascript"
```
Returns the evaluated result.
### canvas.snapshot
```
{"format": "png|jpeg", "maxWidth": 1200, "quality": 80}
```
Returns `{ format, base64 }`.
### canvas.a2ui.push
Render an A2UI v0.8 surface in the canvas. The canvas window opens
automatically - no `canvas.present` required.
```
{
"jsonl": "string", // OR jsonlPath
"jsonlPath": "string", // must live under %TEMP%
"props": {} // optional
}
```
Returns `{ "pushed": true }`. **See A2UI grammar below.**
### canvas.a2ui.pushJSONL
Alias of `canvas.a2ui.push`. It uses the identical handler, size/line caps, and
`jsonl`/`jsonlPath` contract. It exists for naming parity, not as a separate
streaming path.
### canvas.a2ui.reset
No params. Clears any rendered surfaces. Returns `{ "reset": true }`.
### canvas.a2ui.dump
No params. Returns the current surface graph for introspection. **Read-all:**
this exposes every currently-rendered surface - operators should treat it as
equivalent to a screenshot of every open A2UI surface.
### canvas.caps
No params. Returns renderer capabilities (renderer, snapshot, a2ui version).
### screen.snapshot
```
{
"format": "png|jpeg", "maxWidth": 1920, "quality": 80,
"monitor": 0, "screenIndex": 0, // 0 = primary
"includePointer": true
}
```
Returns `{ format, width, height, base64, image }` (image is a `data:` URL).
The first use requires remembered user consent. Every invocation displays a
visible notification before screen capture begins.
### screen.record
```
{
"durationMs": 5000, // required, max 300000
"format": "mp4|webm",
"monitor": 0, "screenIndex": 0,
"maxWidth": 1920, "fps": 30
}
```
Returns `{ format, durationMs, base64 }`.
### camera.list
No params. Returns `{ cameras: [{ deviceId, name, isDefault }] }`.
### camera.snap
```
{"deviceId": "string", "format": "jpeg|png", "maxWidth": 1280, "quality": 80}
```
Returns `{ format, width, height, base64 }`. `deviceId` defaults to system
default camera.
The first use requires remembered user consent. Every invocation displays a
visible notification before camera capture begins.
### camera.clip
```
{
"deviceId": "string", // optional
"durationMs": 3000, // required, max 60000
"format": "mp4|webm",
"maxWidth": 1280
}
```
Returns `{ format, durationMs, base64 }`.
## Speech-to-text (stt.*)
Local Whisper.net runs on this device - no audio leaves the box. The
model is downloaded on first use; until then every `stt.*` call returns
a clear error pointing the caller at the Voice Settings page.
**Privacy-sensitive: requires `NodeSttEnabled` in tray Settings.**
### stt.transcribe
Bounded fixed-duration mic capture + transcription.
```
{
"maxDurationMs": 5000, // required, > 0, max 30000
"language": "en" // optional BCP-47 tag or "auto" - falls back to SttLanguage setting
}
```
Returns `{ transcribed, text, durationMs, language, engineEffective: "whisper" }`.
### stt.listen
Mic capture with voice-activity detection. Returns when the user stops
speaking or after `timeoutMs`. Result is the full silence-bounded
utterance (all Whisper segments concatenated), not a partial first
segment.
```
{
"timeoutMs": 30000, // optional, default 30000, range 1000..120000
"language": "auto" // optional BCP-47 tag or "auto"
}
```
Returns `{ text, language, durationMs, segments[{ text, startMs, endMs }], engineEffective: "whisper" }`.
### stt.status
Engine readiness. No params. Carries no PII (no transcript history,
no language history, no device IDs, no model paths).
View on GitHub