Voice Wake Words: Gateway-Shared List and Sync

Learn how voice wake words are managed as a single shared list owned by the Gateway, synced across nodes. Includes storage details and platform-specific toggles.

Read this when

  • Changing voice wake words behavior or defaults
  • Adding new node platforms that need wake word sync

Wake words are managed as a single shared list that belongs to the Gateway, so custom per-node lists do not exist. Any node or app interface is free to modify the list; the Gateway saves the update and pushes it out to all connected clients.

  • macOS: A local toggle turns Voice Wake on or off. macOS 26 or newer is required; check Voice wake (macOS) for details on runtime behavior and PTT.
  • iOS: The Settings screen offers a local toggle for enabling or disabling Voice Wake.
  • Android: Settings → Voice provides a local toggle plus an editor for wake words. On-device speech recognition is a prerequisite.

Storage

The Gateway state database holds both wake words and routing rules. By default this is ~/.openclaw/state/openclaw.sqlite, though OPENCLAW_STATE_DIR can change it. The relevant tables are voicewake_triggers, voicewake_routing_config, and voicewake_routing_routes. Older settings/voicewake.json and settings/voicewake-routing.json entries serve only as inputs for openclaw doctor --fix migration, and the runtime never reads them.

Protocol

Trigger list

MethodParamsResult
voicewake.getnone{ triggers: string[] }
voicewake.set{ triggers: string[] }{ triggers: string[] }

voicewake.set handles input normalization by trimming whitespace, discarding blank entries, capping the trigger count at 32, and shortening each trigger to 64 UTF-16 code units while avoiding surrogate pair splits. If nothing remains, the built-in defaults (openclaw, claude, computer) are used instead.

Routing (trigger to target)

MethodParamsResult
voicewake.routing.getnone{ config: VoiceWakeRoutingConfig }
{
  "version": 1,
  "defaultTarget": { "mode": "current" },
  "routes": [{ "trigger": "robot wake", "target": { "sessionKey": "agent:main:main" } }],
  "updatedAtMs": 1730000000000
}

A route target accepts one of the following, and only one:

  • { "mode": "current" }
  • { "agentId": "main" }
  • { "sessionKey": "agent:main:main" }

Restrictions apply: no more than 32 routes are allowed, and trigger text cannot exceed 64 characters. For matching and duplicate checks, route triggers undergo normalization that lowercases everything, removes punctuation at the start and end of each word, and condenses whitespace (so "Hey, Bot!!" and "hey bot" are treated as identical and flagged as duplicates). This is a stricter process than the basic trim used for the global trigger list described earlier.

Events

EventPayload
voicewake.changed{ triggers: string[] }
voicewake.routing.changed{ config: VoiceWakeRoutingConfig }

Both events reach every WebSocket client that holds read scope (such as the macOS app, WebChat, and comparable clients) as well as every connected node. Immediately after a node establishes its connection, it also receives both events as an initial snapshot.

Client behavior

  • macOS: It invokes voicewake.set/voicewake.get and watches for voicewake.changed so it remains in sync with other clients.
  • iOS: It calls voicewake.set/voicewake.get and tracks voicewake.changed to maintain responsive local wake-word detection.
  • Android: It uses voicewake.set/voicewake.get, monitors voicewake.changed, and broadcasts voiceWake while enabled. Recognition runs on-device and only while the app is in the foreground; it halts whenever Talk, manual dictation, voice-note capture, or message speech takes over audio.
549 words · updated Aug 14, 2026