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
| Method | Params | Result |
|---|---|---|
voicewake.get | none | { 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)
| Method | Params | Result |
|---|---|---|
voicewake.routing.get | none | { 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
| Event | Payload |
|---|---|
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.getand watches forvoicewake.changedso it remains in sync with other clients. - iOS: It calls
voicewake.set/voicewake.getand tracksvoicewake.changedto maintain responsive local wake-word detection. - Android: It uses
voicewake.set/voicewake.get, monitorsvoicewake.changed, and broadcastsvoiceWakewhile 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.