# Brave Search Source: https://cryptoclawdocs.termix.ai/brave-search # Brave Search API OpenClaw supports Brave Search as a web search provider for `web_search`. ## Get an API key 1. Create a Brave Search API account at [https://brave.com/search/api/](https://brave.com/search/api/) 2. In the dashboard, choose the **Data for Search** plan and generate an API key. 3. Store the key in config (recommended) or set `BRAVE_API_KEY` in the Gateway environment. ## Config example ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { tools: { web: { search: { provider: "brave", apiKey: "BRAVE_API_KEY_HERE", maxResults: 5, timeoutSeconds: 30, }, }, }, } ``` ## Tool parameters | Parameter | Description | | ------------- | ------------------------------------------------------------------- | | `query` | Search query (required) | | `count` | Number of results to return (1-10, default: 5) | | `country` | 2-letter ISO country code (e.g., "US", "DE") | | `language` | ISO 639-1 language code for search results (e.g., "en", "de", "fr") | | `ui_lang` | ISO language code for UI elements | | `freshness` | Time filter: `day` (24h), `week`, `month`, or `year` | | `date_after` | Only results published after this date (YYYY-MM-DD) | | `date_before` | Only results published before this date (YYYY-MM-DD) | **Examples:** ```javascript theme={"theme":{"light":"min-light","dark":"min-dark"}} // Country and language-specific search await web_search({ query: "renewable energy", country: "DE", language: "de", }); // Recent results (past week) await web_search({ query: "AI news", freshness: "week", }); // Date range search await web_search({ query: "AI developments", date_after: "2024-01-01", date_before: "2024-06-30", }); ``` ## Notes * The Data for AI plan is **not** compatible with `web_search`. * Brave provides paid plans; check the Brave API portal for current limits. * Brave Terms include restrictions on some AI-related uses of Search Results. Review the Brave Terms of Service and confirm your intended use is compliant. For legal questions, consult your counsel. * Results are cached for 15 minutes by default (configurable via `cacheTtlMinutes`). See [Web tools](/tools/web) for the full web\_search configuration. # BlueBubbles Source: https://cryptoclawdocs.termix.ai/channels/bluebubbles # BlueBubbles (macOS REST) Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. **Recommended for iMessage integration** due to its richer API and easier setup compared to the legacy imsg channel. ## Overview * Runs on macOS via the BlueBubbles helper app ([bluebubbles.app](https://bluebubbles.app)). * Recommended/tested: macOS Sequoia (15). macOS Tahoe (26) works; edit is currently broken on Tahoe, and group icon updates may report success but not sync. * OpenClaw talks to it through its REST API (`GET /api/v1/ping`, `POST /message/text`, `POST /chat/:id/*`). * Incoming messages arrive via webhooks; outgoing replies, typing indicators, read receipts, and tapbacks are REST calls. * Attachments and stickers are ingested as inbound media (and surfaced to the agent when possible). * Pairing/allowlist works the same way as other channels (`/channels/pairing` etc) with `channels.bluebubbles.allowFrom` + pairing codes. * Reactions are surfaced as system events just like Slack/Telegram so agents can "mention" them before replying. * Advanced features: edit, unsend, reply threading, message effects, group management. ## Quick start 1. Install the BlueBubbles server on your Mac (follow the instructions at [bluebubbles.app/install](https://bluebubbles.app/install)). 2. In the BlueBubbles config, enable the web API and set a password. 3. Run `openclaw onboard` and select BlueBubbles, or configure manually: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { bluebubbles: { enabled: true, serverUrl: "http://192.168.1.100:1234", password: "example-password", webhookPath: "/bluebubbles-webhook", }, }, } ``` 4. Point BlueBubbles webhooks to your gateway (example: `https://your-gateway-host:3000/bluebubbles-webhook?password=`). 5. Start the gateway; it will register the webhook handler and start pairing. Security note: * Always set a webhook password. * Webhook authentication is always required. OpenClaw rejects BlueBubbles webhook requests unless they include a password/guid that matches `channels.bluebubbles.password` (for example `?password=` or `x-password`), regardless of loopback/proxy topology. * Password authentication is checked before reading/parsing full webhook bodies. ## Keeping Messages.app alive (VM / headless setups) Some macOS VM / always-on setups can end up with Messages.app going “idle” (incoming events stop until the app is opened/foregrounded). A simple workaround is to **poke Messages every 5 minutes** using an AppleScript + LaunchAgent. ### 1) Save the AppleScript Save this as: * `~/Scripts/poke-messages.scpt` Example script (non-interactive; does not steal focus): ```applescript theme={"theme":{"light":"min-light","dark":"min-dark"}} try tell application "Messages" if not running then launch end if -- Touch the scripting interface to keep the process responsive. set _chatCount to (count of chats) end tell on error -- Ignore transient failures (first-run prompts, locked session, etc). end try ``` ### 2) Install a LaunchAgent Save this as: * `~/Library/LaunchAgents/com.user.poke-messages.plist` ```xml theme={"theme":{"light":"min-light","dark":"min-dark"}} Label com.user.poke-messages ProgramArguments /bin/bash -lc /usr/bin/osascript "$HOME/Scripts/poke-messages.scpt" RunAtLoad StartInterval 300 StandardOutPath /tmp/poke-messages.log StandardErrorPath /tmp/poke-messages.err ``` Notes: * This runs **every 300 seconds** and **on login**. * The first run may trigger macOS **Automation** prompts (`osascript` → Messages). Approve them in the same user session that runs the LaunchAgent. Load it: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} launchctl unload ~/Library/LaunchAgents/com.user.poke-messages.plist 2>/dev/null || true launchctl load ~/Library/LaunchAgents/com.user.poke-messages.plist ``` ## Onboarding BlueBubbles is available in the interactive setup wizard: ``` openclaw onboard ``` The wizard prompts for: * **Server URL** (required): BlueBubbles server address (e.g., `http://192.168.1.100:1234`) * **Password** (required): API password from BlueBubbles Server settings * **Webhook path** (optional): Defaults to `/bluebubbles-webhook` * **DM policy**: pairing, allowlist, open, or disabled * **Allow list**: Phone numbers, emails, or chat targets You can also add BlueBubbles via CLI: ``` openclaw channels add bluebubbles --http-url http://192.168.1.100:1234 --password ``` ## Access control (DMs + groups) DMs: * Default: `channels.bluebubbles.dmPolicy = "pairing"`. * Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour). * Approve via: * `openclaw pairing list bluebubbles` * `openclaw pairing approve bluebubbles ` * Pairing is the default token exchange. Details: [Pairing](/channels/pairing) Groups: * `channels.bluebubbles.groupPolicy = open | allowlist | disabled` (default: `allowlist`). * `channels.bluebubbles.groupAllowFrom` controls who can trigger in groups when `allowlist` is set. ### Mention gating (groups) BlueBubbles supports mention gating for group chats, matching iMessage/WhatsApp behavior: * Uses `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) to detect mentions. * When `requireMention` is enabled for a group, the agent only responds when mentioned. * Control commands from authorized senders bypass mention gating. Per-group configuration: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { bluebubbles: { groupPolicy: "allowlist", groupAllowFrom: ["+15555550123"], groups: { "*": { requireMention: true }, // default for all groups "iMessage;-;chat123": { requireMention: false }, // override for specific group }, }, }, } ``` ### Command gating * Control commands (e.g., `/config`, `/model`) require authorization. * Uses `allowFrom` and `groupAllowFrom` to determine command authorization. * Authorized senders can run control commands even without mentioning in groups. ## Typing + read receipts * **Typing indicators**: Sent automatically before and during response generation. * **Read receipts**: Controlled by `channels.bluebubbles.sendReadReceipts` (default: `true`). * **Typing indicators**: OpenClaw sends typing start events; BlueBubbles clears typing automatically on send or timeout (manual stop via DELETE is unreliable). ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { bluebubbles: { sendReadReceipts: false, // disable read receipts }, }, } ``` ## Advanced actions BlueBubbles supports advanced message actions when enabled in config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { bluebubbles: { actions: { reactions: true, // tapbacks (default: true) edit: true, // edit sent messages (macOS 13+, broken on macOS 26 Tahoe) unsend: true, // unsend messages (macOS 13+) reply: true, // reply threading by message GUID sendWithEffect: true, // message effects (slam, loud, etc.) renameGroup: true, // rename group chats setGroupIcon: true, // set group chat icon/photo (flaky on macOS 26 Tahoe) addParticipant: true, // add participants to groups removeParticipant: true, // remove participants from groups leaveGroup: true, // leave group chats sendAttachment: true, // send attachments/media }, }, }, } ``` Available actions: * **react**: Add/remove tapback reactions (`messageId`, `emoji`, `remove`) * **edit**: Edit a sent message (`messageId`, `text`) * **unsend**: Unsend a message (`messageId`) * **reply**: Reply to a specific message (`messageId`, `text`, `to`) * **sendWithEffect**: Send with iMessage effect (`text`, `to`, `effectId`) * **renameGroup**: Rename a group chat (`chatGuid`, `displayName`) * **setGroupIcon**: Set a group chat's icon/photo (`chatGuid`, `media`) — flaky on macOS 26 Tahoe (API may return success but the icon does not sync). * **addParticipant**: Add someone to a group (`chatGuid`, `address`) * **removeParticipant**: Remove someone from a group (`chatGuid`, `address`) * **leaveGroup**: Leave a group chat (`chatGuid`) * **sendAttachment**: Send media/files (`to`, `buffer`, `filename`, `asVoice`) * Voice memos: set `asVoice: true` with **MP3** or **CAF** audio to send as an iMessage voice message. BlueBubbles converts MP3 → CAF when sending voice memos. ### Message IDs (short vs full) OpenClaw may surface *short* message IDs (e.g., `1`, `2`) to save tokens. * `MessageSid` / `ReplyToId` can be short IDs. * `MessageSidFull` / `ReplyToIdFull` contain the provider full IDs. * Short IDs are in-memory; they can expire on restart or cache eviction. * Actions accept short or full `messageId`, but short IDs will error if no longer available. Use full IDs for durable automations and storage: * Templates: `{{MessageSidFull}}`, `{{ReplyToIdFull}}` * Context: `MessageSidFull` / `ReplyToIdFull` in inbound payloads See [Configuration](/gateway/configuration) for template variables. ## Block streaming Control whether responses are sent as a single message or streamed in blocks: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { bluebubbles: { blockStreaming: true, // enable block streaming (off by default) }, }, } ``` ## Media + limits * Inbound attachments are downloaded and stored in the media cache. * Media cap via `channels.bluebubbles.mediaMaxMb` (default: 8 MB). * Outbound text is chunked to `channels.bluebubbles.textChunkLimit` (default: 4000 chars). ## Configuration reference Full configuration: [Configuration](/gateway/configuration) Provider options: * `channels.bluebubbles.enabled`: Enable/disable the channel. * `channels.bluebubbles.serverUrl`: BlueBubbles REST API base URL. * `channels.bluebubbles.password`: API password. * `channels.bluebubbles.webhookPath`: Webhook endpoint path (default: `/bluebubbles-webhook`). * `channels.bluebubbles.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`). * `channels.bluebubbles.allowFrom`: DM allowlist (handles, emails, E.164 numbers, `chat_id:*`, `chat_guid:*`). * `channels.bluebubbles.groupPolicy`: `open | allowlist | disabled` (default: `allowlist`). * `channels.bluebubbles.groupAllowFrom`: Group sender allowlist. * `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.). * `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`). * `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `false`; required for streaming replies). * `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000). * `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking. * `channels.bluebubbles.mediaMaxMb`: Inbound media cap in MB (default: 8). * `channels.bluebubbles.mediaLocalRoots`: Explicit allowlist of absolute local directories permitted for outbound local media paths. Local path sends are denied by default unless this is configured. Per-account override: `channels.bluebubbles.accounts..mediaLocalRoots`. * `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables). * `channels.bluebubbles.dmHistoryLimit`: DM history limit. * `channels.bluebubbles.actions`: Enable/disable specific actions. * `channels.bluebubbles.accounts`: Multi-account configuration. Related global options: * `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`). * `messages.responsePrefix`. ## Addressing / delivery targets Prefer `chat_guid` for stable routing: * `chat_guid:iMessage;-;+15555550123` (preferred for groups) * `chat_id:123` * `chat_identifier:...` * Direct handles: `+15555550123`, `user@example.com` * If a direct handle does not have an existing DM chat, OpenClaw will create one via `POST /api/v1/chat/new`. This requires the BlueBubbles Private API to be enabled. ## Security * Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted. * Keep the API password and webhook endpoint secret (treat them like credentials). * Localhost trust means a same-host reverse proxy can unintentionally bypass the password. If you proxy the gateway, require auth at the proxy and configure `gateway.trustedProxies`. See [Gateway security](/gateway/security#reverse-proxy-configuration). * Enable HTTPS + firewall rules on the BlueBubbles server if exposing it outside your LAN. ## Troubleshooting * If typing/read events stop working, check the BlueBubbles webhook logs and verify the gateway path matches `channels.bluebubbles.webhookPath`. * Pairing codes expire after one hour; use `openclaw pairing list bluebubbles` and `openclaw pairing approve bluebubbles `. * Reactions require the BlueBubbles private API (`POST /api/v1/message/react`); ensure the server version exposes it. * Edit/unsend require macOS 13+ and a compatible BlueBubbles server version. On macOS 26 (Tahoe), edit is currently broken due to private API changes. * Group icon updates can be flaky on macOS 26 (Tahoe): the API may return success but the new icon does not sync. * OpenClaw auto-hides known-broken actions based on the BlueBubbles server's macOS version. If edit still appears on macOS 26 (Tahoe), disable it manually with `channels.bluebubbles.actions.edit=false`. * For status/health info: `openclaw status --all` or `openclaw status --deep`. For general channel workflow reference, see [Channels](/channels) and the [Plugins](/tools/plugin) guide. # Broadcast Groups Source: https://cryptoclawdocs.termix.ai/channels/broadcast-groups # Broadcast Groups **Status:** Experimental\ **Version:** Added in 2026.1.9 ## Overview Broadcast Groups enable multiple agents to process and respond to the same message simultaneously. This allows you to create specialized agent teams that work together in a single WhatsApp group or DM — all using one phone number. Current scope: **WhatsApp only** (web channel). Broadcast groups are evaluated after channel allowlists and group activation rules. In WhatsApp groups, this means broadcasts happen when OpenClaw would normally reply (for example: on mention, depending on your group settings). ## Use Cases ### 1. Specialized Agent Teams Deploy multiple agents with atomic, focused responsibilities: ``` Group: "Development Team" Agents: - CodeReviewer (reviews code snippets) - DocumentationBot (generates docs) - SecurityAuditor (checks for vulnerabilities) - TestGenerator (suggests test cases) ``` Each agent processes the same message and provides its specialized perspective. ### 2. Multi-Language Support ``` Group: "International Support" Agents: - Agent_EN (responds in English) - Agent_DE (responds in German) - Agent_ES (responds in Spanish) ``` ### 3. Quality Assurance Workflows ``` Group: "Customer Support" Agents: - SupportAgent (provides answer) - QAAgent (reviews quality, only responds if issues found) ``` ### 4. Task Automation ``` Group: "Project Management" Agents: - TaskTracker (updates task database) - TimeLogger (logs time spent) - ReportGenerator (creates summaries) ``` ## Configuration ### Basic Setup Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer ids: * group chats: group JID (e.g. `120363403215116621@g.us`) * DMs: E.164 phone number (e.g. `+15551234567`) ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "broadcast": { "120363403215116621@g.us": ["alfred", "baerbel", "assistant3"] } } ``` **Result:** When OpenClaw would reply in this chat, it will run all three agents. ### Processing Strategy Control how agents process messages: #### Parallel (Default) All agents process simultaneously: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "broadcast": { "strategy": "parallel", "120363403215116621@g.us": ["alfred", "baerbel"] } } ``` #### Sequential Agents process in order (one waits for previous to finish): ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "broadcast": { "strategy": "sequential", "120363403215116621@g.us": ["alfred", "baerbel"] } } ``` ### Complete Example ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "agents": { "list": [ { "id": "code-reviewer", "name": "Code Reviewer", "workspace": "/path/to/code-reviewer", "sandbox": { "mode": "all" } }, { "id": "security-auditor", "name": "Security Auditor", "workspace": "/path/to/security-auditor", "sandbox": { "mode": "all" } }, { "id": "docs-generator", "name": "Documentation Generator", "workspace": "/path/to/docs-generator", "sandbox": { "mode": "all" } } ] }, "broadcast": { "strategy": "parallel", "120363403215116621@g.us": ["code-reviewer", "security-auditor", "docs-generator"], "120363424282127706@g.us": ["support-en", "support-de"], "+15555550123": ["assistant", "logger"] } } ``` ## How It Works ### Message Flow 1. **Incoming message** arrives in a WhatsApp group 2. **Broadcast check**: System checks if peer ID is in `broadcast` 3. **If in broadcast list**: * All listed agents process the message * Each agent has its own session key and isolated context * Agents process in parallel (default) or sequentially 4. **If not in broadcast list**: * Normal routing applies (first matching binding) Note: broadcast groups do not bypass channel allowlists or group activation rules (mentions/commands/etc). They only change *which agents run* when a message is eligible for processing. ### Session Isolation Each agent in a broadcast group maintains completely separate: * **Session keys** (`agent:alfred:whatsapp:group:120363...` vs `agent:baerbel:whatsapp:group:120363...`) * **Conversation history** (agent doesn't see other agents' messages) * **Workspace** (separate sandboxes if configured) * **Tool access** (different allow/deny lists) * **Memory/context** (separate IDENTITY.md, SOUL.md, etc.) * **Group context buffer** (recent group messages used for context) is shared per peer, so all broadcast agents see the same context when triggered This allows each agent to have: * Different personalities * Different tool access (e.g., read-only vs. read-write) * Different models (e.g., opus vs. sonnet) * Different skills installed ### Example: Isolated Sessions In group `120363403215116621@g.us` with agents `["alfred", "baerbel"]`: **Alfred's context:** ``` Session: agent:alfred:whatsapp:group:120363403215116621@g.us History: [user message, alfred's previous responses] Workspace: /Users/pascal/openclaw-alfred/ Tools: read, write, exec ``` **Bärbel's context:** ``` Session: agent:baerbel:whatsapp:group:120363403215116621@g.us History: [user message, baerbel's previous responses] Workspace: /Users/pascal/openclaw-baerbel/ Tools: read only ``` ## Best Practices ### 1. Keep Agents Focused Design each agent with a single, clear responsibility: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "broadcast": { "DEV_GROUP": ["formatter", "linter", "tester"] } } ``` ✅ **Good:** Each agent has one job\ ❌ **Bad:** One generic "dev-helper" agent ### 2. Use Descriptive Names Make it clear what each agent does: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "agents": { "security-scanner": { "name": "Security Scanner" }, "code-formatter": { "name": "Code Formatter" }, "test-generator": { "name": "Test Generator" } } } ``` ### 3. Configure Different Tool Access Give agents only the tools they need: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "agents": { "reviewer": { "tools": { "allow": ["read", "exec"] } // Read-only }, "fixer": { "tools": { "allow": ["read", "write", "edit", "exec"] } // Read-write } } } ``` ### 4. Monitor Performance With many agents, consider: * Using `"strategy": "parallel"` (default) for speed * Limiting broadcast groups to 5-10 agents * Using faster models for simpler agents ### 5. Handle Failures Gracefully Agents fail independently. One agent's error doesn't block others: ``` Message → [Agent A ✓, Agent B ✗ error, Agent C ✓] Result: Agent A and C respond, Agent B logs error ``` ## Compatibility ### Providers Broadcast groups currently work with: * ✅ WhatsApp (implemented) * 🚧 Telegram (planned) * 🚧 Discord (planned) * 🚧 Slack (planned) ### Routing Broadcast groups work alongside existing routing: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "bindings": [ { "match": { "channel": "whatsapp", "peer": { "kind": "group", "id": "GROUP_A" } }, "agentId": "alfred" } ], "broadcast": { "GROUP_B": ["agent1", "agent2"] } } ``` * `GROUP_A`: Only alfred responds (normal routing) * `GROUP_B`: agent1 AND agent2 respond (broadcast) **Precedence:** `broadcast` takes priority over `bindings`. ## Troubleshooting ### Agents Not Responding **Check:** 1. Agent IDs exist in `agents.list` 2. Peer ID format is correct (e.g., `120363403215116621@g.us`) 3. Agents are not in deny lists **Debug:** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} tail -f ~/.openclaw/logs/gateway.log | grep broadcast ``` ### Only One Agent Responding **Cause:** Peer ID might be in `bindings` but not `broadcast`. **Fix:** Add to broadcast config or remove from bindings. ### Performance Issues **If slow with many agents:** * Reduce number of agents per group * Use lighter models (sonnet instead of opus) * Check sandbox startup time ## Examples ### Example 1: Code Review Team ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "broadcast": { "strategy": "parallel", "120363403215116621@g.us": [ "code-formatter", "security-scanner", "test-coverage", "docs-checker" ] }, "agents": { "list": [ { "id": "code-formatter", "workspace": "~/agents/formatter", "tools": { "allow": ["read", "write"] } }, { "id": "security-scanner", "workspace": "~/agents/security", "tools": { "allow": ["read", "exec"] } }, { "id": "test-coverage", "workspace": "~/agents/testing", "tools": { "allow": ["read", "exec"] } }, { "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } } ] } } ``` **User sends:** Code snippet\ **Responses:** * code-formatter: "Fixed indentation and added type hints" * security-scanner: "⚠️ SQL injection vulnerability in line 12" * test-coverage: "Coverage is 45%, missing tests for error cases" * docs-checker: "Missing docstring for function `process_data`" ### Example 2: Multi-Language Support ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "broadcast": { "strategy": "sequential", "+15555550123": ["detect-language", "translator-en", "translator-de"] }, "agents": { "list": [ { "id": "detect-language", "workspace": "~/agents/lang-detect" }, { "id": "translator-en", "workspace": "~/agents/translate-en" }, { "id": "translator-de", "workspace": "~/agents/translate-de" } ] } } ``` ## API Reference ### Config Schema ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} interface OpenClawConfig { broadcast?: { strategy?: "parallel" | "sequential"; [peerId: string]: string[]; }; } ``` ### Fields * `strategy` (optional): How to process agents * `"parallel"` (default): All agents process simultaneously * `"sequential"`: Agents process in array order * `[peerId]`: WhatsApp group JID, E.164 number, or other peer ID * Value: Array of agent IDs that should process messages ## Limitations 1. **Max agents:** No hard limit, but 10+ agents may be slow 2. **Shared context:** Agents don't see each other's responses (by design) 3. **Message ordering:** Parallel responses may arrive in any order 4. **Rate limits:** All agents count toward WhatsApp rate limits ## Future Enhancements Planned features: * [ ] Shared context mode (agents see each other's responses) * [ ] Agent coordination (agents can signal each other) * [ ] Dynamic agent selection (choose agents based on message content) * [ ] Agent priorities (some agents respond before others) ## See Also * [Multi-Agent Configuration](/tools/multi-agent-sandbox-tools) * [Routing Configuration](/channels/channel-routing) * [Session Management](/concepts/session) # Channel Routing Source: https://cryptoclawdocs.termix.ai/channels/channel-routing # Channels & routing OpenClaw routes replies **back to the channel where a message came from**. The model does not choose a channel; routing is deterministic and controlled by the host configuration. ## Key terms * **Channel**: `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `webchat`. * **AccountId**: per‑channel account instance (when supported). * Optional channel default account: `channels..defaultAccount` chooses which account is used when an outbound path does not specify `accountId`. * In multi-account setups, set an explicit default (`defaultAccount` or `accounts.default`) when two or more accounts are configured. Without it, fallback routing may pick the first normalized account ID. * **AgentId**: an isolated workspace + session store (“brain”). * **SessionKey**: the bucket key used to store context and control concurrency. ## Session key shapes (examples) Direct messages collapse to the agent’s **main** session: * `agent::` (default: `agent:main:main`) Groups and channels remain isolated per channel: * Groups: `agent:::group:` * Channels/rooms: `agent:::channel:` Threads: * Slack/Discord threads append `:thread:` to the base key. * Telegram forum topics embed `:topic:` in the group key. Examples: * `agent:main:telegram:group:-1001234567890:topic:42` * `agent:main:discord:channel:123456:thread:987654` ## Main DM route pinning When `session.dmScope` is `main`, direct messages may share one main session. To prevent the session’s `lastRoute` from being overwritten by non-owner DMs, OpenClaw infers a pinned owner from `allowFrom` when all of these are true: * `allowFrom` has exactly one non-wildcard entry. * The entry can be normalized to a concrete sender ID for that channel. * The inbound DM sender does not match that pinned owner. In that mismatch case, OpenClaw still records inbound session metadata, but it skips updating the main session `lastRoute`. ## Routing rules (how an agent is chosen) Routing picks **one agent** for each inbound message: 1. **Exact peer match** (`bindings` with `peer.kind` + `peer.id`). 2. **Parent peer match** (thread inheritance). 3. **Guild + roles match** (Discord) via `guildId` + `roles`. 4. **Guild match** (Discord) via `guildId`. 5. **Team match** (Slack) via `teamId`. 6. **Account match** (`accountId` on the channel). 7. **Channel match** (any account on that channel, `accountId: "*"`). 8. **Default agent** (`agents.list[].default`, else first list entry, fallback to `main`). When a binding includes multiple match fields (`peer`, `guildId`, `teamId`, `roles`), **all provided fields must match** for that binding to apply. The matched agent determines which workspace and session store are used. ## Broadcast groups (run multiple agents) Broadcast groups let you run **multiple agents** for the same peer **when OpenClaw would normally reply** (for example: in WhatsApp groups, after mention/activation gating). Config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { broadcast: { strategy: "parallel", "120363403215116621@g.us": ["alfred", "baerbel"], "+15555550123": ["support", "logger"], }, } ``` See: [Broadcast Groups](/channels/broadcast-groups). ## Config overview * `agents.list`: named agent definitions (workspace, model, etc.). * `bindings`: map inbound channels/accounts/peers to agents. Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { list: [{ id: "support", name: "Support", workspace: "~/.openclaw/workspace-support" }], }, bindings: [ { match: { channel: "slack", teamId: "T123" }, agentId: "support" }, { match: { channel: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" }, ], } ``` ## Session storage Session stores live under the state directory (default `~/.openclaw`): * `~/.openclaw/agents//sessions/sessions.json` * JSONL transcripts live alongside the store You can override the store path via `session.store` and `{agentId}` templating. ## WebChat behavior WebChat attaches to the **selected agent** and defaults to the agent’s main session. Because of this, WebChat lets you see cross‑channel context for that agent in one place. ## Reply context Inbound replies include: * `ReplyToId`, `ReplyToBody`, and `ReplyToSender` when available. * Quoted context is appended to `Body` as a `[Replying to ...]` block. This is consistent across channels. # Discord Source: https://cryptoclawdocs.termix.ai/channels/discord # Discord (Bot API) Status: ready for DMs and guild channels via the official Discord gateway. Discord DMs default to pairing mode. Native command behavior and command catalog. Cross-channel diagnostics and repair flow. ## Quick setup You will need to create a new application with a bot, add the bot to your server, and pair it to OpenClaw. We recommend adding your bot to your own private server. If you don't have one yet, [create one first](https://support.discord.com/hc/en-us/articles/204849977-How-do-I-create-a-server) (choose **Create My Own > For me and my friends**). Go to the [Discord Developer Portal](https://discord.com/developers/applications) and click **New Application**. Name it something like "OpenClaw". Click **Bot** on the sidebar. Set the **Username** to whatever you call your OpenClaw agent. Still on the **Bot** page, scroll down to **Privileged Gateway Intents** and enable: * **Message Content Intent** (required) * **Server Members Intent** (recommended; required for role allowlists and name-to-ID matching) * **Presence Intent** (optional; only needed for presence updates) Scroll back up on the **Bot** page and click **Reset Token**. Despite the name, this generates your first token — nothing is being "reset." Copy the token and save it somewhere. This is your **Bot Token** and you will need it shortly. Click **OAuth2** on the sidebar. You'll generate an invite URL with the right permissions to add the bot to your server. Scroll down to **OAuth2 URL Generator** and enable: * `bot` * `applications.commands` A **Bot Permissions** section will appear below. Enable: * View Channels * Send Messages * Read Message History * Embed Links * Attach Files * Add Reactions (optional) Copy the generated URL at the bottom, paste it into your browser, select your server, and click **Continue** to connect. You should now see your bot in the Discord server. Back in the Discord app, you need to enable Developer Mode so you can copy internal IDs. 1. Click **User Settings** (gear icon next to your avatar) → **Advanced** → toggle on **Developer Mode** 2. Right-click your **server icon** in the sidebar → **Copy Server ID** 3. Right-click your **own avatar** → **Copy User ID** Save your **Server ID** and **User ID** alongside your Bot Token — you'll send all three to OpenClaw in the next step. For pairing to work, Discord needs to allow your bot to DM you. Right-click your **server icon** → **Privacy Settings** → toggle on **Direct Messages**. This lets server members (including bots) send you DMs. Keep this enabled if you want to use Discord DMs with OpenClaw. If you only plan to use guild channels, you can disable DMs after pairing. Your Discord bot token is a secret (like a password). Set it on the machine running OpenClaw before messaging your agent. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw config set channels.discord.token '"YOUR_BOT_TOKEN"' --json openclaw config set channels.discord.enabled true --json openclaw gateway ``` If OpenClaw is already running as a background service, use `openclaw gateway restart` instead. Chat with your OpenClaw agent on any existing channel (e.g. Telegram) and tell it. If Discord is your first channel, use the CLI / config tab instead. > "I already set my Discord bot token in config. Please finish Discord setup with User ID `` and Server ID ``." If you prefer file-based config, set: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { enabled: true, token: "YOUR_BOT_TOKEN", }, }, } ``` Env fallback for the default account: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} DISCORD_BOT_TOKEN=... ``` SecretRef values are also supported for `channels.discord.token` (env/file/exec providers). See [Secrets Management](/gateway/secrets). Wait until the gateway is running, then DM your bot in Discord. It will respond with a pairing code. Send the pairing code to your agent on your existing channel: > "Approve this Discord pairing code: ``" ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw pairing list discord openclaw pairing approve discord ``` Pairing codes expire after 1 hour. You should now be able to chat with your agent in Discord via DM. Token resolution is account-aware. Config token values win over env fallback. `DISCORD_BOT_TOKEN` is only used for the default account. ## Recommended: Set up a guild workspace Once DMs are working, you can set up your Discord server as a full workspace where each channel gets its own agent session with its own context. This is recommended for private servers where it's just you and your bot. This enables your agent to respond in any channel on your server, not just DMs. > "Add my Discord Server ID `` to the guild allowlist" ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { groupPolicy: "allowlist", guilds: { YOUR_SERVER_ID: { requireMention: true, users: ["YOUR_USER_ID"], }, }, }, }, } ``` By default, your agent only responds in guild channels when @mentioned. For a private server, you probably want it to respond to every message. > "Allow my agent to respond on this server without having to be @mentioned" Set `requireMention: false` in your guild config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { guilds: { YOUR_SERVER_ID: { requireMention: false, }, }, }, }, } ``` By default, long-term memory (MEMORY.md) only loads in DM sessions. Guild channels do not auto-load MEMORY.md. > "When I ask questions in Discord channels, use memory\_search or memory\_get if you need long-term context from MEMORY.md." If you need shared context in every channel, put the stable instructions in `AGENTS.md` or `USER.md` (they are injected for every session). Keep long-term notes in `MEMORY.md` and access them on demand with memory tools. Now create some channels on your Discord server and start chatting. Your agent can see the channel name, and each channel gets its own isolated session — so you can set up `#coding`, `#home`, `#research`, or whatever fits your workflow. ## Runtime model * Gateway owns the Discord connection. * Reply routing is deterministic: Discord inbound replies back to Discord. * By default (`session.dmScope=main`), direct chats share the agent main session (`agent:main:main`). * Guild channels are isolated session keys (`agent::discord:channel:`). * Group DMs are ignored by default (`channels.discord.dm.groupEnabled=false`). * Native slash commands run in isolated command sessions (`agent::discord:slash:`), while still carrying `CommandTargetSessionKey` to the routed conversation session. ## Forum channels Discord forum and media channels only accept thread posts. OpenClaw supports two ways to create them: * Send a message to the forum parent (`channel:`) to auto-create a thread. The thread title uses the first non-empty line of your message. * Use `openclaw message thread create` to create a thread directly. Do not pass `--message-id` for forum channels. Example: send to forum parent to create a thread ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw message send --channel discord --target channel: \ --message "Topic title\nBody of the post" ``` Example: create a forum thread explicitly ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw message thread create --channel discord --target channel: \ --thread-name "Topic title" --message "Body of the post" ``` Forum parents do not accept Discord components. If you need components, send to the thread itself (`channel:`). ## Interactive components OpenClaw supports Discord components v2 containers for agent messages. Use the message tool with a `components` payload. Interaction results are routed back to the agent as normal inbound messages and follow the existing Discord `replyToMode` settings. Supported blocks: * `text`, `section`, `separator`, `actions`, `media-gallery`, `file` * Action rows allow up to 5 buttons or a single select menu * Select types: `string`, `user`, `role`, `mentionable`, `channel` By default, components are single use. Set `components.reusable=true` to allow buttons, selects, and forms to be used multiple times until they expire. To restrict who can click a button, set `allowedUsers` on that button (Discord user IDs, tags, or `*`). When configured, unmatched users receive an ephemeral denial. The `/model` and `/models` slash commands open an interactive model picker with provider and model dropdowns plus a Submit step. The picker reply is ephemeral and only the invoking user can use it. File attachments: * `file` blocks must point to an attachment reference (`attachment://`) * Provide the attachment via `media`/`path`/`filePath` (single file); use `media-gallery` for multiple files * Use `filename` to override the upload name when it should match the attachment reference Modal forms: * Add `components.modal` with up to 5 fields * Field types: `text`, `checkbox`, `radio`, `select`, `role-select`, `user-select` * OpenClaw adds a trigger button automatically Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channel: "discord", action: "send", to: "channel:123456789012345678", message: "Optional fallback text", components: { reusable: true, text: "Choose a path", blocks: [ { type: "actions", buttons: [ { label: "Approve", style: "success", allowedUsers: ["123456789012345678"], }, { label: "Decline", style: "danger" }, ], }, { type: "actions", select: { type: "string", placeholder: "Pick an option", options: [ { label: "Option A", value: "a" }, { label: "Option B", value: "b" }, ], }, }, ], modal: { title: "Details", triggerLabel: "Open form", fields: [ { type: "text", label: "Requester" }, { type: "select", label: "Priority", options: [ { label: "Low", value: "low" }, { label: "High", value: "high" }, ], }, ], }, }, } ``` ## Access control and routing `channels.discord.dmPolicy` controls DM access (legacy: `channels.discord.dm.policy`): * `pairing` (default) * `allowlist` * `open` (requires `channels.discord.allowFrom` to include `"*"`; legacy: `channels.discord.dm.allowFrom`) * `disabled` If DM policy is not open, unknown users are blocked (or prompted for pairing in `pairing` mode). Multi-account precedence: * `channels.discord.accounts.default.allowFrom` applies only to the `default` account. * Named accounts inherit `channels.discord.allowFrom` when their own `allowFrom` is unset. * Named accounts do not inherit `channels.discord.accounts.default.allowFrom`. DM target format for delivery: * `user:` * `<@id>` mention Bare numeric IDs are ambiguous and rejected unless an explicit user/channel target kind is provided. Guild handling is controlled by `channels.discord.groupPolicy`: * `open` * `allowlist` * `disabled` Secure baseline when `channels.discord` exists is `allowlist`. `allowlist` behavior: * guild must match `channels.discord.guilds` (`id` preferred, slug accepted) * optional sender allowlists: `users` (stable IDs recommended) and `roles` (role IDs only); if either is configured, senders are allowed when they match `users` OR `roles` * direct name/tag matching is disabled by default; enable `channels.discord.dangerouslyAllowNameMatching: true` only as break-glass compatibility mode * names/tags are supported for `users`, but IDs are safer; `openclaw security audit` warns when name/tag entries are used * if a guild has `channels` configured, non-listed channels are denied * if a guild has no `channels` block, all channels in that allowlisted guild are allowed Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { groupPolicy: "allowlist", guilds: { "123456789012345678": { requireMention: true, ignoreOtherMentions: true, users: ["987654321098765432"], roles: ["123456789012345678"], channels: { general: { allow: true }, help: { allow: true, requireMention: true }, }, }, }, }, }, } ``` If you only set `DISCORD_BOT_TOKEN` and do not create a `channels.discord` block, runtime fallback is `groupPolicy="allowlist"` (with a warning in logs), even if `channels.defaults.groupPolicy` is `open`. Guild messages are mention-gated by default. Mention detection includes: * explicit bot mention * configured mention patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`) * implicit reply-to-bot behavior in supported cases `requireMention` is configured per guild/channel (`channels.discord.guilds...`). `ignoreOtherMentions` optionally drops messages that mention another user/role but not the bot (excluding @everyone/@here). Group DMs: * default: ignored (`dm.groupEnabled=false`) * optional allowlist via `dm.groupChannels` (channel IDs or slugs) ### Role-based agent routing Use `bindings[].match.roles` to route Discord guild members to different agents by role ID. Role-based bindings accept role IDs only and are evaluated after peer or parent-peer bindings and before guild-only bindings. If a binding also sets other match fields (for example `peer` + `guildId` + `roles`), all configured fields must match. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { bindings: [ { agentId: "opus", match: { channel: "discord", guildId: "123456789012345678", roles: ["111111111111111111"], }, }, { agentId: "sonnet", match: { channel: "discord", guildId: "123456789012345678", }, }, ], } ``` ## Developer Portal setup 1. Discord Developer Portal -> **Applications** -> **New Application** 2. **Bot** -> **Add Bot** 3. Copy bot token In **Bot -> Privileged Gateway Intents**, enable: * Message Content Intent * Server Members Intent (recommended) Presence intent is optional and only required if you want to receive presence updates. Setting bot presence (`setPresence`) does not require enabling presence updates for members. OAuth URL generator: * scopes: `bot`, `applications.commands` Typical baseline permissions: * View Channels * Send Messages * Read Message History * Embed Links * Attach Files * Add Reactions (optional) Avoid `Administrator` unless explicitly needed. Enable Discord Developer Mode, then copy: * server ID * channel ID * user ID Prefer numeric IDs in OpenClaw config for reliable audits and probes. ## Native commands and command auth * `commands.native` defaults to `"auto"` and is enabled for Discord. * Per-channel override: `channels.discord.commands.native`. * `commands.native=false` explicitly clears previously registered Discord native commands. * Native command auth uses the same Discord allowlists/policies as normal message handling. * Commands may still be visible in Discord UI for users who are not authorized; execution still enforces OpenClaw auth and returns "not authorized". See [Slash commands](/tools/slash-commands) for command catalog and behavior. Default slash command settings: * `ephemeral: true` ## Feature details Discord supports reply tags in agent output: * `[[reply_to_current]]` * `[[reply_to:]]` Controlled by `channels.discord.replyToMode`: * `off` (default) * `first` * `all` Note: `off` disables implicit reply threading. Explicit `[[reply_to_*]]` tags are still honored. Message IDs are surfaced in context/history so agents can target specific messages. OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. * `channels.discord.streaming` controls preview streaming (`off` | `partial` | `block` | `progress`, default: `off`). * `progress` is accepted for cross-channel consistency and maps to `partial` on Discord. * `channels.discord.streamMode` is a legacy alias and is auto-migrated. * `partial` edits a single preview message as tokens arrive. * `block` emits draft-sized chunks (use `draftChunk` to tune size and breakpoints). Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { streaming: "partial", }, }, } ``` `block` mode chunking defaults (clamped to `channels.discord.textChunkLimit`): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { streaming: "block", draftChunk: { minChars: 200, maxChars: 800, breakPreference: "paragraph", }, }, }, } ``` Preview streaming is text-only; media replies fall back to normal delivery. Note: preview streaming is separate from block streaming. When block streaming is explicitly enabled for Discord, OpenClaw skips the preview stream to avoid double streaming. Guild history context: * `channels.discord.historyLimit` default `20` * fallback: `messages.groupChat.historyLimit` * `0` disables DM history controls: * `channels.discord.dmHistoryLimit` * `channels.discord.dms[""].historyLimit` Thread behavior: * Discord threads are routed as channel sessions * parent thread metadata can be used for parent-session linkage * thread config inherits parent channel config unless a thread-specific entry exists Channel topics are injected as **untrusted** context (not as system prompt). Discord can bind a thread to a session target so follow-up messages in that thread keep routing to the same session (including subagent sessions). Commands: * `/focus ` bind current/new thread to a subagent/session target * `/unfocus` remove current thread binding * `/agents` show active runs and binding state * `/session idle ` inspect/update inactivity auto-unfocus for focused bindings * `/session max-age ` inspect/update hard max age for focused bindings Config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { session: { threadBindings: { enabled: true, idleHours: 24, maxAgeHours: 0, }, }, channels: { discord: { threadBindings: { enabled: true, idleHours: 24, maxAgeHours: 0, spawnSubagentSessions: false, // opt-in }, }, }, } ``` Notes: * `session.threadBindings.*` sets global defaults. * `channels.discord.threadBindings.*` overrides Discord behavior. * `spawnSubagentSessions` must be true to auto-create/bind threads for `sessions_spawn({ thread: true })`. * `spawnAcpSessions` must be true to auto-create/bind threads for ACP (`/acp spawn ... --thread ...` or `sessions_spawn({ runtime: "acp", thread: true })`). * If thread bindings are disabled for an account, `/focus` and related thread binding operations are unavailable. See [Sub-agents](/tools/subagents), [ACP Agents](/tools/acp-agents), and [Configuration Reference](/gateway/configuration-reference). Per-guild reaction notification mode: * `off` * `own` (default) * `all` * `allowlist` (uses `guilds..users`) Reaction events are turned into system events and attached to the routed Discord session. `ackReaction` sends an acknowledgement emoji while OpenClaw is processing an inbound message. Resolution order: * `channels.discord.accounts..ackReaction` * `channels.discord.ackReaction` * `messages.ackReaction` * agent identity emoji fallback (`agents.list[].identity.emoji`, else "👀") Notes: * Discord accepts unicode emoji or custom emoji names. * Use `""` to disable the reaction for a channel or account. Channel-initiated config writes are enabled by default. This affects `/config set|unset` flows (when command features are enabled). Disable: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { configWrites: false, }, }, } ``` Route Discord gateway WebSocket traffic and startup REST lookups (application ID + allowlist resolution) through an HTTP(S) proxy with `channels.discord.proxy`. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { proxy: "http://proxy.example:8080", }, }, } ``` Per-account override: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { accounts: { primary: { proxy: "http://proxy.example:8080", }, }, }, }, } ``` Enable PluralKit resolution to map proxied messages to system member identity: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { pluralkit: { enabled: true, token: "pk_live_...", // optional; needed for private systems }, }, }, } ``` Notes: * allowlists can use `pk:` * member display names are matched by name/slug only when `channels.discord.dangerouslyAllowNameMatching: true` * lookups use original message ID and are time-window constrained * if lookup fails, proxied messages are treated as bot messages and dropped unless `allowBots=true` Presence updates are applied when you set a status or activity field, or when you enable auto presence. Status only example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { status: "idle", }, }, } ``` Activity example (custom status is the default activity type): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { activity: "Focus time", activityType: 4, }, }, } ``` Streaming example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { activity: "Live coding", activityType: 1, activityUrl: "https://twitch.tv/openclaw", }, }, } ``` Activity type map: * 0: Playing * 1: Streaming (requires `activityUrl`) * 2: Listening * 3: Watching * 4: Custom (uses the activity text as the status state; emoji is optional) * 5: Competing Auto presence example (runtime health signal): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { autoPresence: { enabled: true, intervalMs: 30000, minUpdateIntervalMs: 15000, exhaustedText: "token exhausted", }, }, }, } ``` Auto presence maps runtime availability to Discord status: healthy => online, degraded or unknown => idle, exhausted or unavailable => dnd. Optional text overrides: * `autoPresence.healthyText` * `autoPresence.degradedText` * `autoPresence.exhaustedText` (supports `{reason}` placeholder) Discord supports button-based exec approvals in DMs and can optionally post approval prompts in the originating channel. Config path: * `channels.discord.execApprovals.enabled` * `channels.discord.execApprovals.approvers` * `channels.discord.execApprovals.target` (`dm` | `channel` | `both`, default: `dm`) * `agentFilter`, `sessionFilter`, `cleanupAfterResolve` When `target` is `channel` or `both`, the approval prompt is visible in the channel. Only configured approvers can use the buttons; other users receive an ephemeral denial. Approval prompts include the command text, so only enable channel delivery in trusted channels. If the channel ID cannot be derived from the session key, OpenClaw falls back to DM delivery. If approvals fail with unknown approval IDs, verify approver list and feature enablement. Related docs: [Exec approvals](/tools/exec-approvals) ## Tools and action gates Discord message actions include messaging, channel admin, moderation, presence, and metadata actions. Core examples: * messaging: `sendMessage`, `readMessages`, `editMessage`, `deleteMessage`, `threadReply` * reactions: `react`, `reactions`, `emojiList` * moderation: `timeout`, `kick`, `ban` * presence: `setPresence` Action gates live under `channels.discord.actions.*`. Default gate behavior: | Action group | Default | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | | reactions, messages, threads, pins, polls, search, memberInfo, roleInfo, channelInfo, channels, voiceStatus, events, stickers, emojiUploads, stickerUploads, permissions | enabled | | roles | disabled | | moderation | disabled | | presence | disabled | ## Components v2 UI OpenClaw uses Discord components v2 for exec approvals and cross-context markers. Discord message actions can also accept `components` for custom UI (advanced; requires Carbon component instances), while legacy `embeds` remain available but are not recommended. * `channels.discord.ui.components.accentColor` sets the accent color used by Discord component containers (hex). * Set per account with `channels.discord.accounts..ui.components.accentColor`. * `embeds` are ignored when components v2 are present. Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { ui: { components: { accentColor: "#5865F2", }, }, }, }, } ``` ## Voice channels OpenClaw can join Discord voice channels for realtime, continuous conversations. This is separate from voice message attachments. Requirements: * Enable native commands (`commands.native` or `channels.discord.commands.native`). * Configure `channels.discord.voice`. * The bot needs Connect + Speak permissions in the target voice channel. Use the Discord-only native command `/vc join|leave|status` to control sessions. The command uses the account default agent and follows the same allowlist and group policy rules as other Discord commands. Auto-join example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { voice: { enabled: true, autoJoin: [ { guildId: "123456789012345678", channelId: "234567890123456789", }, ], daveEncryption: true, decryptionFailureTolerance: 24, tts: { provider: "openai", openai: { voice: "alloy" }, }, }, }, }, } ``` Notes: * `voice.tts` overrides `messages.tts` for voice playback only. * Voice transcript turns derive owner status from Discord `allowFrom` (or `dm.allowFrom`); non-owner speakers cannot access owner-only tools (for example `gateway` and `cron`). * Voice is enabled by default; set `channels.discord.voice.enabled=false` to disable it. * `voice.daveEncryption` and `voice.decryptionFailureTolerance` pass through to `@discordjs/voice` join options. * `@discordjs/voice` defaults are `daveEncryption=true` and `decryptionFailureTolerance=24` if unset. * OpenClaw also watches receive decrypt failures and auto-recovers by leaving/rejoining the voice channel after repeated failures in a short window. * If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)`, this may be the upstream `@discordjs/voice` receive bug tracked in [discord.js #11419](https://github.com/discordjs/discord.js/issues/11419). ## Voice messages Discord voice messages show a waveform preview and require OGG/Opus audio plus metadata. OpenClaw generates the waveform automatically, but it needs `ffmpeg` and `ffprobe` available on the gateway host to inspect and convert audio files. Requirements and constraints: * Provide a **local file path** (URLs are rejected). * Omit text content (Discord does not allow text + voice message in the same payload). * Any audio format is accepted; OpenClaw converts to OGG/Opus when needed. Example: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} message(action="send", channel="discord", target="channel:123", path="/path/to/audio.mp3", asVoice=true) ``` ## Troubleshooting * enable Message Content Intent * enable Server Members Intent when you depend on user/member resolution * restart gateway after changing intents * verify `groupPolicy` * verify guild allowlist under `channels.discord.guilds` * if guild `channels` map exists, only listed channels are allowed * verify `requireMention` behavior and mention patterns Useful checks: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw doctor openclaw channels status --probe openclaw logs --follow ``` Common causes: * `groupPolicy="allowlist"` without matching guild/channel allowlist * `requireMention` configured in the wrong place (must be under `channels.discord.guilds` or channel entry) * sender blocked by guild/channel `users` allowlist Typical logs: * `Listener DiscordMessageListener timed out after 30000ms for event MESSAGE_CREATE` * `Slow listener detected ...` Canonical knob: * single-account: `channels.discord.eventQueue.listenerTimeout` * multi-account: `channels.discord.accounts..eventQueue.listenerTimeout` Recommended baseline: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { discord: { accounts: { default: { eventQueue: { listenerTimeout: 120000, }, }, }, }, }, } ``` Tune this first before adding alternate timeout controls elsewhere. `channels status --probe` permission checks only work for numeric channel IDs. If you use slug keys, runtime matching can still work, but probe cannot fully verify permissions. * DM disabled: `channels.discord.dm.enabled=false` * DM policy disabled: `channels.discord.dmPolicy="disabled"` (legacy: `channels.discord.dm.policy`) * awaiting pairing approval in `pairing` mode By default bot-authored messages are ignored. If you set `channels.discord.allowBots=true`, use strict mention and allowlist rules to avoid loop behavior. Prefer `channels.discord.allowBots="mentions"` to only accept bot messages that mention the bot. * keep OpenClaw current (`openclaw update`) so the Discord voice receive recovery logic is present * confirm `channels.discord.voice.daveEncryption=true` (default) * start from `channels.discord.voice.decryptionFailureTolerance=24` (upstream default) and tune only if needed * watch logs for: * `discord voice: DAVE decrypt failures detected` * `discord voice: repeated decrypt failures; attempting rejoin` * if failures continue after automatic rejoin, collect logs and compare against [discord.js #11419](https://github.com/discordjs/discord.js/issues/11419) ## Configuration reference pointers Primary reference: * [Configuration reference - Discord](/gateway/configuration-reference#discord) High-signal Discord fields: * startup/auth: `enabled`, `token`, `accounts.*`, `allowBots` * policy: `groupPolicy`, `dm.*`, `guilds.*`, `guilds.*.channels.*` * command: `commands.native`, `commands.useAccessGroups`, `configWrites`, `slashCommand.*` * event queue: `eventQueue.listenerTimeout` (canonical), `eventQueue.maxQueueSize`, `eventQueue.maxConcurrency` * reply/history: `replyToMode`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit` * delivery: `textChunkLimit`, `chunkMode`, `maxLinesPerMessage` * streaming: `streaming` (legacy alias: `streamMode`), `draftChunk`, `blockStreaming`, `blockStreamingCoalesce` * media/retry: `mediaMaxMb`, `retry` * actions: `actions.*` * presence: `activity`, `status`, `activityType`, `activityUrl` * UI: `ui.components.accentColor` * features: `pluralkit`, `execApprovals`, `intents`, `agentComponents`, `heartbeat`, `responsePrefix` ## Safety and operations * Treat bot tokens as secrets (`DISCORD_BOT_TOKEN` preferred in supervised environments). * Grant least-privilege Discord permissions. * If command deploy/state is stale, restart gateway and re-check with `openclaw channels status --probe`. ## Related * [Pairing](/channels/pairing) * [Channel routing](/channels/channel-routing) * [Multi-agent routing](/concepts/multi-agent) * [Troubleshooting](/channels/troubleshooting) * [Slash commands](/tools/slash-commands) # Feishu Source: https://cryptoclawdocs.termix.ai/channels/feishu # Feishu bot Feishu (Lark) is a team chat platform used by companies for messaging and collaboration. This plugin connects OpenClaw to a Feishu/Lark bot using the platform’s WebSocket event subscription so messages can be received without exposing a public webhook URL. *** ## Plugin required Install the Feishu plugin: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install @openclaw/feishu ``` Local checkout (when running from a git repo): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install ./extensions/feishu ``` *** ## Quickstart There are two ways to add the Feishu channel: ### Method 1: onboarding wizard (recommended) If you just installed OpenClaw, run the wizard: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw onboard ``` The wizard guides you through: 1. Creating a Feishu app and collecting credentials 2. Configuring app credentials in OpenClaw 3. Starting the gateway ✅ **After configuration**, check gateway status: * `openclaw gateway status` * `openclaw logs --follow` ### Method 2: CLI setup If you already completed initial install, add the channel via CLI: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw channels add ``` Choose **Feishu**, then enter the App ID and App Secret. ✅ **After configuration**, manage the gateway: * `openclaw gateway status` * `openclaw gateway restart` * `openclaw logs --follow` *** ## Step 1: Create a Feishu app ### 1. Open Feishu Open Platform Visit [Feishu Open Platform](https://open.feishu.cn/app) and sign in. Lark (global) tenants should use [https://open.larksuite.com/app](https://open.larksuite.com/app) and set `domain: "lark"` in the Feishu config. ### 2. Create an app 1. Click **Create enterprise app** 2. Fill in the app name + description 3. Choose an app icon Create enterprise app ### 3. Copy credentials From **Credentials & Basic Info**, copy: * **App ID** (format: `cli_xxx`) * **App Secret** ❗ **Important:** keep the App Secret private. Get credentials ### 4. Configure permissions On **Permissions**, click **Batch import** and paste: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "scopes": { "tenant": [ "aily:file:read", "aily:file:write", "application:application.app_message_stats.overview:readonly", "application:application:self_manage", "application:bot.menu:write", "cardkit:card:read", "cardkit:card:write", "contact:user.employee_id:readonly", "corehr:file:download", "event:ip_list", "im:chat.access_event.bot_p2p_chat:read", "im:chat.members:bot_access", "im:message", "im:message.group_at_msg:readonly", "im:message.p2p_msg:readonly", "im:message:readonly", "im:message:send_as_bot", "im:resource" ], "user": ["aily:file:read", "aily:file:write", "im:chat.access_event.bot_p2p_chat:read"] } } ``` Configure permissions ### 5. Enable bot capability In **App Capability** > **Bot**: 1. Enable bot capability 2. Set the bot name Enable bot capability ### 6. Configure event subscription ⚠️ **Important:** before setting event subscription, make sure: 1. You already ran `openclaw channels add` for Feishu 2. The gateway is running (`openclaw gateway status`) In **Event Subscription**: 1. Choose **Use long connection to receive events** (WebSocket) 2. Add the event: `im.message.receive_v1` ⚠️ If the gateway is not running, the long-connection setup may fail to save. Configure event subscription ### 7. Publish the app 1. Create a version in **Version Management & Release** 2. Submit for review and publish 3. Wait for admin approval (enterprise apps usually auto-approve) *** ## Step 2: Configure OpenClaw ### Configure with the wizard (recommended) ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw channels add ``` Choose **Feishu** and paste your App ID + App Secret. ### Configure via config file Edit `~/.openclaw/openclaw.json`: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { feishu: { enabled: true, dmPolicy: "pairing", accounts: { main: { appId: "cli_xxx", appSecret: "xxx", botName: "My AI assistant", }, }, }, }, } ``` If you use `connectionMode: "webhook"`, set `verificationToken`. The Feishu webhook server binds to `127.0.0.1` by default; set `webhookHost` only if you intentionally need a different bind address. #### Verification Token (webhook mode) When using webhook mode, set `channels.feishu.verificationToken` in your config. To get the value: 1. In Feishu Open Platform, open your app 2. Go to **Development** → **Events & Callbacks** (开发配置 → 事件与回调) 3. Open the **Encryption** tab (加密策略) 4. Copy **Verification Token** Verification Token location ### Configure via environment variables ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export FEISHU_APP_ID="cli_xxx" export FEISHU_APP_SECRET="xxx" ``` ### Lark (global) domain If your tenant is on Lark (international), set the domain to `lark` (or a full domain string). You can set it at `channels.feishu.domain` or per account (`channels.feishu.accounts..domain`). ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { feishu: { domain: "lark", accounts: { main: { appId: "cli_xxx", appSecret: "xxx", }, }, }, }, } ``` ### Quota optimization flags You can reduce Feishu API usage with two optional flags: * `typingIndicator` (default `true`): when `false`, skip typing reaction calls. * `resolveSenderNames` (default `true`): when `false`, skip sender profile lookup calls. Set them at top level or per account: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { feishu: { typingIndicator: false, resolveSenderNames: false, accounts: { main: { appId: "cli_xxx", appSecret: "xxx", typingIndicator: true, resolveSenderNames: false, }, }, }, }, } ``` *** ## Step 3: Start + test ### 1. Start the gateway ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway ``` ### 2. Send a test message In Feishu, find your bot and send a message. ### 3. Approve pairing By default, the bot replies with a pairing code. Approve it: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw pairing approve feishu ``` After approval, you can chat normally. *** ## Overview * **Feishu bot channel**: Feishu bot managed by the gateway * **Deterministic routing**: replies always return to Feishu * **Session isolation**: DMs share a main session; groups are isolated * **WebSocket connection**: long connection via Feishu SDK, no public URL needed *** ## Access control ### Direct messages * **Default**: `dmPolicy: "pairing"` (unknown users get a pairing code) * **Approve pairing**: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw pairing list feishu openclaw pairing approve feishu ``` * **Allowlist mode**: set `channels.feishu.allowFrom` with allowed Open IDs ### Group chats **1. Group policy** (`channels.feishu.groupPolicy`): * `"open"` = allow everyone in groups (default) * `"allowlist"` = only allow `groupAllowFrom` * `"disabled"` = disable group messages **2. Mention requirement** (`channels.feishu.groups..requireMention`): * `true` = require @mention (default) * `false` = respond without mentions *** ## Group configuration examples ### Allow all groups, require @mention (default) ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { feishu: { groupPolicy: "open", // Default requireMention: true }, }, } ``` ### Allow all groups, no @mention required ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { feishu: { groups: { oc_xxx: { requireMention: false }, }, }, }, } ``` ### Allow specific groups only ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { feishu: { groupPolicy: "allowlist", // Feishu group IDs (chat_id) look like: oc_xxx groupAllowFrom: ["oc_xxx", "oc_yyy"], }, }, } ``` ### Restrict which senders can message in a group (sender allowlist) In addition to allowing the group itself, **all messages** in that group are gated by the sender open\_id: only users listed in `groups..allowFrom` have their messages processed; messages from other members are ignored (this is full sender-level gating, not only for control commands like /reset or /new). ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { feishu: { groupPolicy: "allowlist", groupAllowFrom: ["oc_xxx"], groups: { oc_xxx: { // Feishu user IDs (open_id) look like: ou_xxx allowFrom: ["ou_user1", "ou_user2"], }, }, }, }, } ``` *** ## Get group/user IDs ### Group IDs (chat\_id) Group IDs look like `oc_xxx`. **Method 1 (recommended)** 1. Start the gateway and @mention the bot in the group 2. Run `openclaw logs --follow` and look for `chat_id` **Method 2** Use the Feishu API debugger to list group chats. ### User IDs (open\_id) User IDs look like `ou_xxx`. **Method 1 (recommended)** 1. Start the gateway and DM the bot 2. Run `openclaw logs --follow` and look for `open_id` **Method 2** Check pairing requests for user Open IDs: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw pairing list feishu ``` *** ## Common commands | Command | Description | | --------- | ----------------- | | `/status` | Show bot status | | `/reset` | Reset the session | | `/model` | Show/switch model | > Note: Feishu does not support native command menus yet, so commands must be sent as text. ## Gateway management commands | Command | Description | | -------------------------- | ----------------------------- | | `openclaw gateway status` | Show gateway status | | `openclaw gateway install` | Install/start gateway service | | `openclaw gateway stop` | Stop gateway service | | `openclaw gateway restart` | Restart gateway service | | `openclaw logs --follow` | Tail gateway logs | *** ## Troubleshooting ### Bot does not respond in group chats 1. Ensure the bot is added to the group 2. Ensure you @mention the bot (default behavior) 3. Check `groupPolicy` is not set to `"disabled"` 4. Check logs: `openclaw logs --follow` ### Bot does not receive messages 1. Ensure the app is published and approved 2. Ensure event subscription includes `im.message.receive_v1` 3. Ensure **long connection** is enabled 4. Ensure app permissions are complete 5. Ensure the gateway is running: `openclaw gateway status` 6. Check logs: `openclaw logs --follow` ### App Secret leak 1. Reset the App Secret in Feishu Open Platform 2. Update the App Secret in your config 3. Restart the gateway ### Message send failures 1. Ensure the app has `im:message:send_as_bot` permission 2. Ensure the app is published 3. Check logs for detailed errors *** ## Advanced configuration ### Multiple accounts ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { feishu: { defaultAccount: "main", accounts: { main: { appId: "cli_xxx", appSecret: "xxx", botName: "Primary bot", }, backup: { appId: "cli_yyy", appSecret: "yyy", botName: "Backup bot", enabled: false, }, }, }, }, } ``` `defaultAccount` controls which Feishu account is used when outbound APIs do not specify an `accountId` explicitly. ### Message limits * `textChunkLimit`: outbound text chunk size (default: 2000 chars) * `mediaMaxMb`: media upload/download limit (default: 30MB) ### Streaming Feishu supports streaming replies via interactive cards. When enabled, the bot updates a card as it generates text. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { feishu: { streaming: true, // enable streaming card output (default true) blockStreaming: true, // enable block-level streaming (default true) }, }, } ``` Set `streaming: false` to wait for the full reply before sending. ### Multi-agent routing Use `bindings` to route Feishu DMs or groups to different agents. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { list: [ { id: "main" }, { id: "clawd-fan", workspace: "/home/user/clawd-fan", agentDir: "/home/user/.openclaw/agents/clawd-fan/agent", }, { id: "clawd-xi", workspace: "/home/user/clawd-xi", agentDir: "/home/user/.openclaw/agents/clawd-xi/agent", }, ], }, bindings: [ { agentId: "main", match: { channel: "feishu", peer: { kind: "direct", id: "ou_xxx" }, }, }, { agentId: "clawd-fan", match: { channel: "feishu", peer: { kind: "direct", id: "ou_yyy" }, }, }, { agentId: "clawd-xi", match: { channel: "feishu", peer: { kind: "group", id: "oc_zzz" }, }, }, ], } ``` Routing fields: * `match.channel`: `"feishu"` * `match.peer.kind`: `"direct"` or `"group"` * `match.peer.id`: user Open ID (`ou_xxx`) or group ID (`oc_xxx`) See [Get group/user IDs](#get-groupuser-ids) for lookup tips. *** ## Configuration reference Full configuration: [Gateway configuration](/gateway/configuration) Key options: | Setting | Description | Default | | ------------------------------------------------- | --------------------------------------- | ---------------- | | `channels.feishu.enabled` | Enable/disable channel | `true` | | `channels.feishu.domain` | API domain (`feishu` or `lark`) | `feishu` | | `channels.feishu.connectionMode` | Event transport mode | `websocket` | | `channels.feishu.defaultAccount` | Default account ID for outbound routing | `default` | | `channels.feishu.verificationToken` | Required for webhook mode | - | | `channels.feishu.webhookPath` | Webhook route path | `/feishu/events` | | `channels.feishu.webhookHost` | Webhook bind host | `127.0.0.1` | | `channels.feishu.webhookPort` | Webhook bind port | `3000` | | `channels.feishu.accounts..appId` | App ID | - | | `channels.feishu.accounts..appSecret` | App Secret | - | | `channels.feishu.accounts..domain` | Per-account API domain override | `feishu` | | `channels.feishu.dmPolicy` | DM policy | `pairing` | | `channels.feishu.allowFrom` | DM allowlist (open\_id list) | - | | `channels.feishu.groupPolicy` | Group policy | `open` | | `channels.feishu.groupAllowFrom` | Group allowlist | - | | `channels.feishu.groups..requireMention` | Require @mention | `true` | | `channels.feishu.groups..enabled` | Enable group | `true` | | `channels.feishu.textChunkLimit` | Message chunk size | `2000` | | `channels.feishu.mediaMaxMb` | Media size limit | `30` | | `channels.feishu.streaming` | Enable streaming card output | `true` | | `channels.feishu.blockStreaming` | Enable block streaming | `true` | *** ## dmPolicy reference | Value | Behavior | | ------------- | --------------------------------------------------------------- | | `"pairing"` | **Default.** Unknown users get a pairing code; must be approved | | `"allowlist"` | Only users in `allowFrom` can chat | | `"open"` | Allow all users (requires `"*"` in allowFrom) | | `"disabled"` | Disable DMs | *** ## Supported message types ### Receive * ✅ Text * ✅ Rich text (post) * ✅ Images * ✅ Files * ✅ Audio * ✅ Video * ✅ Stickers ### Send * ✅ Text * ✅ Images * ✅ Files * ✅ Audio * ⚠️ Rich text (partial support) # Google Chat Source: https://cryptoclawdocs.termix.ai/channels/googlechat # Google Chat (Chat API) Status: ready for DMs + spaces via Google Chat API webhooks (HTTP only). ## Quick setup (beginner) 1. Create a Google Cloud project and enable the **Google Chat API**. * Go to: [Google Chat API Credentials](https://console.cloud.google.com/apis/api/chat.googleapis.com/credentials) * Enable the API if it is not already enabled. 2. Create a **Service Account**: * Press **Create Credentials** > **Service Account**. * Name it whatever you want (e.g., `openclaw-chat`). * Leave permissions blank (press **Continue**). * Leave principals with access blank (press **Done**). 3. Create and download the **JSON Key**: * In the list of service accounts, click on the one you just created. * Go to the **Keys** tab. * Click **Add Key** > **Create new key**. * Select **JSON** and press **Create**. 4. Store the downloaded JSON file on your gateway host (e.g., `~/.openclaw/googlechat-service-account.json`). 5. Create a Google Chat app in the [Google Cloud Console Chat Configuration](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat): * Fill in the **Application info**: * **App name**: (e.g. `OpenClaw`) * **Avatar URL**: (e.g. `https://openclaw.ai/logo.png`) * **Description**: (e.g. `Personal AI Assistant`) * Enable **Interactive features**. * Under **Functionality**, check **Join spaces and group conversations**. * Under **Connection settings**, select **HTTP endpoint URL**. * Under **Triggers**, select **Use a common HTTP endpoint URL for all triggers** and set it to your gateway's public URL followed by `/googlechat`. * *Tip: Run `openclaw status` to find your gateway's public URL.* * Under **Visibility**, check **Make this Chat app available to specific people and groups in \**. * Enter your email address (e.g. `user@example.com`) in the text box. * Click **Save** at the bottom. 6. **Enable the app status**: * After saving, **refresh the page**. * Look for the **App status** section (usually near the top or bottom after saving). * Change the status to **Live - available to users**. * Click **Save** again. 7. Configure OpenClaw with the service account path + webhook audience: * Env: `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE=/path/to/service-account.json` * Or config: `channels.googlechat.serviceAccountFile: "/path/to/service-account.json"`. 8. Set the webhook audience type + value (matches your Chat app config). 9. Start the gateway. Google Chat will POST to your webhook path. ## Add to Google Chat Once the gateway is running and your email is added to the visibility list: 1. Go to [Google Chat](https://chat.google.com/). 2. Click the **+** (plus) icon next to **Direct Messages**. 3. In the search bar (where you usually add people), type the **App name** you configured in the Google Cloud Console. * **Note**: The bot will *not* appear in the "Marketplace" browse list because it is a private app. You must search for it by name. 4. Select your bot from the results. 5. Click **Add** or **Chat** to start a 1:1 conversation. 6. Send "Hello" to trigger the assistant! ## Public URL (Webhook-only) Google Chat webhooks require a public HTTPS endpoint. For security, **only expose the `/googlechat` path** to the internet. Keep the OpenClaw dashboard and other sensitive endpoints on your private network. ### Option A: Tailscale Funnel (Recommended) Use Tailscale Serve for the private dashboard and Funnel for the public webhook path. This keeps `/` private while exposing only `/googlechat`. 1. **Check what address your gateway is bound to:** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ss -tlnp | grep 18789 ``` Note the IP address (e.g., `127.0.0.1`, `0.0.0.0`, or your Tailscale IP like `100.x.x.x`). 2. **Expose the dashboard to the tailnet only (port 8443):** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # If bound to localhost (127.0.0.1 or 0.0.0.0): tailscale serve --bg --https 8443 http://127.0.0.1:18789 # If bound to Tailscale IP only (e.g., 100.106.161.80): tailscale serve --bg --https 8443 http://100.106.161.80:18789 ``` 3. **Expose only the webhook path publicly:** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # If bound to localhost (127.0.0.1 or 0.0.0.0): tailscale funnel --bg --set-path /googlechat http://127.0.0.1:18789/googlechat # If bound to Tailscale IP only (e.g., 100.106.161.80): tailscale funnel --bg --set-path /googlechat http://100.106.161.80:18789/googlechat ``` 4. **Authorize the node for Funnel access:** If prompted, visit the authorization URL shown in the output to enable Funnel for this node in your tailnet policy. 5. **Verify the configuration:** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} tailscale serve status tailscale funnel status ``` Your public webhook URL will be: `https://..ts.net/googlechat` Your private dashboard stays tailnet-only: `https://..ts.net:8443/` Use the public URL (without `:8443`) in the Google Chat app config. > Note: This configuration persists across reboots. To remove it later, run `tailscale funnel reset` and `tailscale serve reset`. ### Option B: Reverse Proxy (Caddy) If you use a reverse proxy like Caddy, only proxy the specific path: ```caddy theme={"theme":{"light":"min-light","dark":"min-dark"}} your-domain.com { reverse_proxy /googlechat* localhost:18789 } ``` With this config, any request to `your-domain.com/` will be ignored or returned as 404, while `your-domain.com/googlechat` is safely routed to OpenClaw. ### Option C: Cloudflare Tunnel Configure your tunnel's ingress rules to only route the webhook path: * **Path**: `/googlechat` -> `http://localhost:18789/googlechat` * **Default Rule**: HTTP 404 (Not Found) ## How it works 1. Google Chat sends webhook POSTs to the gateway. Each request includes an `Authorization: Bearer ` header. * OpenClaw verifies bearer auth before reading/parsing full webhook bodies when the header is present. * Google Workspace Add-on requests that carry `authorizationEventObject.systemIdToken` in the body are supported via a stricter pre-auth body budget. 2. OpenClaw verifies the token against the configured `audienceType` + `audience`: * `audienceType: "app-url"` → audience is your HTTPS webhook URL. * `audienceType: "project-number"` → audience is the Cloud project number. 3. Messages are routed by space: * DMs use session key `agent::googlechat:dm:`. * Spaces use session key `agent::googlechat:group:`. 4. DM access is pairing by default. Unknown senders receive a pairing code; approve with: * `openclaw pairing approve googlechat ` 5. Group spaces require @-mention by default. Use `botUser` if mention detection needs the app’s user name. ## Targets Use these identifiers for delivery and allowlists: * Direct messages: `users/` (recommended). * Raw email `name@example.com` is mutable and only used for direct allowlist matching when `channels.googlechat.dangerouslyAllowNameMatching: true`. * Deprecated: `users/` is treated as a user id, not an email allowlist. * Spaces: `spaces/`. ## Config highlights ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { googlechat: { enabled: true, serviceAccountFile: "/path/to/service-account.json", // or serviceAccountRef: { source: "file", provider: "filemain", id: "/channels/googlechat/serviceAccount" } audienceType: "app-url", audience: "https://gateway.example.com/googlechat", webhookPath: "/googlechat", botUser: "users/1234567890", // optional; helps mention detection dm: { policy: "pairing", allowFrom: ["users/1234567890"], }, groupPolicy: "allowlist", groups: { "spaces/AAAA": { allow: true, requireMention: true, users: ["users/1234567890"], systemPrompt: "Short answers only.", }, }, actions: { reactions: true }, typingIndicator: "message", mediaMaxMb: 20, }, }, } ``` Notes: * Service account credentials can also be passed inline with `serviceAccount` (JSON string). * `serviceAccountRef` is also supported (env/file SecretRef), including per-account refs under `channels.googlechat.accounts..serviceAccountRef`. * Default webhook path is `/googlechat` if `webhookPath` isn’t set. * `dangerouslyAllowNameMatching` re-enables mutable email principal matching for allowlists (break-glass compatibility mode). * Reactions are available via the `reactions` tool and `channels action` when `actions.reactions` is enabled. * `typingIndicator` supports `none`, `message` (default), and `reaction` (reaction requires user OAuth). * Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`). Secrets reference details: [Secrets Management](/gateway/secrets). ## Troubleshooting ### 405 Method Not Allowed If Google Cloud Logs Explorer shows errors like: ``` status code: 405, reason phrase: HTTP error response: HTTP/1.1 405 Method Not Allowed ``` This means the webhook handler isn't registered. Common causes: 1. **Channel not configured**: The `channels.googlechat` section is missing from your config. Verify with: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw config get channels.googlechat ``` If it returns "Config path not found", add the configuration (see [Config highlights](#config-highlights)). 2. **Plugin not enabled**: Check plugin status: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins list | grep googlechat ``` If it shows "disabled", add `plugins.entries.googlechat.enabled: true` to your config. 3. **Gateway not restarted**: After adding config, restart the gateway: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway restart ``` Verify the channel is running: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw channels status # Should show: Google Chat default: enabled, configured, ... ``` ### Other issues * Check `openclaw channels status --probe` for auth errors or missing audience config. * If no messages arrive, confirm the Chat app's webhook URL + event subscriptions. * If mention gating blocks replies, set `botUser` to the app's user resource name and verify `requireMention`. * Use `openclaw logs --follow` while sending a test message to see if requests reach the gateway. Related docs: * [Gateway configuration](/gateway/configuration) * [Security](/gateway/security) * [Reactions](/tools/reactions) # Group Messages Source: https://cryptoclawdocs.termix.ai/channels/group-messages # Group messages (WhatsApp web channel) Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that thread separate from the personal DM session. Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/Slack/iMessage as well; this doc focuses on WhatsApp-specific behavior. For multi-agent setups, set `agents.list[].groupChat.mentionPatterns` per agent (or use `messages.groupChat.mentionPatterns` as a global fallback). ## What’s implemented (2025-12-03) * Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all). * Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders). * Per-group sessions: session keys look like `agent::whatsapp:group:` so commands such as `/verbose on` or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads. * Context injection: **pending-only** group messages (default 50) that *did not* trigger a run are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. Messages already in the session are not re-injected. * Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Pi knows who is speaking. * Ephemeral/view-once: we unwrap those before extracting text/mentions, so pings inside them still trigger. * Group system prompt: on the first turn of a group session (and whenever `/activation` changes the mode) we inject a short blurb into the system prompt like `You are replying inside the WhatsApp group "". Group members: Alice (+44...), Bob (+43...), … Activation: trigger-only … Address the specific sender noted in the message context.` If metadata isn’t available we still tell the agent it’s a group chat. ## Config example (WhatsApp) Add a `groupChat` block to `~/.openclaw/openclaw.json` so display-name pings work even when WhatsApp strips the visual `@` in the text body: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { groups: { "*": { requireMention: true }, }, }, }, agents: { list: [ { id: "main", groupChat: { historyLimit: 50, mentionPatterns: ["@?openclaw", "\\+?15555550123"], }, }, ], }, } ``` Notes: * The regexes are case-insensitive; they cover a display-name ping like `@openclaw` and the raw number with or without `+`/spaces. * WhatsApp still sends canonical mentions via `mentionedJids` when someone taps the contact, so the number fallback is rarely needed but is a useful safety net. ### Activation command (owner-only) Use the group chat command: * `/activation mention` * `/activation always` Only the owner number (from `channels.whatsapp.allowFrom`, or the bot’s own E.164 when unset) can change this. Send `/status` as a standalone message in the group to see the current activation mode. ## How to use 1. Add your WhatsApp account (the one running OpenClaw) to the group. 2. Say `@openclaw …` (or include the number). Only allowlisted senders can trigger it unless you set `groupPolicy: "open"`. 3. The agent prompt will include recent group context plus the trailing `[from: …]` marker so it can address the right person. 4. Session-level directives (`/verbose on`, `/think high`, `/new` or `/reset`, `/compact`) apply only to that group’s session; send them as standalone messages so they register. Your personal DM session remains independent. ## Testing / verification * Manual smoke: * Send an `@openclaw` ping in the group and confirm a reply that references the sender name. * Send a second ping and verify the history block is included then cleared on the next turn. * Check gateway logs (run with `--verbose`) to see `inbound web message` entries showing `from: ` and the `[from: …]` suffix. ## Known considerations * Heartbeats are intentionally skipped for groups to avoid noisy broadcasts. * Echo suppression uses the combined batch string; if you send identical text twice without mentions, only the first will get a response. * Session store entries will appear as `agent::whatsapp:group:` in the session store (`~/.openclaw/agents//sessions/sessions.json` by default); a missing entry just means the group hasn’t triggered a run yet. * Typing indicators in groups follow `agents.defaults.typingMode` (default: `message` when unmentioned). # Groups Source: https://cryptoclawdocs.termix.ai/channels/groups # Groups OpenClaw treats group chats consistently across surfaces: WhatsApp, Telegram, Discord, Slack, Signal, iMessage, Microsoft Teams, Zalo. ## Beginner intro (2 minutes) OpenClaw “lives” on your own messaging accounts. There is no separate WhatsApp bot user. If **you** are in a group, OpenClaw can see that group and respond there. Default behavior: * Groups are restricted (`groupPolicy: "allowlist"`). * Replies require a mention unless you explicitly disable mention gating. Translation: allowlisted senders can trigger OpenClaw by mentioning it. > TL;DR > > * **DM access** is controlled by `*.allowFrom`. > * **Group access** is controlled by `*.groupPolicy` + allowlists (`*.groups`, `*.groupAllowFrom`). > * **Reply triggering** is controlled by mention gating (`requireMention`, `/activation`). Quick flow (what happens to a group message): ``` groupPolicy? disabled -> drop groupPolicy? allowlist -> group allowed? no -> drop requireMention? yes -> mentioned? no -> store for context only otherwise -> reply ``` Group message flow If you want... | Goal | What to set | | -------------------------------------------- | ---------------------------------------------------------- | | Allow all groups but only reply on @mentions | `groups: { "*": { requireMention: true } }` | | Disable all group replies | `groupPolicy: "disabled"` | | Only specific groups | `groups: { "": { ... } }` (no `"*"` key) | | Only you can trigger in groups | `groupPolicy: "allowlist"`, `groupAllowFrom: ["+1555..."]` | ## Session keys * Group sessions use `agent:::group:` session keys (rooms/channels use `agent:::channel:`). * Telegram forum topics add `:topic:` to the group id so each topic has its own session. * Direct chats use the main session (or per-sender if configured). * Heartbeats are skipped for group sessions. ## Pattern: personal DMs + public groups (single agent) Yes — this works well if your “personal” traffic is **DMs** and your “public” traffic is **groups**. Why: in single-agent mode, DMs typically land in the **main** session key (`agent:main:main`), while groups always use **non-main** session keys (`agent:main::group:`). If you enable sandboxing with `mode: "non-main"`, those group sessions run in Docker while your main DM session stays on-host. This gives you one agent “brain” (shared workspace + memory), but two execution postures: * **DMs**: full tools (host) * **Groups**: sandbox + restricted tools (Docker) > If you need truly separate workspaces/personas (“personal” and “public” must never mix), use a second agent + bindings. See [Multi-Agent Routing](/concepts/multi-agent). Example (DMs on host, groups sandboxed + messaging-only tools): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { defaults: { sandbox: { mode: "non-main", // groups/channels are non-main -> sandboxed scope: "session", // strongest isolation (one container per group/channel) workspaceAccess: "none", }, }, }, tools: { sandbox: { tools: { // If allow is non-empty, everything else is blocked (deny still wins). allow: ["group:messaging", "group:sessions"], deny: ["group:runtime", "group:fs", "group:ui", "nodes", "cron", "gateway"], }, }, }, } ``` Want “groups can only see folder X” instead of “no host access”? Keep `workspaceAccess: "none"` and mount only allowlisted paths into the sandbox: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { defaults: { sandbox: { mode: "non-main", scope: "session", workspaceAccess: "none", docker: { binds: [ // hostPath:containerPath:mode "/home/user/FriendsShared:/data:ro", ], }, }, }, }, } ``` Related: * Configuration keys and defaults: [Gateway configuration](/gateway/configuration#agentsdefaultssandbox) * Debugging why a tool is blocked: [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) * Bind mounts details: [Sandboxing](/gateway/sandboxing#custom-bind-mounts) ## Display labels * UI labels use `displayName` when available, formatted as `:`. * `#room` is reserved for rooms/channels; group chats use `g-` (lowercase, spaces -> `-`, keep `#@+._-`). ## Group policy Control how group/room messages are handled per channel: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { groupPolicy: "disabled", // "open" | "disabled" | "allowlist" groupAllowFrom: ["+15551234567"], }, telegram: { groupPolicy: "disabled", groupAllowFrom: ["123456789"], // numeric Telegram user id (wizard can resolve @username) }, signal: { groupPolicy: "disabled", groupAllowFrom: ["+15551234567"], }, imessage: { groupPolicy: "disabled", groupAllowFrom: ["chat_id:123"], }, msteams: { groupPolicy: "disabled", groupAllowFrom: ["user@org.com"], }, discord: { groupPolicy: "allowlist", guilds: { GUILD_ID: { channels: { help: { allow: true } } }, }, }, slack: { groupPolicy: "allowlist", channels: { "#general": { allow: true } }, }, matrix: { groupPolicy: "allowlist", groupAllowFrom: ["@owner:example.org"], groups: { "!roomId:example.org": { allow: true }, "#alias:example.org": { allow: true }, }, }, }, } ``` | Policy | Behavior | | ------------- | ------------------------------------------------------------ | | `"open"` | Groups bypass allowlists; mention-gating still applies. | | `"disabled"` | Block all group messages entirely. | | `"allowlist"` | Only allow groups/rooms that match the configured allowlist. | Notes: * `groupPolicy` is separate from mention-gating (which requires @mentions). * WhatsApp/Telegram/Signal/iMessage/Microsoft Teams/Zalo: use `groupAllowFrom` (fallback: explicit `allowFrom`). * DM pairing approvals (`*-allowFrom` store entries) apply to DM access only; group sender authorization stays explicit to group allowlists. * Discord: allowlist uses `channels.discord.guilds..channels`. * Slack: allowlist uses `channels.slack.channels`. * Matrix: allowlist uses `channels.matrix.groups` (room IDs, aliases, or names). Use `channels.matrix.groupAllowFrom` to restrict senders; per-room `users` allowlists are also supported. * Group DMs are controlled separately (`channels.discord.dm.*`, `channels.slack.dm.*`). * Telegram allowlist can match user IDs (`"123456789"`, `"telegram:123456789"`, `"tg:123456789"`) or usernames (`"@alice"` or `"alice"`); prefixes are case-insensitive. * Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked. * Runtime safety: when a provider block is completely missing (`channels.` absent), group policy falls back to a fail-closed mode (typically `allowlist`) instead of inheriting `channels.defaults.groupPolicy`. Quick mental model (evaluation order for group messages): 1. `groupPolicy` (open/disabled/allowlist) 2. group allowlists (`*.groups`, `*.groupAllowFrom`, channel-specific allowlist) 3. mention gating (`requireMention`, `/activation`) ## Mention gating (default) Group messages require a mention unless overridden per group. Defaults live per subsystem under `*.groups."*"`. Replying to a bot message counts as an implicit mention (when the channel supports reply metadata). This applies to Telegram, WhatsApp, Slack, Discord, and Microsoft Teams. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { groups: { "*": { requireMention: true }, "123@g.us": { requireMention: false }, }, }, telegram: { groups: { "*": { requireMention: true }, "123456789": { requireMention: false }, }, }, imessage: { groups: { "*": { requireMention: true }, "123": { requireMention: false }, }, }, }, agents: { list: [ { id: "main", groupChat: { mentionPatterns: ["@openclaw", "openclaw", "\\+15555550123"], historyLimit: 50, }, }, ], }, } ``` Notes: * `mentionPatterns` are case-insensitive regexes. * Surfaces that provide explicit mentions still pass; patterns are a fallback. * Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group). * Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured). * Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel). * Group history context is wrapped uniformly across channels and is **pending-only** (messages skipped due to mention gating); use `messages.groupChat.historyLimit` for the global default and `channels..historyLimit` (or `channels..accounts.*.historyLimit`) for overrides. Set `0` to disable. ## Group/channel tool restrictions (optional) Some channel configs support restricting which tools are available **inside a specific group/room/channel**. * `tools`: allow/deny tools for the whole group. * `toolsBySender`: per-sender overrides within the group. Use explicit key prefixes: `id:`, `e164:`, `username:`, `name:`, and `"*"` wildcard. Legacy unprefixed keys are still accepted and matched as `id:` only. Resolution order (most specific wins): 1. group/channel `toolsBySender` match 2. group/channel `tools` 3. default (`"*"`) `toolsBySender` match 4. default (`"*"`) `tools` Example (Telegram): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { telegram: { groups: { "*": { tools: { deny: ["exec"] } }, "-1001234567890": { tools: { deny: ["exec", "read", "write"] }, toolsBySender: { "id:123456789": { alsoAllow: ["exec"] }, }, }, }, }, }, } ``` Notes: * Group/channel tool restrictions are applied in addition to global/agent tool policy (deny still wins). * Some channels use different nesting for rooms/channels (e.g., Discord `guilds.*.channels.*`, Slack `channels.*`, MS Teams `teams.*.channels.*`). ## Group allowlists When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior. Common intents (copy/paste): 1. Disable all group replies ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { groupPolicy: "disabled" } }, } ``` 2. Allow only specific groups (WhatsApp) ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { groups: { "123@g.us": { requireMention: true }, "456@g.us": { requireMention: false }, }, }, }, } ``` 3. Allow all groups but require mention (explicit) ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { groups: { "*": { requireMention: true } }, }, }, } ``` 4. Only the owner can trigger in groups (WhatsApp) ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { groupPolicy: "allowlist", groupAllowFrom: ["+15551234567"], groups: { "*": { requireMention: true } }, }, }, } ``` ## Activation (owner-only) Group owners can toggle per-group activation: * `/activation mention` * `/activation always` Owner is determined by `channels.whatsapp.allowFrom` (or the bot’s self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore `/activation`. ## Context fields Group inbound payloads set: * `ChatType=group` * `GroupSubject` (if known) * `GroupMembers` (if known) * `WasMentioned` (mention gating result) * Telegram forum topics also include `MessageThreadId` and `IsForum`. The agent system prompt includes a group intro on the first turn of a new group session. It reminds the model to respond like a human, avoid Markdown tables, and avoid typing literal `\n` sequences. ## iMessage specifics * Prefer `chat_id:` when routing or allowlisting. * List chats: `imsg chats --limit 20`. * Group replies always go back to the same `chat_id`. ## WhatsApp specifics See [Group messages](/channels/group-messages) for WhatsApp-only behavior (history injection, mention handling details). # iMessage Source: https://cryptoclawdocs.termix.ai/channels/imessage # iMessage (legacy: imsg) For new iMessage deployments, use BlueBubbles. The `imsg` integration is legacy and may be removed in a future release. Status: legacy external CLI integration. Gateway spawns `imsg rpc` and communicates over JSON-RPC on stdio (no separate daemon/port). Preferred iMessage path for new setups. iMessage DMs default to pairing mode. Full iMessage field reference. ## Quick setup ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} brew install steipete/tap/imsg imsg rpc --help ``` ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { imessage: { enabled: true, cliPath: "/usr/local/bin/imsg", dbPath: "/Users//Library/Messages/chat.db", }, }, } ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw pairing list imessage openclaw pairing approve imessage ``` Pairing requests expire after 1 hour. OpenClaw only requires a stdio-compatible `cliPath`, so you can point `cliPath` at a wrapper script that SSHes to a remote Mac and runs `imsg`. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} #!/usr/bin/env bash exec ssh -T gateway-host imsg "$@" ``` Recommended config when attachments are enabled: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { imessage: { enabled: true, cliPath: "~/.openclaw/scripts/imsg-ssh", remoteHost: "user@gateway-host", // used for SCP attachment fetches includeAttachments: true, // Optional: override allowed attachment roots. // Defaults include /Users/*/Library/Messages/Attachments attachmentRoots: ["/Users/*/Library/Messages/Attachments"], remoteAttachmentRoots: ["/Users/*/Library/Messages/Attachments"], }, }, } ``` If `remoteHost` is not set, OpenClaw attempts to auto-detect it by parsing the SSH wrapper script. `remoteHost` must be `host` or `user@host` (no spaces or SSH options). OpenClaw uses strict host-key checking for SCP, so the relay host key must already exist in `~/.ssh/known_hosts`. Attachment paths are validated against allowed roots (`attachmentRoots` / `remoteAttachmentRoots`). ## Requirements and permissions (macOS) * Messages must be signed in on the Mac running `imsg`. * Full Disk Access is required for the process context running OpenClaw/`imsg` (Messages DB access). * Automation permission is required to send messages through Messages.app. Permissions are granted per process context. If gateway runs headless (LaunchAgent/SSH), run a one-time interactive command in that same context to trigger prompts: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} imsg chats --limit 1 # or imsg send "test" ``` ## Access control and routing `channels.imessage.dmPolicy` controls direct messages: * `pairing` (default) * `allowlist` * `open` (requires `allowFrom` to include `"*"`) * `disabled` Allowlist field: `channels.imessage.allowFrom`. Allowlist entries can be handles or chat targets (`chat_id:*`, `chat_guid:*`, `chat_identifier:*`). `channels.imessage.groupPolicy` controls group handling: * `allowlist` (default when configured) * `open` * `disabled` Group sender allowlist: `channels.imessage.groupAllowFrom`. Runtime fallback: if `groupAllowFrom` is unset, iMessage group sender checks fall back to `allowFrom` when available. Runtime note: if `channels.imessage` is completely missing, runtime falls back to `groupPolicy="allowlist"` and logs a warning (even if `channels.defaults.groupPolicy` is set). Mention gating for groups: * iMessage has no native mention metadata * mention detection uses regex patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`) * with no configured patterns, mention gating cannot be enforced Control commands from authorized senders can bypass mention gating in groups. * DMs use direct routing; groups use group routing. * With default `session.dmScope=main`, iMessage DMs collapse into the agent main session. * Group sessions are isolated (`agent::imessage:group:`). * Replies route back to iMessage using originating channel/target metadata. Group-ish thread behavior: Some multi-participant iMessage threads can arrive with `is_group=false`. If that `chat_id` is explicitly configured under `channels.imessage.groups`, OpenClaw treats it as group traffic (group gating + group session isolation). ## Deployment patterns Use a dedicated Apple ID and macOS user so bot traffic is isolated from your personal Messages profile. Typical flow: 1. Create/sign in a dedicated macOS user. 2. Sign into Messages with the bot Apple ID in that user. 3. Install `imsg` in that user. 4. Create SSH wrapper so OpenClaw can run `imsg` in that user context. 5. Point `channels.imessage.accounts..cliPath` and `.dbPath` to that user profile. First run may require GUI approvals (Automation + Full Disk Access) in that bot user session. Common topology: * gateway runs on Linux/VM * iMessage + `imsg` runs on a Mac in your tailnet * `cliPath` wrapper uses SSH to run `imsg` * `remoteHost` enables SCP attachment fetches Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { imessage: { enabled: true, cliPath: "~/.openclaw/scripts/imsg-ssh", remoteHost: "bot@mac-mini.tailnet-1234.ts.net", includeAttachments: true, dbPath: "/Users/bot/Library/Messages/chat.db", }, }, } ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} #!/usr/bin/env bash exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@" ``` Use SSH keys so both SSH and SCP are non-interactive. Ensure the host key is trusted first (for example `ssh bot@mac-mini.tailnet-1234.ts.net`) so `known_hosts` is populated. iMessage supports per-account config under `channels.imessage.accounts`. Each account can override fields such as `cliPath`, `dbPath`, `allowFrom`, `groupPolicy`, `mediaMaxMb`, history settings, and attachment root allowlists. ## Media, chunking, and delivery targets * inbound attachment ingestion is optional: `channels.imessage.includeAttachments` * remote attachment paths can be fetched via SCP when `remoteHost` is set * attachment paths must match allowed roots: * `channels.imessage.attachmentRoots` (local) * `channels.imessage.remoteAttachmentRoots` (remote SCP mode) * default root pattern: `/Users/*/Library/Messages/Attachments` * SCP uses strict host-key checking (`StrictHostKeyChecking=yes`) * outbound media size uses `channels.imessage.mediaMaxMb` (default 16 MB) * text chunk limit: `channels.imessage.textChunkLimit` (default 4000) * chunk mode: `channels.imessage.chunkMode` * `length` (default) * `newline` (paragraph-first splitting) Preferred explicit targets: * `chat_id:123` (recommended for stable routing) * `chat_guid:...` * `chat_identifier:...` Handle targets are also supported: * `imessage:+1555...` * `sms:+1555...` * `user@example.com` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} imsg chats --limit 20 ``` ## Config writes iMessage allows channel-initiated config writes by default (for `/config set|unset` when `commands.config: true`). Disable: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { imessage: { configWrites: false, }, }, } ``` ## Troubleshooting Validate the binary and RPC support: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} imsg rpc --help openclaw channels status --probe ``` If probe reports RPC unsupported, update `imsg`. Check: * `channels.imessage.dmPolicy` * `channels.imessage.allowFrom` * pairing approvals (`openclaw pairing list imessage`) Check: * `channels.imessage.groupPolicy` * `channels.imessage.groupAllowFrom` * `channels.imessage.groups` allowlist behavior * mention pattern configuration (`agents.list[].groupChat.mentionPatterns`) Check: * `channels.imessage.remoteHost` * `channels.imessage.remoteAttachmentRoots` * SSH/SCP key auth from the gateway host * host key exists in `~/.ssh/known_hosts` on the gateway host * remote path readability on the Mac running Messages Re-run in an interactive GUI terminal in the same user/session context and approve prompts: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} imsg chats --limit 1 imsg send "test" ``` Confirm Full Disk Access + Automation are granted for the process context that runs OpenClaw/`imsg`. ## Configuration reference pointers * [Configuration reference - iMessage](/gateway/configuration-reference#imessage) * [Gateway configuration](/gateway/configuration) * [Pairing](/channels/pairing) * [BlueBubbles](/channels/bluebubbles) # Chat Channels Source: https://cryptoclawdocs.termix.ai/channels/index # Chat Channels OpenClaw can talk to you on any chat app you already use. Each channel connects via the Gateway. Text is supported everywhere; media and reactions vary by channel. ## Supported channels * [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe). * [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs. * [Feishu](/channels/feishu) — Feishu/Lark bot via WebSocket (plugin, installed separately). * [Google Chat](/channels/googlechat) — Google Chat API app via HTTP webhook. * [iMessage (legacy)](/channels/imessage) — Legacy macOS integration via imsg CLI (deprecated, use BlueBubbles for new setups). * [IRC](/channels/irc) — Classic IRC servers; channels + DMs with pairing/allowlist controls. * [LINE](/channels/line) — LINE Messaging API bot (plugin, installed separately). * [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately). * [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (plugin, installed separately). * [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately). * [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately). * [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately). * [Signal](/channels/signal) — signal-cli; privacy-focused. * [Synology Chat](/channels/synology-chat) — Synology NAS Chat via outgoing+incoming webhooks (plugin, installed separately). * [Slack](/channels/slack) — Bolt SDK; workspace apps. * [Telegram](/channels/telegram) — Bot API via grammY; supports groups. * [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately). * [Twitch](/channels/twitch) — Twitch chat via IRC connection (plugin, installed separately). * [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket. * [WhatsApp](/channels/whatsapp) — Most popular; uses Baileys and requires QR pairing. * [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately). * [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately). ## Notes * Channels can run simultaneously; configure multiple and OpenClaw will route per chat. * Fastest setup is usually **Telegram** (simple bot token). WhatsApp requires QR pairing and stores more state on disk. * Group behavior varies by channel; see [Groups](/channels/groups). * DM pairing and allowlists are enforced for safety; see [Security](/gateway/security). * Troubleshooting: [Channel troubleshooting](/channels/troubleshooting). * Model providers are documented separately; see [Model Providers](/providers/models). # IRC Source: https://cryptoclawdocs.termix.ai/channels/irc Connect OpenClaw to IRC channels and direct messages. Use IRC when you want OpenClaw in classic channels (`#room`) and direct messages. IRC ships as an extension plugin, but it is configured in the main config under `channels.irc`. ## Quick start 1. Enable IRC config in `~/.openclaw/openclaw.json`. 2. Set at least: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "channels": { "irc": { "enabled": true, "host": "irc.libera.chat", "port": 6697, "tls": true, "nick": "openclaw-bot", "channels": ["#openclaw"] } } } ``` 3. Start/restart gateway: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway run ``` ## Security defaults * `channels.irc.dmPolicy` defaults to `"pairing"`. * `channels.irc.groupPolicy` defaults to `"allowlist"`. * With `groupPolicy="allowlist"`, set `channels.irc.groups` to define allowed channels. * Use TLS (`channels.irc.tls=true`) unless you intentionally accept plaintext transport. ## Access control There are two separate “gates” for IRC channels: 1. **Channel access** (`groupPolicy` + `groups`): whether the bot accepts messages from a channel at all. 2. **Sender access** (`groupAllowFrom` / per-channel `groups["#channel"].allowFrom`): who is allowed to trigger the bot inside that channel. Config keys: * DM allowlist (DM sender access): `channels.irc.allowFrom` * Group sender allowlist (channel sender access): `channels.irc.groupAllowFrom` * Per-channel controls (channel + sender + mention rules): `channels.irc.groups["#channel"]` * `channels.irc.groupPolicy="open"` allows unconfigured channels (**still mention-gated by default**) Allowlist entries should use stable sender identities (`nick!user@host`). Bare nick matching is mutable and only enabled when `channels.irc.dangerouslyAllowNameMatching: true`. ### Common gotcha: `allowFrom` is for DMs, not channels If you see logs like: * `irc: drop group sender alice!ident@host (policy=allowlist)` …it means the sender wasn’t allowed for **group/channel** messages. Fix it by either: * setting `channels.irc.groupAllowFrom` (global for all channels), or * setting per-channel sender allowlists: `channels.irc.groups["#channel"].allowFrom` Example (allow anyone in `#tuirc-dev` to talk to the bot): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { irc: { groupPolicy: "allowlist", groups: { "#tuirc-dev": { allowFrom: ["*"] }, }, }, }, } ``` ## Reply triggering (mentions) Even if a channel is allowed (via `groupPolicy` + `groups`) and the sender is allowed, OpenClaw defaults to **mention-gating** in group contexts. That means you may see logs like `drop channel … (missing-mention)` unless the message includes a mention pattern that matches the bot. To make the bot reply in an IRC channel **without needing a mention**, disable mention gating for that channel: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { irc: { groupPolicy: "allowlist", groups: { "#tuirc-dev": { requireMention: false, allowFrom: ["*"], }, }, }, }, } ``` Or to allow **all** IRC channels (no per-channel allowlist) and still reply without mentions: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { irc: { groupPolicy: "open", groups: { "*": { requireMention: false, allowFrom: ["*"] }, }, }, }, } ``` ## Security note (recommended for public channels) If you allow `allowFrom: ["*"]` in a public channel, anyone can prompt the bot. To reduce risk, restrict tools for that channel. ### Same tools for everyone in the channel ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { irc: { groups: { "#tuirc-dev": { allowFrom: ["*"], tools: { deny: ["group:runtime", "group:fs", "gateway", "nodes", "cron", "browser"], }, }, }, }, }, } ``` ### Different tools per sender (owner gets more power) Use `toolsBySender` to apply a stricter policy to `"*"` and a looser one to your nick: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { irc: { groups: { "#tuirc-dev": { allowFrom: ["*"], toolsBySender: { "*": { deny: ["group:runtime", "group:fs", "gateway", "nodes", "cron", "browser"], }, "id:eigen": { deny: ["gateway", "nodes", "cron"], }, }, }, }, }, }, } ``` Notes: * `toolsBySender` keys should use `id:` for IRC sender identity values: `id:eigen` or `id:eigen!~eigen@174.127.248.171` for stronger matching. * Legacy unprefixed keys are still accepted and matched as `id:` only. * The first matching sender policy wins; `"*"` is the wildcard fallback. For more on group access vs mention-gating (and how they interact), see: [/channels/groups](/channels/groups). ## NickServ To identify with NickServ after connect: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "channels": { "irc": { "nickserv": { "enabled": true, "service": "NickServ", "password": "your-nickserv-password" } } } } ``` Optional one-time registration on connect: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "channels": { "irc": { "nickserv": { "register": true, "registerEmail": "bot@example.com" } } } } ``` Disable `register` after the nick is registered to avoid repeated REGISTER attempts. ## Environment variables Default account supports: * `IRC_HOST` * `IRC_PORT` * `IRC_TLS` * `IRC_NICK` * `IRC_USERNAME` * `IRC_REALNAME` * `IRC_PASSWORD` * `IRC_CHANNELS` (comma-separated) * `IRC_NICKSERV_PASSWORD` * `IRC_NICKSERV_REGISTER_EMAIL` ## Troubleshooting * If the bot connects but never replies in channels, verify `channels.irc.groups` **and** whether mention-gating is dropping messages (`missing-mention`). If you want it to reply without pings, set `requireMention:false` for the channel. * If login fails, verify nick availability and server password. * If TLS fails on a custom network, verify host/port and certificate setup. # LINE Source: https://cryptoclawdocs.termix.ai/channels/line # LINE (plugin) LINE connects to OpenClaw via the LINE Messaging API. The plugin runs as a webhook receiver on the gateway and uses your channel access token + channel secret for authentication. Status: supported via plugin. Direct messages, group chats, media, locations, Flex messages, template messages, and quick replies are supported. Reactions and threads are not supported. ## Plugin required Install the LINE plugin: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install @openclaw/line ``` Local checkout (when running from a git repo): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install ./extensions/line ``` ## Setup 1. Create a LINE Developers account and open the Console: [https://developers.line.biz/console/](https://developers.line.biz/console/) 2. Create (or pick) a Provider and add a **Messaging API** channel. 3. Copy the **Channel access token** and **Channel secret** from the channel settings. 4. Enable **Use webhook** in the Messaging API settings. 5. Set the webhook URL to your gateway endpoint (HTTPS required): ``` https://gateway-host/line/webhook ``` The gateway responds to LINE’s webhook verification (GET) and inbound events (POST). If you need a custom path, set `channels.line.webhookPath` or `channels.line.accounts..webhookPath` and update the URL accordingly. Security note: * LINE signature verification is body-dependent (HMAC over the raw body), so OpenClaw applies strict pre-auth body limits and timeout before verification. ## Configure Minimal config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { line: { enabled: true, channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN", channelSecret: "LINE_CHANNEL_SECRET", dmPolicy: "pairing", }, }, } ``` Env vars (default account only): * `LINE_CHANNEL_ACCESS_TOKEN` * `LINE_CHANNEL_SECRET` Token/secret files: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { line: { tokenFile: "/path/to/line-token.txt", secretFile: "/path/to/line-secret.txt", }, }, } ``` Multiple accounts: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { line: { accounts: { marketing: { channelAccessToken: "...", channelSecret: "...", webhookPath: "/line/marketing", }, }, }, }, } ``` ## Access control Direct messages default to pairing. Unknown senders get a pairing code and their messages are ignored until approved. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw pairing list line openclaw pairing approve line ``` Allowlists and policies: * `channels.line.dmPolicy`: `pairing | allowlist | open | disabled` * `channels.line.allowFrom`: allowlisted LINE user IDs for DMs * `channels.line.groupPolicy`: `allowlist | open | disabled` * `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups * Per-group overrides: `channels.line.groups..allowFrom` * Runtime note: if `channels.line` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set). LINE IDs are case-sensitive. Valid IDs look like: * User: `U` + 32 hex chars * Group: `C` + 32 hex chars * Room: `R` + 32 hex chars ## Message behavior * Text is chunked at 5000 characters. * Markdown formatting is stripped; code blocks and tables are converted into Flex cards when possible. * Streaming responses are buffered; LINE receives full chunks with a loading animation while the agent works. * Media downloads are capped by `channels.line.mediaMaxMb` (default 10). ## Channel data (rich messages) Use `channelData.line` to send quick replies, locations, Flex cards, or template messages. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { text: "Here you go", channelData: { line: { quickReplies: ["Status", "Help"], location: { title: "Office", address: "123 Main St", latitude: 35.681236, longitude: 139.767125, }, flexMessage: { altText: "Status card", contents: { /* Flex payload */ }, }, templateMessage: { type: "confirm", text: "Proceed?", confirmLabel: "Yes", confirmData: "yes", cancelLabel: "No", cancelData: "no", }, }, }, } ``` The LINE plugin also ships a `/card` command for Flex message presets: ``` /card info "Welcome" "Thanks for joining!" ``` ## Troubleshooting * **Webhook verification fails:** ensure the webhook URL is HTTPS and the `channelSecret` matches the LINE console. * **No inbound events:** confirm the webhook path matches `channels.line.webhookPath` and that the gateway is reachable from LINE. * **Media download errors:** raise `channels.line.mediaMaxMb` if media exceeds the default limit. # Channel Location Parsing Source: https://cryptoclawdocs.termix.ai/channels/location # Channel location parsing OpenClaw normalizes shared locations from chat channels into: * human-readable text appended to the inbound body, and * structured fields in the auto-reply context payload. Currently supported: * **Telegram** (location pins + venues + live locations) * **WhatsApp** (locationMessage + liveLocationMessage) * **Matrix** (`m.location` with `geo_uri`) ## Text formatting Locations are rendered as friendly lines without brackets: * Pin: * `📍 48.858844, 2.294351 ±12m` * Named place: * `📍 Eiffel Tower — Champ de Mars, Paris (48.858844, 2.294351 ±12m)` * Live share: * `🛰 Live location: 48.858844, 2.294351 ±12m` If the channel includes a caption/comment, it is appended on the next line: ``` 📍 48.858844, 2.294351 ±12m Meet here ``` ## Context fields When a location is present, these fields are added to `ctx`: * `LocationLat` (number) * `LocationLon` (number) * `LocationAccuracy` (number, meters; optional) * `LocationName` (string; optional) * `LocationAddress` (string; optional) * `LocationSource` (`pin | place | live`) * `LocationIsLive` (boolean) ## Channel notes * **Telegram**: venues map to `LocationName/LocationAddress`; live locations use `live_period`. * **WhatsApp**: `locationMessage.comment` and `liveLocationMessage.caption` are appended as the caption line. * **Matrix**: `geo_uri` is parsed as a pin location; altitude is ignored and `LocationIsLive` is always false. # Matrix Source: https://cryptoclawdocs.termix.ai/channels/matrix # Matrix (plugin) Matrix is an open, decentralized messaging protocol. OpenClaw connects as a Matrix **user** on any homeserver, so you need a Matrix account for the bot. Once it is logged in, you can DM the bot directly or invite it to rooms (Matrix "groups"). Beeper is a valid client option too, but it requires E2EE to be enabled. Status: supported via plugin (@vector-im/matrix-bot-sdk). Direct messages, rooms, threads, media, reactions, polls (send + poll-start as text), location, and E2EE (with crypto support). ## Plugin required Matrix ships as a plugin and is not bundled with the core install. Install via CLI (npm registry): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install @openclaw/matrix ``` Local checkout (when running from a git repo): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install ./extensions/matrix ``` If you choose Matrix during configure/onboarding and a git checkout is detected, OpenClaw will offer the local install path automatically. Details: [Plugins](/tools/plugin) ## Setup 1. Install the Matrix plugin: * From npm: `openclaw plugins install @openclaw/matrix` * From a local checkout: `openclaw plugins install ./extensions/matrix` 2. Create a Matrix account on a homeserver: * Browse hosting options at [https://matrix.org/ecosystem/hosting/](https://matrix.org/ecosystem/hosting/) * Or host it yourself. 3. Get an access token for the bot account: * Use the Matrix login API with `curl` at your home server: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl --request POST \ --url https://matrix.example.org/_matrix/client/v3/login \ --header 'Content-Type: application/json' \ --data '{ "type": "m.login.password", "identifier": { "type": "m.id.user", "user": "your-user-name" }, "password": "your-password" }' ``` * Replace `matrix.example.org` with your homeserver URL. * Or set `channels.matrix.userId` + `channels.matrix.password`: OpenClaw calls the same login endpoint, stores the access token in `~/.openclaw/credentials/matrix/credentials.json`, and reuses it on next start. 4. Configure credentials: * Env: `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN` (or `MATRIX_USER_ID` + `MATRIX_PASSWORD`) * Or config: `channels.matrix.*` * If both are set, config takes precedence. * With access token: user ID is fetched automatically via `/whoami`. * When set, `channels.matrix.userId` should be the full Matrix ID (example: `@bot:example.org`). 5. Restart the gateway (or finish onboarding). 6. Start a DM with the bot or invite it to a room from any Matrix client (Element, Beeper, etc.; see [https://matrix.org/ecosystem/clients/](https://matrix.org/ecosystem/clients/)). Beeper requires E2EE, so set `channels.matrix.encryption: true` and verify the device. Minimal config (access token, user ID auto-fetched): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { matrix: { enabled: true, homeserver: "https://matrix.example.org", accessToken: "syt_***", dm: { policy: "pairing" }, }, }, } ``` E2EE config (end to end encryption enabled): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { matrix: { enabled: true, homeserver: "https://matrix.example.org", accessToken: "syt_***", encryption: true, dm: { policy: "pairing" }, }, }, } ``` ## Encryption (E2EE) End-to-end encryption is **supported** via the Rust crypto SDK. Enable with `channels.matrix.encryption: true`: * If the crypto module loads, encrypted rooms are decrypted automatically. * Outbound media is encrypted when sending to encrypted rooms. * On first connection, OpenClaw requests device verification from your other sessions. * Verify the device in another Matrix client (Element, etc.) to enable key sharing. * If the crypto module cannot be loaded, E2EE is disabled and encrypted rooms will not decrypt; OpenClaw logs a warning. * If you see missing crypto module errors (for example, `@matrix-org/matrix-sdk-crypto-nodejs-*`), allow build scripts for `@matrix-org/matrix-sdk-crypto-nodejs` and run `pnpm rebuild @matrix-org/matrix-sdk-crypto-nodejs` or fetch the binary with `node node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js`. Crypto state is stored per account + access token in `~/.openclaw/matrix/accounts//__//crypto/` (SQLite database). Sync state lives alongside it in `bot-storage.json`. If the access token (device) changes, a new store is created and the bot must be re-verified for encrypted rooms. **Device verification:** When E2EE is enabled, the bot will request verification from your other sessions on startup. Open Element (or another client) and approve the verification request to establish trust. Once verified, the bot can decrypt messages in encrypted rooms. ## Multi-account Multi-account support: use `channels.matrix.accounts` with per-account credentials and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. Each account runs as a separate Matrix user on any homeserver. Per-account config inherits from the top-level `channels.matrix` settings and can override any option (DM policy, groups, encryption, etc.). ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { matrix: { enabled: true, dm: { policy: "pairing" }, accounts: { assistant: { name: "Main assistant", homeserver: "https://matrix.example.org", accessToken: "syt_assistant_***", encryption: true, }, alerts: { name: "Alerts bot", homeserver: "https://matrix.example.org", accessToken: "syt_alerts_***", dm: { policy: "allowlist", allowFrom: ["@admin:example.org"] }, }, }, }, }, } ``` Notes: * Account startup is serialized to avoid race conditions with concurrent module imports. * Env variables (`MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN`, etc.) only apply to the **default** account. * Base channel settings (DM policy, group policy, mention gating, etc.) apply to all accounts unless overridden per account. * Use `bindings[].match.accountId` to route each account to a different agent. * Crypto state is stored per account + access token (separate key stores per account). ## Routing model * Replies always go back to Matrix. * DMs share the agent's main session; rooms map to group sessions. ## Access control (DMs) * Default: `channels.matrix.dm.policy = "pairing"`. Unknown senders get a pairing code. * Approve via: * `openclaw pairing list matrix` * `openclaw pairing approve matrix ` * Public DMs: `channels.matrix.dm.policy="open"` plus `channels.matrix.dm.allowFrom=["*"]`. * `channels.matrix.dm.allowFrom` accepts full Matrix user IDs (example: `@user:server`). The wizard resolves display names to user IDs when directory search finds a single exact match. * Do not use display names or bare localparts (example: `"Alice"` or `"alice"`). They are ambiguous and are ignored for allowlist matching. Use full `@user:server` IDs. ## Rooms (groups) * Default: `channels.matrix.groupPolicy = "allowlist"` (mention-gated). Use `channels.defaults.groupPolicy` to override the default when unset. * Runtime note: if `channels.matrix` is completely missing, runtime falls back to `groupPolicy="allowlist"` for room checks (even if `channels.defaults.groupPolicy` is set). * Allowlist rooms with `channels.matrix.groups` (room IDs or aliases; names are resolved to IDs when directory search finds a single exact match): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { matrix: { groupPolicy: "allowlist", groups: { "!roomId:example.org": { allow: true }, "#alias:example.org": { allow: true }, }, groupAllowFrom: ["@owner:example.org"], }, }, } ``` * `requireMention: false` enables auto-reply in that room. * `groups."*"` can set defaults for mention gating across rooms. * `groupAllowFrom` restricts which senders can trigger the bot in rooms (full Matrix user IDs). * Per-room `users` allowlists can further restrict senders inside a specific room (use full Matrix user IDs). * The configure wizard prompts for room allowlists (room IDs, aliases, or names) and resolves names only on an exact, unique match. * On startup, OpenClaw resolves room/user names in allowlists to IDs and logs the mapping; unresolved entries are ignored for allowlist matching. * Invites are auto-joined by default; control with `channels.matrix.autoJoin` and `channels.matrix.autoJoinAllowlist`. * To allow **no rooms**, set `channels.matrix.groupPolicy: "disabled"` (or keep an empty allowlist). * Legacy key: `channels.matrix.rooms` (same shape as `groups`). ## Threads * Reply threading is supported. * `channels.matrix.threadReplies` controls whether replies stay in threads: * `off`, `inbound` (default), `always` * `channels.matrix.replyToMode` controls reply-to metadata when not replying in a thread: * `off` (default), `first`, `all` ## Capabilities | Feature | Status | | --------------- | ------------------------------------------------------------------------------------ | | Direct messages | ✅ Supported | | Rooms | ✅ Supported | | Threads | ✅ Supported | | Media | ✅ Supported | | E2EE | ✅ Supported (crypto module required) | | Reactions | ✅ Supported (send/read via tools) | | Polls | ✅ Send supported; inbound poll starts are converted to text (responses/ends ignored) | | Location | ✅ Supported (geo URI; altitude ignored) | | Native commands | ✅ Supported | ## Troubleshooting Run this ladder first: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw status openclaw gateway status openclaw logs --follow openclaw doctor openclaw channels status --probe ``` Then confirm DM pairing state if needed: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw pairing list matrix ``` Common failures: * Logged in but room messages ignored: room blocked by `groupPolicy` or room allowlist. * DMs ignored: sender pending approval when `channels.matrix.dm.policy="pairing"`. * Encrypted rooms fail: crypto support or encryption settings mismatch. For triage flow: [/channels/troubleshooting](/channels/troubleshooting). ## Configuration reference (Matrix) Full configuration: [Configuration](/gateway/configuration) Provider options: * `channels.matrix.enabled`: enable/disable channel startup. * `channels.matrix.homeserver`: homeserver URL. * `channels.matrix.userId`: Matrix user ID (optional with access token). * `channels.matrix.accessToken`: access token. * `channels.matrix.password`: password for login (token stored). * `channels.matrix.deviceName`: device display name. * `channels.matrix.encryption`: enable E2EE (default: false). * `channels.matrix.initialSyncLimit`: initial sync limit. * `channels.matrix.threadReplies`: `off | inbound | always` (default: inbound). * `channels.matrix.textChunkLimit`: outbound text chunk size (chars). * `channels.matrix.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. * `channels.matrix.dm.policy`: `pairing | allowlist | open | disabled` (default: pairing). * `channels.matrix.dm.allowFrom`: DM allowlist (full Matrix user IDs). `open` requires `"*"`. The wizard resolves names to IDs when possible. * `channels.matrix.groupPolicy`: `allowlist | open | disabled` (default: allowlist). * `channels.matrix.groupAllowFrom`: allowlisted senders for group messages (full Matrix user IDs). * `channels.matrix.allowlistOnly`: force allowlist rules for DMs + rooms. * `channels.matrix.groups`: group allowlist + per-room settings map. * `channels.matrix.rooms`: legacy group allowlist/config. * `channels.matrix.replyToMode`: reply-to mode for threads/tags. * `channels.matrix.mediaMaxMb`: inbound/outbound media cap (MB). * `channels.matrix.autoJoin`: invite handling (`always | allowlist | off`, default: always). * `channels.matrix.autoJoinAllowlist`: allowed room IDs/aliases for auto-join. * `channels.matrix.accounts`: multi-account configuration keyed by account ID (each account inherits top-level settings). * `channels.matrix.actions`: per-action tool gating (reactions/messages/pins/memberInfo/channelInfo). # Mattermost Source: https://cryptoclawdocs.termix.ai/channels/mattermost # Mattermost (plugin) Status: supported via plugin (bot token + WebSocket events). Channels, groups, and DMs are supported. Mattermost is a self-hostable team messaging platform; see the official site at [mattermost.com](https://mattermost.com) for product details and downloads. ## Plugin required Mattermost ships as a plugin and is not bundled with the core install. Install via CLI (npm registry): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install @openclaw/mattermost ``` Local checkout (when running from a git repo): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install ./extensions/mattermost ``` If you choose Mattermost during configure/onboarding and a git checkout is detected, OpenClaw will offer the local install path automatically. Details: [Plugins](/tools/plugin) ## Quick setup 1. Install the Mattermost plugin. 2. Create a Mattermost bot account and copy the **bot token**. 3. Copy the Mattermost **base URL** (e.g., `https://chat.example.com`). 4. Configure OpenClaw and start the gateway. Minimal config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { mattermost: { enabled: true, botToken: "mm-token", baseUrl: "https://chat.example.com", dmPolicy: "pairing", }, }, } ``` ## Native slash commands Native slash commands are opt-in. When enabled, OpenClaw registers `oc_*` slash commands via the Mattermost API and receives callback POSTs on the gateway HTTP server. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { mattermost: { commands: { native: true, nativeSkills: true, callbackPath: "/api/channels/mattermost/command", // Use when Mattermost cannot reach the gateway directly (reverse proxy/public URL). callbackUrl: "https://gateway.example.com/api/channels/mattermost/command", }, }, }, } ``` Notes: * `native: "auto"` defaults to disabled for Mattermost. Set `native: true` to enable. * If `callbackUrl` is omitted, OpenClaw derives one from gateway host/port + `callbackPath`. * For multi-account setups, `commands` can be set at the top level or under `channels.mattermost.accounts..commands` (account values override top-level fields). * Command callbacks are validated with per-command tokens and fail closed when token checks fail. * Reachability requirement: the callback endpoint must be reachable from the Mattermost server. * Do not set `callbackUrl` to `localhost` unless Mattermost runs on the same host/network namespace as OpenClaw. * Do not set `callbackUrl` to your Mattermost base URL unless that URL reverse-proxies `/api/channels/mattermost/command` to OpenClaw. * A quick check is `curl https:///api/channels/mattermost/command`; a GET should return `405 Method Not Allowed` from OpenClaw, not `404`. * Mattermost egress allowlist requirement: * If your callback targets private/tailnet/internal addresses, set Mattermost `ServiceSettings.AllowedUntrustedInternalConnections` to include the callback host/domain. * Use host/domain entries, not full URLs. * Good: `gateway.tailnet-name.ts.net` * Bad: `https://gateway.tailnet-name.ts.net` ## Environment variables (default account) Set these on the gateway host if you prefer env vars: * `MATTERMOST_BOT_TOKEN=...` * `MATTERMOST_URL=https://chat.example.com` Env vars apply only to the **default** account (`default`). Other accounts must use config values. ## Chat modes Mattermost responds to DMs automatically. Channel behavior is controlled by `chatmode`: * `oncall` (default): respond only when @mentioned in channels. * `onmessage`: respond to every channel message. * `onchar`: respond when a message starts with a trigger prefix. Config example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { mattermost: { chatmode: "onchar", oncharPrefixes: [">", "!"], }, }, } ``` Notes: * `onchar` still responds to explicit @mentions. * `channels.mattermost.requireMention` is honored for legacy configs but `chatmode` is preferred. ## Access control (DMs) * Default: `channels.mattermost.dmPolicy = "pairing"` (unknown senders get a pairing code). * Approve via: * `openclaw pairing list mattermost` * `openclaw pairing approve mattermost ` * Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]`. ## Channels (groups) * Default: `channels.mattermost.groupPolicy = "allowlist"` (mention-gated). * Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs recommended). * `@username` matching is mutable and only enabled when `channels.mattermost.dangerouslyAllowNameMatching: true`. * Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated). * Runtime note: if `channels.mattermost` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set). ## Targets for outbound delivery Use these target formats with `openclaw message send` or cron/webhooks: * `channel:` for a channel * `user:` for a DM * `@username` for a DM (resolved via the Mattermost API) Bare IDs are treated as channels. ## Reactions (message tool) * Use `message action=react` with `channel=mattermost`. * `messageId` is the Mattermost post id. * `emoji` accepts names like `thumbsup` or `:+1:` (colons are optional). * Set `remove=true` (boolean) to remove a reaction. * Reaction add/remove events are forwarded as system events to the routed agent session. Examples: ``` message action=react channel=mattermost target=channel: messageId= emoji=thumbsup message action=react channel=mattermost target=channel: messageId= emoji=thumbsup remove=true ``` Config: * `channels.mattermost.actions.reactions`: enable/disable reaction actions (default true). * Per-account override: `channels.mattermost.accounts..actions.reactions`. ## Multi-account Mattermost supports multiple accounts under `channels.mattermost.accounts`: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { mattermost: { accounts: { default: { name: "Primary", botToken: "mm-token", baseUrl: "https://chat.example.com" }, alerts: { name: "Alerts", botToken: "mm-token-2", baseUrl: "https://alerts.example.com" }, }, }, }, } ``` ## Troubleshooting * No replies in channels: ensure the bot is in the channel and mention it (oncall), use a trigger prefix (onchar), or set `chatmode: "onmessage"`. * Auth errors: check the bot token, base URL, and whether the account is enabled. * Multi-account issues: env vars only apply to the `default` account. # Microsoft Teams Source: https://cryptoclawdocs.termix.ai/channels/msteams # Microsoft Teams (plugin) > "Abandon all hope, ye who enter here." Updated: 2026-01-21 Status: text + DM attachments are supported; channel/group file sending requires `sharePointSiteId` + Graph permissions (see [Sending files in group chats](#sending-files-in-group-chats)). Polls are sent via Adaptive Cards. ## Plugin required Microsoft Teams ships as a plugin and is not bundled with the core install. **Breaking change (2026.1.15):** MS Teams moved out of core. If you use it, you must install the plugin. Explainable: keeps core installs lighter and lets MS Teams dependencies update independently. Install via CLI (npm registry): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install @openclaw/msteams ``` Local checkout (when running from a git repo): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install ./extensions/msteams ``` If you choose Teams during configure/onboarding and a git checkout is detected, OpenClaw will offer the local install path automatically. Details: [Plugins](/tools/plugin) ## Quick setup (beginner) 1. Install the Microsoft Teams plugin. 2. Create an **Azure Bot** (App ID + client secret + tenant ID). 3. Configure OpenClaw with those credentials. 4. Expose `/api/messages` (port 3978 by default) via a public URL or tunnel. 5. Install the Teams app package and start the gateway. Minimal config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { msteams: { enabled: true, appId: "", appPassword: "", tenantId: "", webhook: { port: 3978, path: "/api/messages" }, }, }, } ``` Note: group chats are blocked by default (`channels.msteams.groupPolicy: "allowlist"`). To allow group replies, set `channels.msteams.groupAllowFrom` (or use `groupPolicy: "open"` to allow any member, mention-gated). ## Goals * Talk to OpenClaw via Teams DMs, group chats, or channels. * Keep routing deterministic: replies always go back to the channel they arrived on. * Default to safe channel behavior (mentions required unless configured otherwise). ## Config writes By default, Microsoft Teams is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`). Disable with: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { msteams: { configWrites: false } }, } ``` ## Access control (DMs + groups) **DM access** * Default: `channels.msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved. * `channels.msteams.allowFrom` should use stable AAD object IDs. * UPNs/display names are mutable; direct matching is disabled by default and only enabled with `channels.msteams.dangerouslyAllowNameMatching: true`. * The wizard can resolve names to IDs via Microsoft Graph when credentials allow. **Group access** * Default: `channels.msteams.groupPolicy = "allowlist"` (blocked unless you add `groupAllowFrom`). Use `channels.defaults.groupPolicy` to override the default when unset. * `channels.msteams.groupAllowFrom` controls which senders can trigger in group chats/channels (falls back to `channels.msteams.allowFrom`). * Set `groupPolicy: "open"` to allow any member (still mention‑gated by default). * To allow **no channels**, set `channels.msteams.groupPolicy: "disabled"`. Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { msteams: { groupPolicy: "allowlist", groupAllowFrom: ["user@org.com"], }, }, } ``` **Teams + channel allowlist** * Scope group/channel replies by listing teams and channels under `channels.msteams.teams`. * Keys can be team IDs or names; channel keys can be conversation IDs or names. * When `groupPolicy="allowlist"` and a teams allowlist is present, only listed teams/channels are accepted (mention‑gated). * The configure wizard accepts `Team/Channel` entries and stores them for you. * On startup, OpenClaw resolves team/channel and user allowlist names to IDs (when Graph permissions allow) and logs the mapping; unresolved entries are kept as typed. Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { msteams: { groupPolicy: "allowlist", teams: { "My Team": { channels: { General: { requireMention: true }, }, }, }, }, }, } ``` ## How it works 1. Install the Microsoft Teams plugin. 2. Create an **Azure Bot** (App ID + secret + tenant ID). 3. Build a **Teams app package** that references the bot and includes the RSC permissions below. 4. Upload/install the Teams app into a team (or personal scope for DMs). 5. Configure `msteams` in `~/.openclaw/openclaw.json` (or env vars) and start the gateway. 6. The gateway listens for Bot Framework webhook traffic on `/api/messages` by default. ## Azure Bot Setup (Prerequisites) Before configuring OpenClaw, you need to create an Azure Bot resource. ### Step 1: Create Azure Bot 1. Go to [Create Azure Bot](https://portal.azure.com/#create/Microsoft.AzureBot) 2. Fill in the **Basics** tab: | Field | Value | | ------------------ | -------------------------------------------------------- | | **Bot handle** | Your bot name, e.g., `openclaw-msteams` (must be unique) | | **Subscription** | Select your Azure subscription | | **Resource group** | Create new or use existing | | **Pricing tier** | **Free** for dev/testing | | **Type of App** | **Single Tenant** (recommended - see note below) | | **Creation type** | **Create new Microsoft App ID** | > **Deprecation notice:** Creation of new multi-tenant bots was deprecated after 2025-07-31. Use **Single Tenant** for new bots. 3. Click **Review + create** → **Create** (wait \~1-2 minutes) ### Step 2: Get Credentials 1. Go to your Azure Bot resource → **Configuration** 2. Copy **Microsoft App ID** → this is your `appId` 3. Click **Manage Password** → go to the App Registration 4. Under **Certificates & secrets** → **New client secret** → copy the **Value** → this is your `appPassword` 5. Go to **Overview** → copy **Directory (tenant) ID** → this is your `tenantId` ### Step 3: Configure Messaging Endpoint 1. In Azure Bot → **Configuration** 2. Set **Messaging endpoint** to your webhook URL: * Production: `https://your-domain.com/api/messages` * Local dev: Use a tunnel (see [Local Development](#local-development-tunneling) below) ### Step 4: Enable Teams Channel 1. In Azure Bot → **Channels** 2. Click **Microsoft Teams** → Configure → Save 3. Accept the Terms of Service ## Local Development (Tunneling) Teams can't reach `localhost`. Use a tunnel for local development: **Option A: ngrok** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ngrok http 3978 # Copy the https URL, e.g., https://abc123.ngrok.io # Set messaging endpoint to: https://abc123.ngrok.io/api/messages ``` **Option B: Tailscale Funnel** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} tailscale funnel 3978 # Use your Tailscale funnel URL as the messaging endpoint ``` ## Teams Developer Portal (Alternative) Instead of manually creating a manifest ZIP, you can use the [Teams Developer Portal](https://dev.teams.microsoft.com/apps): 1. Click **+ New app** 2. Fill in basic info (name, description, developer info) 3. Go to **App features** → **Bot** 4. Select **Enter a bot ID manually** and paste your Azure Bot App ID 5. Check scopes: **Personal**, **Team**, **Group Chat** 6. Click **Distribute** → **Download app package** 7. In Teams: **Apps** → **Manage your apps** → **Upload a custom app** → select the ZIP This is often easier than hand-editing JSON manifests. ## Testing the Bot **Option A: Azure Web Chat (verify webhook first)** 1. In Azure Portal → your Azure Bot resource → **Test in Web Chat** 2. Send a message - you should see a response 3. This confirms your webhook endpoint works before Teams setup **Option B: Teams (after app installation)** 1. Install the Teams app (sideload or org catalog) 2. Find the bot in Teams and send a DM 3. Check gateway logs for incoming activity ## Setup (minimal text-only) 1. **Install the Microsoft Teams plugin** * From npm: `openclaw plugins install @openclaw/msteams` * From a local checkout: `openclaw plugins install ./extensions/msteams` 2. **Bot registration** * Create an Azure Bot (see above) and note: * App ID * Client secret (App password) * Tenant ID (single-tenant) 3. **Teams app manifest** * Include a `bot` entry with `botId = `. * Scopes: `personal`, `team`, `groupChat`. * `supportsFiles: true` (required for personal scope file handling). * Add RSC permissions (below). * Create icons: `outline.png` (32x32) and `color.png` (192x192). * Zip all three files together: `manifest.json`, `outline.png`, `color.png`. 4. **Configure OpenClaw** ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "msteams": { "enabled": true, "appId": "", "appPassword": "", "tenantId": "", "webhook": { "port": 3978, "path": "/api/messages" } } } ``` You can also use environment variables instead of config keys: * `MSTEAMS_APP_ID` * `MSTEAMS_APP_PASSWORD` * `MSTEAMS_TENANT_ID` 5. **Bot endpoint** * Set the Azure Bot Messaging Endpoint to: * `https://:3978/api/messages` (or your chosen path/port). 6. **Run the gateway** * The Teams channel starts automatically when the plugin is installed and `msteams` config exists with credentials. ## History context * `channels.msteams.historyLimit` controls how many recent channel/group messages are wrapped into the prompt. * Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). * DM history can be limited with `channels.msteams.dmHistoryLimit` (user turns). Per-user overrides: `channels.msteams.dms[""].historyLimit`. ## Current Teams RSC Permissions (Manifest) These are the **existing resourceSpecific permissions** in our Teams app manifest. They only apply inside the team/chat where the app is installed. **For channels (team scope):** * `ChannelMessage.Read.Group` (Application) - receive all channel messages without @mention * `ChannelMessage.Send.Group` (Application) * `Member.Read.Group` (Application) * `Owner.Read.Group` (Application) * `ChannelSettings.Read.Group` (Application) * `TeamMember.Read.Group` (Application) * `TeamSettings.Read.Group` (Application) **For group chats:** * `ChatMessage.Read.Chat` (Application) - receive all group chat messages without @mention ## Example Teams Manifest (redacted) Minimal, valid example with the required fields. Replace IDs and URLs. ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json", "manifestVersion": "1.23", "version": "1.0.0", "id": "00000000-0000-0000-0000-000000000000", "name": { "short": "OpenClaw" }, "developer": { "name": "Your Org", "websiteUrl": "https://example.com", "privacyUrl": "https://example.com/privacy", "termsOfUseUrl": "https://example.com/terms" }, "description": { "short": "OpenClaw in Teams", "full": "OpenClaw in Teams" }, "icons": { "outline": "outline.png", "color": "color.png" }, "accentColor": "#5B6DEF", "bots": [ { "botId": "11111111-1111-1111-1111-111111111111", "scopes": ["personal", "team", "groupChat"], "isNotificationOnly": false, "supportsCalling": false, "supportsVideo": false, "supportsFiles": true } ], "webApplicationInfo": { "id": "11111111-1111-1111-1111-111111111111" }, "authorization": { "permissions": { "resourceSpecific": [ { "name": "ChannelMessage.Read.Group", "type": "Application" }, { "name": "ChannelMessage.Send.Group", "type": "Application" }, { "name": "Member.Read.Group", "type": "Application" }, { "name": "Owner.Read.Group", "type": "Application" }, { "name": "ChannelSettings.Read.Group", "type": "Application" }, { "name": "TeamMember.Read.Group", "type": "Application" }, { "name": "TeamSettings.Read.Group", "type": "Application" }, { "name": "ChatMessage.Read.Chat", "type": "Application" } ] } } } ``` ### Manifest caveats (must-have fields) * `bots[].botId` **must** match the Azure Bot App ID. * `webApplicationInfo.id` **must** match the Azure Bot App ID. * `bots[].scopes` must include the surfaces you plan to use (`personal`, `team`, `groupChat`). * `bots[].supportsFiles: true` is required for file handling in personal scope. * `authorization.permissions.resourceSpecific` must include channel read/send if you want channel traffic. ### Updating an existing app To update an already-installed Teams app (e.g., to add RSC permissions): 1. Update your `manifest.json` with the new settings 2. **Increment the `version` field** (e.g., `1.0.0` → `1.1.0`) 3. **Re-zip** the manifest with icons (`manifest.json`, `outline.png`, `color.png`) 4. Upload the new zip: * **Option A (Teams Admin Center):** Teams Admin Center → Teams apps → Manage apps → find your app → Upload new version * **Option B (Sideload):** In Teams → Apps → Manage your apps → Upload a custom app 5. **For team channels:** Reinstall the app in each team for new permissions to take effect 6. **Fully quit and relaunch Teams** (not just close the window) to clear cached app metadata ## Capabilities: RSC only vs Graph ### With **Teams RSC only** (app installed, no Graph API permissions) Works: * Read channel message **text** content. * Send channel message **text** content. * Receive **personal (DM)** file attachments. Does NOT work: * Channel/group **image or file contents** (payload only includes HTML stub). * Downloading attachments stored in SharePoint/OneDrive. * Reading message history (beyond the live webhook event). ### With **Teams RSC + Microsoft Graph Application permissions** Adds: * Downloading hosted contents (images pasted into messages). * Downloading file attachments stored in SharePoint/OneDrive. * Reading channel/chat message history via Graph. ### RSC vs Graph API | Capability | RSC Permissions | Graph API | | ----------------------- | -------------------- | ----------------------------------- | | **Real-time messages** | Yes (via webhook) | No (polling only) | | **Historical messages** | No | Yes (can query history) | | **Setup complexity** | App manifest only | Requires admin consent + token flow | | **Works offline** | No (must be running) | Yes (query anytime) | **Bottom line:** RSC is for real-time listening; Graph API is for historical access. For catching up on missed messages while offline, you need Graph API with `ChannelMessage.Read.All` (requires admin consent). ## Graph-enabled media + history (required for channels) If you need images/files in **channels** or want to fetch **message history**, you must enable Microsoft Graph permissions and grant admin consent. 1. In Entra ID (Azure AD) **App Registration**, add Microsoft Graph **Application permissions**: * `ChannelMessage.Read.All` (channel attachments + history) * `Chat.Read.All` or `ChatMessage.Read.All` (group chats) 2. **Grant admin consent** for the tenant. 3. Bump the Teams app **manifest version**, re-upload, and **reinstall the app in Teams**. 4. **Fully quit and relaunch Teams** to clear cached app metadata. **Additional permission for user mentions:** User @mentions work out of the box for users in the conversation. However, if you want to dynamically search and mention users who are **not in the current conversation**, add `User.Read.All` (Application) permission and grant admin consent. ## Known Limitations ### Webhook timeouts Teams delivers messages via HTTP webhook. If processing takes too long (e.g., slow LLM responses), you may see: * Gateway timeouts * Teams retrying the message (causing duplicates) * Dropped replies OpenClaw handles this by returning quickly and sending replies proactively, but very slow responses may still cause issues. ### Formatting Teams markdown is more limited than Slack or Discord: * Basic formatting works: **bold**, *italic*, `code`, links * Complex markdown (tables, nested lists) may not render correctly * Adaptive Cards are supported for polls and arbitrary card sends (see below) ## Configuration Key settings (see `/gateway/configuration` for shared channel patterns): * `channels.msteams.enabled`: enable/disable the channel. * `channels.msteams.appId`, `channels.msteams.appPassword`, `channels.msteams.tenantId`: bot credentials. * `channels.msteams.webhook.port` (default `3978`) * `channels.msteams.webhook.path` (default `/api/messages`) * `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing) * `channels.msteams.allowFrom`: DM allowlist (AAD object IDs recommended). The wizard resolves names to IDs during setup when Graph access is available. * `channels.msteams.dangerouslyAllowNameMatching`: break-glass toggle to re-enable mutable UPN/display-name matching. * `channels.msteams.textChunkLimit`: outbound text chunk size. * `channels.msteams.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. * `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains). * `channels.msteams.mediaAuthAllowHosts`: allowlist for attaching Authorization headers on media retries (defaults to Graph + Bot Framework hosts). * `channels.msteams.requireMention`: require @mention in channels/groups (default true). * `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)). * `channels.msteams.teams..replyStyle`: per-team override. * `channels.msteams.teams..requireMention`: per-team override. * `channels.msteams.teams..tools`: default per-team tool policy overrides (`allow`/`deny`/`alsoAllow`) used when a channel override is missing. * `channels.msteams.teams..toolsBySender`: default per-team per-sender tool policy overrides (`"*"` wildcard supported). * `channels.msteams.teams..channels..replyStyle`: per-channel override. * `channels.msteams.teams..channels..requireMention`: per-channel override. * `channels.msteams.teams..channels..tools`: per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`). * `channels.msteams.teams..channels..toolsBySender`: per-channel per-sender tool policy overrides (`"*"` wildcard supported). * `toolsBySender` keys should use explicit prefixes: `id:`, `e164:`, `username:`, `name:` (legacy unprefixed keys still map to `id:` only). * `channels.msteams.sharePointSiteId`: SharePoint site ID for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats)). ## Routing & Sessions * Session keys follow the standard agent format (see [/concepts/session](/concepts/session)): * Direct messages share the main session (`agent::`). * Channel/group messages use conversation id: * `agent::msteams:channel:` * `agent::msteams:group:` ## Reply Style: Threads vs Posts Teams recently introduced two channel UI styles over the same underlying data model: | Style | Description | Recommended `replyStyle` | | ------------------------ | --------------------------------------------------------- | ------------------------ | | **Posts** (classic) | Messages appear as cards with threaded replies underneath | `thread` (default) | | **Threads** (Slack-like) | Messages flow linearly, more like Slack | `top-level` | **The problem:** The Teams API does not expose which UI style a channel uses. If you use the wrong `replyStyle`: * `thread` in a Threads-style channel → replies appear nested awkwardly * `top-level` in a Posts-style channel → replies appear as separate top-level posts instead of in-thread **Solution:** Configure `replyStyle` per-channel based on how the channel is set up: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "msteams": { "replyStyle": "thread", "teams": { "19:abc...@thread.tacv2": { "channels": { "19:xyz...@thread.tacv2": { "replyStyle": "top-level" } } } } } } ``` ## Attachments & Images **Current limitations:** * **DMs:** Images and file attachments work via Teams bot file APIs. * **Channels/groups:** Attachments live in M365 storage (SharePoint/OneDrive). The webhook payload only includes an HTML stub, not the actual file bytes. **Graph API permissions are required** to download channel attachments. Without Graph permissions, channel messages with images will be received as text-only (the image content is not accessible to the bot). By default, OpenClaw only downloads media from Microsoft/Teams hostnames. Override with `channels.msteams.mediaAllowHosts` (use `["*"]` to allow any host). Authorization headers are only attached for hosts in `channels.msteams.mediaAuthAllowHosts` (defaults to Graph + Bot Framework hosts). Keep this list strict (avoid multi-tenant suffixes). ## Sending files in group chats Bots can send files in DMs using the FileConsentCard flow (built-in). However, **sending files in group chats/channels** requires additional setup: | Context | How files are sent | Setup needed | | ------------------------ | -------------------------------------------- | ----------------------------------------------- | | **DMs** | FileConsentCard → user accepts → bot uploads | Works out of the box | | **Group chats/channels** | Upload to SharePoint → share link | Requires `sharePointSiteId` + Graph permissions | | **Images (any context)** | Base64-encoded inline | Works out of the box | ### Why group chats need SharePoint Bots don't have a personal OneDrive drive (the `/me/drive` Graph API endpoint doesn't work for application identities). To send files in group chats/channels, the bot uploads to a **SharePoint site** and creates a sharing link. ### Setup 1. **Add Graph API permissions** in Entra ID (Azure AD) → App Registration: * `Sites.ReadWrite.All` (Application) - upload files to SharePoint * `Chat.Read.All` (Application) - optional, enables per-user sharing links 2. **Grant admin consent** for the tenant. 3. **Get your SharePoint site ID:** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Via Graph Explorer or curl with a valid token: curl -H "Authorization: Bearer $TOKEN" \ "https://graph.microsoft.com/v1.0/sites/{hostname}:/{site-path}" # Example: for a site at "contoso.sharepoint.com/sites/BotFiles" curl -H "Authorization: Bearer $TOKEN" \ "https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/BotFiles" # Response includes: "id": "contoso.sharepoint.com,guid1,guid2" ``` 4. **Configure OpenClaw:** ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { msteams: { // ... other config ... sharePointSiteId: "contoso.sharepoint.com,guid1,guid2", }, }, } ``` ### Sharing behavior | Permission | Sharing behavior | | --------------------------------------- | --------------------------------------------------------- | | `Sites.ReadWrite.All` only | Organization-wide sharing link (anyone in org can access) | | `Sites.ReadWrite.All` + `Chat.Read.All` | Per-user sharing link (only chat members can access) | Per-user sharing is more secure as only the chat participants can access the file. If `Chat.Read.All` permission is missing, the bot falls back to organization-wide sharing. ### Fallback behavior | Scenario | Result | | ------------------------------------------------- | -------------------------------------------------- | | Group chat + file + `sharePointSiteId` configured | Upload to SharePoint, send sharing link | | Group chat + file + no `sharePointSiteId` | Attempt OneDrive upload (may fail), send text only | | Personal chat + file | FileConsentCard flow (works without SharePoint) | | Any context + image | Base64-encoded inline (works without SharePoint) | ### Files stored location Uploaded files are stored in a `/OpenClawShared/` folder in the configured SharePoint site's default document library. ## Polls (Adaptive Cards) OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API). * CLI: `openclaw message poll --channel msteams --target conversation: ...` * Votes are recorded by the gateway in `~/.openclaw/msteams-polls.json`. * The gateway must stay online to record votes. * Polls do not auto-post result summaries yet (inspect the store file if needed). ## Adaptive Cards (arbitrary) Send any Adaptive Card JSON to Teams users or conversations using the `message` tool or CLI. The `card` parameter accepts an Adaptive Card JSON object. When `card` is provided, the message text is optional. **Agent tool:** ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "action": "send", "channel": "msteams", "target": "user:", "card": { "type": "AdaptiveCard", "version": "1.5", "body": [{ "type": "TextBlock", "text": "Hello!" }] } } ``` **CLI:** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw message send --channel msteams \ --target "conversation:19:abc...@thread.tacv2" \ --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello!"}]}' ``` See [Adaptive Cards documentation](https://adaptivecards.io/) for card schema and examples. For target format details, see [Target formats](#target-formats) below. ## Target formats MSTeams targets use prefixes to distinguish between users and conversations: | Target type | Format | Example | | ------------------- | -------------------------------- | --------------------------------------------------- | | User (by ID) | `user:` | `user:40a1a0ed-4ff2-4164-a219-55518990c197` | | User (by name) | `user:` | `user:John Smith` (requires Graph API) | | Group/channel | `conversation:` | `conversation:19:abc123...@thread.tacv2` | | Group/channel (raw) | `` | `19:abc123...@thread.tacv2` (if contains `@thread`) | **CLI examples:** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Send to a user by ID openclaw message send --channel msteams --target "user:40a1a0ed-..." --message "Hello" # Send to a user by display name (triggers Graph API lookup) openclaw message send --channel msteams --target "user:John Smith" --message "Hello" # Send to a group chat or channel openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" --message "Hello" # Send an Adaptive Card to a conversation openclaw message send --channel msteams --target "conversation:19:abc...@thread.tacv2" \ --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello"}]}' ``` **Agent tool examples:** ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "action": "send", "channel": "msteams", "target": "user:John Smith", "message": "Hello!" } ``` ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "action": "send", "channel": "msteams", "target": "conversation:19:abc...@thread.tacv2", "card": { "type": "AdaptiveCard", "version": "1.5", "body": [{ "type": "TextBlock", "text": "Hello" }] } } ``` Note: Without the `user:` prefix, names default to group/team resolution. Always use `user:` when targeting people by display name. ## Proactive messaging * Proactive messages are only possible **after** a user has interacted, because we store conversation references at that point. * See `/gateway/configuration` for `dmPolicy` and allowlist gating. ## Team and Channel IDs (Common Gotcha) The `groupId` query parameter in Teams URLs is **NOT** the team ID used for configuration. Extract IDs from the URL path instead: **Team URL:** ``` https://teams.microsoft.com/l/team/19%3ABk4j...%40thread.tacv2/conversations?groupId=... └────────────────────────────┘ Team ID (URL-decode this) ``` **Channel URL:** ``` https://teams.microsoft.com/l/channel/19%3A15bc...%40thread.tacv2/ChannelName?groupId=... └─────────────────────────┘ Channel ID (URL-decode this) ``` **For config:** * Team ID = path segment after `/team/` (URL-decoded, e.g., `19:Bk4j...@thread.tacv2`) * Channel ID = path segment after `/channel/` (URL-decoded) * **Ignore** the `groupId` query parameter ## Private Channels Bots have limited support in private channels: | Feature | Standard Channels | Private Channels | | ---------------------------- | ----------------- | ---------------------- | | Bot installation | Yes | Limited | | Real-time messages (webhook) | Yes | May not work | | RSC permissions | Yes | May behave differently | | @mentions | Yes | If bot is accessible | | Graph API history | Yes | Yes (with permissions) | **Workarounds if private channels don't work:** 1. Use standard channels for bot interactions 2. Use DMs - users can always message the bot directly 3. Use Graph API for historical access (requires `ChannelMessage.Read.All`) ## Troubleshooting ### Common issues * **Images not showing in channels:** Graph permissions or admin consent missing. Reinstall the Teams app and fully quit/reopen Teams. * **No responses in channel:** mentions are required by default; set `channels.msteams.requireMention=false` or configure per team/channel. * **Version mismatch (Teams still shows old manifest):** remove + re-add the app and fully quit Teams to refresh. * **401 Unauthorized from webhook:** Expected when testing manually without Azure JWT - means endpoint is reachable but auth failed. Use Azure Web Chat to test properly. ### Manifest upload errors * **"Icon file cannot be empty":** The manifest references icon files that are 0 bytes. Create valid PNG icons (32x32 for `outline.png`, 192x192 for `color.png`). * **"webApplicationInfo.Id already in use":** The app is still installed in another team/chat. Find and uninstall it first, or wait 5-10 minutes for propagation. * **"Something went wrong" on upload:** Upload via [https://admin.teams.microsoft.com](https://admin.teams.microsoft.com) instead, open browser DevTools (F12) → Network tab, and check the response body for the actual error. * **Sideload failing:** Try "Upload an app to your org's app catalog" instead of "Upload a custom app" - this often bypasses sideload restrictions. ### RSC permissions not working 1. Verify `webApplicationInfo.id` matches your bot's App ID exactly 2. Re-upload the app and reinstall in the team/chat 3. Check if your org admin has blocked RSC permissions 4. Confirm you're using the right scope: `ChannelMessage.Read.Group` for teams, `ChatMessage.Read.Chat` for group chats ## References * [Create Azure Bot](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration) - Azure Bot setup guide * [Teams Developer Portal](https://dev.teams.microsoft.com/apps) - create/manage Teams apps * [Teams app manifest schema](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema) * [Receive channel messages with RSC](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/channel-messages-with-rsc) * [RSC permissions reference](https://learn.microsoft.com/en-us/microsoftteams/platform/graph-api/rsc/resource-specific-consent) * [Teams bot file handling](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4) (channel/group requires Graph) * [Proactive messaging](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages) # Nextcloud Talk Source: https://cryptoclawdocs.termix.ai/channels/nextcloud-talk # Nextcloud Talk (plugin) Status: supported via plugin (webhook bot). Direct messages, rooms, reactions, and markdown messages are supported. ## Plugin required Nextcloud Talk ships as a plugin and is not bundled with the core install. Install via CLI (npm registry): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install @openclaw/nextcloud-talk ``` Local checkout (when running from a git repo): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install ./extensions/nextcloud-talk ``` If you choose Nextcloud Talk during configure/onboarding and a git checkout is detected, OpenClaw will offer the local install path automatically. Details: [Plugins](/tools/plugin) ## Quick setup (beginner) 1. Install the Nextcloud Talk plugin. 2. On your Nextcloud server, create a bot: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ./occ talk:bot:install "OpenClaw" "" "" --feature reaction ``` 3. Enable the bot in the target room settings. 4. Configure OpenClaw: * Config: `channels.nextcloud-talk.baseUrl` + `channels.nextcloud-talk.botSecret` * Or env: `NEXTCLOUD_TALK_BOT_SECRET` (default account only) 5. Restart the gateway (or finish onboarding). Minimal config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { "nextcloud-talk": { enabled: true, baseUrl: "https://cloud.example.com", botSecret: "shared-secret", dmPolicy: "pairing", }, }, } ``` ## Notes * Bots cannot initiate DMs. The user must message the bot first. * Webhook URL must be reachable by the Gateway; set `webhookPublicUrl` if behind a proxy. * Media uploads are not supported by the bot API; media is sent as URLs. * The webhook payload does not distinguish DMs vs rooms; set `apiUser` + `apiPassword` to enable room-type lookups (otherwise DMs are treated as rooms). ## Access control (DMs) * Default: `channels.nextcloud-talk.dmPolicy = "pairing"`. Unknown senders get a pairing code. * Approve via: * `openclaw pairing list nextcloud-talk` * `openclaw pairing approve nextcloud-talk ` * Public DMs: `channels.nextcloud-talk.dmPolicy="open"` plus `channels.nextcloud-talk.allowFrom=["*"]`. * `allowFrom` matches Nextcloud user IDs only; display names are ignored. ## Rooms (groups) * Default: `channels.nextcloud-talk.groupPolicy = "allowlist"` (mention-gated). * Allowlist rooms with `channels.nextcloud-talk.rooms`: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { "nextcloud-talk": { rooms: { "room-token": { requireMention: true }, }, }, }, } ``` * To allow no rooms, keep the allowlist empty or set `channels.nextcloud-talk.groupPolicy="disabled"`. ## Capabilities | Feature | Status | | --------------- | ------------- | | Direct messages | Supported | | Rooms | Supported | | Threads | Not supported | | Media | URL-only | | Reactions | Supported | | Native commands | Not supported | ## Configuration reference (Nextcloud Talk) Full configuration: [Configuration](/gateway/configuration) Provider options: * `channels.nextcloud-talk.enabled`: enable/disable channel startup. * `channels.nextcloud-talk.baseUrl`: Nextcloud instance URL. * `channels.nextcloud-talk.botSecret`: bot shared secret. * `channels.nextcloud-talk.botSecretFile`: secret file path. * `channels.nextcloud-talk.apiUser`: API user for room lookups (DM detection). * `channels.nextcloud-talk.apiPassword`: API/app password for room lookups. * `channels.nextcloud-talk.apiPasswordFile`: API password file path. * `channels.nextcloud-talk.webhookPort`: webhook listener port (default: 8788). * `channels.nextcloud-talk.webhookHost`: webhook host (default: 0.0.0.0). * `channels.nextcloud-talk.webhookPath`: webhook path (default: /nextcloud-talk-webhook). * `channels.nextcloud-talk.webhookPublicUrl`: externally reachable webhook URL. * `channels.nextcloud-talk.dmPolicy`: `pairing | allowlist | open | disabled`. * `channels.nextcloud-talk.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`. * `channels.nextcloud-talk.groupPolicy`: `allowlist | open | disabled`. * `channels.nextcloud-talk.groupAllowFrom`: group allowlist (user IDs). * `channels.nextcloud-talk.rooms`: per-room settings and allowlist. * `channels.nextcloud-talk.historyLimit`: group history limit (0 disables). * `channels.nextcloud-talk.dmHistoryLimit`: DM history limit (0 disables). * `channels.nextcloud-talk.dms`: per-DM overrides (historyLimit). * `channels.nextcloud-talk.textChunkLimit`: outbound text chunk size (chars). * `channels.nextcloud-talk.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. * `channels.nextcloud-talk.blockStreaming`: disable block streaming for this channel. * `channels.nextcloud-talk.blockStreamingCoalesce`: block streaming coalesce tuning. * `channels.nextcloud-talk.mediaMaxMb`: inbound media cap (MB). # Nostr Source: https://cryptoclawdocs.termix.ai/channels/nostr # Nostr **Status:** Optional plugin (disabled by default). Nostr is a decentralized protocol for social networking. This channel enables OpenClaw to receive and respond to encrypted direct messages (DMs) via NIP-04. ## Install (on demand) ### Onboarding (recommended) * The onboarding wizard (`openclaw onboard`) and `openclaw channels add` list optional channel plugins. * Selecting Nostr prompts you to install the plugin on demand. Install defaults: * **Dev channel + git checkout available:** uses the local plugin path. * **Stable/Beta:** downloads from npm. You can always override the choice in the prompt. ### Manual install ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install @openclaw/nostr ``` Use a local checkout (dev workflows): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install --link /extensions/nostr ``` Restart the Gateway after installing or enabling plugins. ## Quick setup 1. Generate a Nostr keypair (if needed): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Using nak nak key generate ``` 2. Add to config: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "channels": { "nostr": { "privateKey": "${NOSTR_PRIVATE_KEY}" } } } ``` 3. Export the key: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export NOSTR_PRIVATE_KEY="nsec1..." ``` 4. Restart the Gateway. ## Configuration reference | Key | Type | Default | Description | | ------------ | --------- | ------------------------------------------- | ----------------------------------- | | `privateKey` | string | required | Private key in `nsec` or hex format | | `relays` | string\[] | `['wss://relay.damus.io', 'wss://nos.lol']` | Relay URLs (WebSocket) | | `dmPolicy` | string | `pairing` | DM access policy | | `allowFrom` | string\[] | `[]` | Allowed sender pubkeys | | `enabled` | boolean | `true` | Enable/disable channel | | `name` | string | - | Display name | | `profile` | object | - | NIP-01 profile metadata | ## Profile metadata Profile data is published as a NIP-01 `kind:0` event. You can manage it from the Control UI (Channels -> Nostr -> Profile) or set it directly in config. Example: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "channels": { "nostr": { "privateKey": "${NOSTR_PRIVATE_KEY}", "profile": { "name": "openclaw", "displayName": "OpenClaw", "about": "Personal assistant DM bot", "picture": "https://example.com/avatar.png", "banner": "https://example.com/banner.png", "website": "https://example.com", "nip05": "openclaw@example.com", "lud16": "openclaw@example.com" } } } } ``` Notes: * Profile URLs must use `https://`. * Importing from relays merges fields and preserves local overrides. ## Access control ### DM policies * **pairing** (default): unknown senders get a pairing code. * **allowlist**: only pubkeys in `allowFrom` can DM. * **open**: public inbound DMs (requires `allowFrom: ["*"]`). * **disabled**: ignore inbound DMs. ### Allowlist example ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "channels": { "nostr": { "privateKey": "${NOSTR_PRIVATE_KEY}", "dmPolicy": "allowlist", "allowFrom": ["npub1abc...", "npub1xyz..."] } } } ``` ## Key formats Accepted formats: * **Private key:** `nsec...` or 64-char hex * **Pubkeys (`allowFrom`):** `npub...` or hex ## Relays Defaults: `relay.damus.io` and `nos.lol`. ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "channels": { "nostr": { "privateKey": "${NOSTR_PRIVATE_KEY}", "relays": ["wss://relay.damus.io", "wss://relay.primal.net", "wss://nostr.wine"] } } } ``` Tips: * Use 2-3 relays for redundancy. * Avoid too many relays (latency, duplication). * Paid relays can improve reliability. * Local relays are fine for testing (`ws://localhost:7777`). ## Protocol support | NIP | Status | Description | | ------ | --------- | ------------------------------------- | | NIP-01 | Supported | Basic event format + profile metadata | | NIP-04 | Supported | Encrypted DMs (`kind:4`) | | NIP-17 | Planned | Gift-wrapped DMs | | NIP-44 | Planned | Versioned encryption | ## Testing ### Local relay ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Start strfry docker run -p 7777:7777 ghcr.io/hoytech/strfry ``` ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "channels": { "nostr": { "privateKey": "${NOSTR_PRIVATE_KEY}", "relays": ["ws://localhost:7777"] } } } ``` ### Manual test 1. Note the bot pubkey (npub) from logs. 2. Open a Nostr client (Damus, Amethyst, etc.). 3. DM the bot pubkey. 4. Verify the response. ## Troubleshooting ### Not receiving messages * Verify the private key is valid. * Ensure relay URLs are reachable and use `wss://` (or `ws://` for local). * Confirm `enabled` is not `false`. * Check Gateway logs for relay connection errors. ### Not sending responses * Check relay accepts writes. * Verify outbound connectivity. * Watch for relay rate limits. ### Duplicate responses * Expected when using multiple relays. * Messages are deduplicated by event ID; only the first delivery triggers a response. ## Security * Never commit private keys. * Use environment variables for keys. * Consider `allowlist` for production bots. ## Limitations (MVP) * Direct messages only (no group chats). * No media attachments. * NIP-04 only (NIP-17 gift-wrap planned). # Pairing Source: https://cryptoclawdocs.termix.ai/channels/pairing # Pairing “Pairing” is OpenClaw’s explicit **owner approval** step. It is used in two places: 1. **DM pairing** (who is allowed to talk to the bot) 2. **Node pairing** (which devices/nodes are allowed to join the gateway network) Security context: [Security](/gateway/security) ## 1) DM pairing (inbound chat access) When a channel is configured with DM policy `pairing`, unknown senders get a short code and their message is **not processed** until you approve. Default DM policies are documented in: [Security](/gateway/security) Pairing codes: * 8 characters, uppercase, no ambiguous chars (`0O1I`). * **Expire after 1 hour**. The bot only sends the pairing message when a new request is created (roughly once per hour per sender). * Pending DM pairing requests are capped at **3 per channel** by default; additional requests are ignored until one expires or is approved. ### Approve a sender ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw pairing list telegram openclaw pairing approve telegram ``` Supported channels: `telegram`, `whatsapp`, `signal`, `imessage`, `discord`, `slack`, `feishu`. ### Where the state lives Stored under `~/.openclaw/credentials/`: * Pending requests: `-pairing.json` * Approved allowlist store: * Default account: `-allowFrom.json` * Non-default account: `--allowFrom.json` Account scoping behavior: * Non-default accounts read/write only their scoped allowlist file. * Default account uses the channel-scoped unscoped allowlist file. Treat these as sensitive (they gate access to your assistant). ## 2) Node device pairing (iOS/Android/macOS/headless nodes) Nodes connect to the Gateway as **devices** with `role: node`. The Gateway creates a device pairing request that must be approved. ### Pair via Telegram (recommended for iOS) If you use the `device-pair` plugin, you can do first-time device pairing entirely from Telegram: 1. In Telegram, message your bot: `/pair` 2. The bot replies with two messages: an instruction message and a separate **setup code** message (easy to copy/paste in Telegram). 3. On your phone, open the OpenClaw iOS app → Settings → Gateway. 4. Paste the setup code and connect. 5. Back in Telegram: `/pair approve` The setup code is a base64-encoded JSON payload that contains: * `url`: the Gateway WebSocket URL (`ws://...` or `wss://...`) * `token`: a short-lived pairing token Treat the setup code like a password while it is valid. ### Approve a node device ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw devices list openclaw devices approve openclaw devices reject ``` ### Node pairing state storage Stored under `~/.openclaw/devices/`: * `pending.json` (short-lived; pending requests expire) * `paired.json` (paired devices + tokens) ### Notes * The legacy `node.pair.*` API (CLI: `openclaw nodes pending/approve`) is a separate gateway-owned pairing store. WS nodes still require device pairing. ## Related docs * Security model + prompt injection: [Security](/gateway/security) * Updating safely (run doctor): [Updating](/install/updating) * Channel configs: * Telegram: [Telegram](/channels/telegram) * WhatsApp: [WhatsApp](/channels/whatsapp) * Signal: [Signal](/channels/signal) * BlueBubbles (iMessage): [BlueBubbles](/channels/bluebubbles) * iMessage (legacy): [iMessage](/channels/imessage) * Discord: [Discord](/channels/discord) * Slack: [Slack](/channels/slack) # Signal Source: https://cryptoclawdocs.termix.ai/channels/signal # Signal (signal-cli) Status: external CLI integration. Gateway talks to `signal-cli` over HTTP JSON-RPC + SSE. ## Prerequisites * OpenClaw installed on your server (Linux flow below tested on Ubuntu 24). * `signal-cli` available on the host where the gateway runs. * A phone number that can receive one verification SMS (for SMS registration path). * Browser access for Signal captcha (`signalcaptchas.org`) during registration. ## Quick setup (beginner) 1. Use a **separate Signal number** for the bot (recommended). 2. Install `signal-cli` (Java required if you use the JVM build). 3. Choose one setup path: * **Path A (QR link):** `signal-cli link -n "OpenClaw"` and scan with Signal. * **Path B (SMS register):** register a dedicated number with captcha + SMS verification. 4. Configure OpenClaw and restart the gateway. 5. Send a first DM and approve pairing (`openclaw pairing approve signal `). Minimal config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { signal: { enabled: true, account: "+15551234567", cliPath: "signal-cli", dmPolicy: "pairing", allowFrom: ["+15557654321"], }, }, } ``` Field reference: | Field | Description | | ----------- | ------------------------------------------------- | | `account` | Bot phone number in E.164 format (`+15551234567`) | | `cliPath` | Path to `signal-cli` (`signal-cli` if on `PATH`) | | `dmPolicy` | DM access policy (`pairing` recommended) | | `allowFrom` | Phone numbers or `uuid:` values allowed to DM | ## What it is * Signal channel via `signal-cli` (not embedded libsignal). * Deterministic routing: replies always go back to Signal. * DMs share the agent's main session; groups are isolated (`agent::signal:group:`). ## Config writes By default, Signal is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`). Disable with: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { signal: { configWrites: false } }, } ``` ## The number model (important) * The gateway connects to a **Signal device** (the `signal-cli` account). * If you run the bot on **your personal Signal account**, it will ignore your own messages (loop protection). * For "I text the bot and it replies," use a **separate bot number**. ## Setup path A: link existing Signal account (QR) 1. Install `signal-cli` (JVM or native build). 2. Link a bot account: * `signal-cli link -n "OpenClaw"` then scan the QR in Signal. 3. Configure Signal and start the gateway. Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { signal: { enabled: true, account: "+15551234567", cliPath: "signal-cli", dmPolicy: "pairing", allowFrom: ["+15557654321"], }, }, } ``` Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. ## Setup path B: register dedicated bot number (SMS, Linux) Use this when you want a dedicated bot number instead of linking an existing Signal app account. 1. Get a number that can receive SMS (or voice verification for landlines). * Use a dedicated bot number to avoid account/session conflicts. 2. Install `signal-cli` on the gateway host: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} VERSION=$(curl -Ls -o /dev/null -w %{url_effective} https://github.com/AsamK/signal-cli/releases/latest | sed -e 's/^.*\/v//') curl -L -O "https://github.com/AsamK/signal-cli/releases/download/v${VERSION}/signal-cli-${VERSION}-Linux-native.tar.gz" sudo tar xf "signal-cli-${VERSION}-Linux-native.tar.gz" -C /opt sudo ln -sf /opt/signal-cli /usr/local/bin/ signal-cli --version ``` If you use the JVM build (`signal-cli-${VERSION}.tar.gz`), install JRE 25+ first. Keep `signal-cli` updated; upstream notes that old releases can break as Signal server APIs change. 3. Register and verify the number: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} signal-cli -a + register ``` If captcha is required: 1. Open `https://signalcaptchas.org/registration/generate.html`. 2. Complete captcha, copy the `signalcaptcha://...` link target from "Open Signal". 3. Run from the same external IP as the browser session when possible. 4. Run registration again immediately (captcha tokens expire quickly): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} signal-cli -a + register --captcha '' signal-cli -a + verify ``` 4. Configure OpenClaw, restart gateway, verify channel: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # If you run the gateway as a user systemd service: systemctl --user restart openclaw-gateway # Then verify: openclaw doctor openclaw channels status --probe ``` 5. Pair your DM sender: * Send any message to the bot number. * Approve code on the server: `openclaw pairing approve signal `. * Save the bot number as a contact on your phone to avoid "Unknown contact". Important: registering a phone number account with `signal-cli` can de-authenticate the main Signal app session for that number. Prefer a dedicated bot number, or use QR link mode if you need to keep your existing phone app setup. Upstream references: * `signal-cli` README: `https://github.com/AsamK/signal-cli` * Captcha flow: `https://github.com/AsamK/signal-cli/wiki/Registration-with-captcha` * Linking flow: `https://github.com/AsamK/signal-cli/wiki/Linking-other-devices-(Provisioning)` ## External daemon mode (httpUrl) If you want to manage `signal-cli` yourself (slow JVM cold starts, container init, or shared CPUs), run the daemon separately and point OpenClaw at it: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { signal: { httpUrl: "http://127.0.0.1:8080", autoStart: false, }, }, } ``` This skips auto-spawn and the startup wait inside OpenClaw. For slow starts when auto-spawning, set `channels.signal.startupTimeoutMs`. ## Access control (DMs + groups) DMs: * Default: `channels.signal.dmPolicy = "pairing"`. * Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour). * Approve via: * `openclaw pairing list signal` * `openclaw pairing approve signal ` * Pairing is the default token exchange for Signal DMs. Details: [Pairing](/channels/pairing) * UUID-only senders (from `sourceUuid`) are stored as `uuid:` in `channels.signal.allowFrom`. Groups: * `channels.signal.groupPolicy = open | allowlist | disabled`. * `channels.signal.groupAllowFrom` controls who can trigger in groups when `allowlist` is set. * Runtime note: if `channels.signal` is completely missing, runtime falls back to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set). ## How it works (behavior) * `signal-cli` runs as a daemon; the gateway reads events via SSE. * Inbound messages are normalized into the shared channel envelope. * Replies always route back to the same number or group. ## Media + limits * Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000). * Optional newline chunking: set `channels.signal.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking. * Attachments supported (base64 fetched from `signal-cli`). * Default media cap: `channels.signal.mediaMaxMb` (default 8). * Use `channels.signal.ignoreAttachments` to skip downloading media. * Group history context uses `channels.signal.historyLimit` (or `channels.signal.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). ## Typing + read receipts * **Typing indicators**: OpenClaw sends typing signals via `signal-cli sendTyping` and refreshes them while a reply is running. * **Read receipts**: when `channels.signal.sendReadReceipts` is true, OpenClaw forwards read receipts for allowed DMs. * Signal-cli does not expose read receipts for groups. ## Reactions (message tool) * Use `message action=react` with `channel=signal`. * Targets: sender E.164 or UUID (use `uuid:` from pairing output; bare UUID works too). * `messageId` is the Signal timestamp for the message you’re reacting to. * Group reactions require `targetAuthor` or `targetAuthorUuid`. Examples: ``` message action=react channel=signal target=uuid:123e4567-e89b-12d3-a456-426614174000 messageId=1737630212345 emoji=🔥 message action=react channel=signal target=+15551234567 messageId=1737630212345 emoji=🔥 remove=true message action=react channel=signal target=signal:group: targetAuthor=uuid: messageId=1737630212345 emoji=✅ ``` Config: * `channels.signal.actions.reactions`: enable/disable reaction actions (default true). * `channels.signal.reactionLevel`: `off | ack | minimal | extensive`. * `off`/`ack` disables agent reactions (message tool `react` will error). * `minimal`/`extensive` enables agent reactions and sets the guidance level. * Per-account overrides: `channels.signal.accounts..actions.reactions`, `channels.signal.accounts..reactionLevel`. ## Delivery targets (CLI/cron) * DMs: `signal:+15551234567` (or plain E.164). * UUID DMs: `uuid:` (or bare UUID). * Groups: `signal:group:`. * Usernames: `username:` (if supported by your Signal account). ## Troubleshooting Run this ladder first: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw status openclaw gateway status openclaw logs --follow openclaw doctor openclaw channels status --probe ``` Then confirm DM pairing state if needed: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw pairing list signal ``` Common failures: * Daemon reachable but no replies: verify account/daemon settings (`httpUrl`, `account`) and receive mode. * DMs ignored: sender is pending pairing approval. * Group messages ignored: group sender/mention gating blocks delivery. * Config validation errors after edits: run `openclaw doctor --fix`. * Signal missing from diagnostics: confirm `channels.signal.enabled: true`. Extra checks: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw pairing list signal pgrep -af signal-cli grep -i "signal" "/tmp/openclaw/openclaw-$(date +%Y-%m-%d).log" | tail -20 ``` For triage flow: [/channels/troubleshooting](/channels/troubleshooting). ## Security notes * `signal-cli` stores account keys locally (typically `~/.local/share/signal-cli/data/`). * Back up Signal account state before server migration or rebuild. * Keep `channels.signal.dmPolicy: "pairing"` unless you explicitly want broader DM access. * SMS verification is only needed for registration or recovery flows, but losing control of the number/account can complicate re-registration. ## Configuration reference (Signal) Full configuration: [Configuration](/gateway/configuration) Provider options: * `channels.signal.enabled`: enable/disable channel startup. * `channels.signal.account`: E.164 for the bot account. * `channels.signal.cliPath`: path to `signal-cli`. * `channels.signal.httpUrl`: full daemon URL (overrides host/port). * `channels.signal.httpHost`, `channels.signal.httpPort`: daemon bind (default 127.0.0.1:8080). * `channels.signal.autoStart`: auto-spawn daemon (default true if `httpUrl` unset). * `channels.signal.startupTimeoutMs`: startup wait timeout in ms (cap 120000). * `channels.signal.receiveMode`: `on-start | manual`. * `channels.signal.ignoreAttachments`: skip attachment downloads. * `channels.signal.ignoreStories`: ignore stories from the daemon. * `channels.signal.sendReadReceipts`: forward read receipts. * `channels.signal.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). * `channels.signal.allowFrom`: DM allowlist (E.164 or `uuid:`). `open` requires `"*"`. Signal has no usernames; use phone/UUID ids. * `channels.signal.groupPolicy`: `open | allowlist | disabled` (default: allowlist). * `channels.signal.groupAllowFrom`: group sender allowlist. * `channels.signal.historyLimit`: max group messages to include as context (0 disables). * `channels.signal.dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `channels.signal.dms[""].historyLimit`. * `channels.signal.textChunkLimit`: outbound chunk size (chars). * `channels.signal.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. * `channels.signal.mediaMaxMb`: inbound/outbound media cap (MB). Related global options: * `agents.list[].groupChat.mentionPatterns` (Signal does not support native mentions). * `messages.groupChat.mentionPatterns` (global fallback). * `messages.responsePrefix`. # Slack Source: https://cryptoclawdocs.termix.ai/channels/slack # Slack Status: production-ready for DMs + channels via Slack app integrations. Default mode is Socket Mode; HTTP Events API mode is also supported. Slack DMs default to pairing mode. Native command behavior and command catalog. Cross-channel diagnostics and repair playbooks. ## Quick setup In Slack app settings: * enable **Socket Mode** * create **App Token** (`xapp-...`) with `connections:write` * install app and copy **Bot Token** (`xoxb-...`) ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { slack: { enabled: true, mode: "socket", appToken: "xapp-...", botToken: "xoxb-...", }, }, } ``` Env fallback (default account only): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} SLACK_APP_TOKEN=xapp-... SLACK_BOT_TOKEN=xoxb-... ``` Subscribe bot events for: * `app_mention` * `message.channels`, `message.groups`, `message.im`, `message.mpim` * `reaction_added`, `reaction_removed` * `member_joined_channel`, `member_left_channel` * `channel_rename` * `pin_added`, `pin_removed` Also enable App Home **Messages Tab** for DMs. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway ``` * set mode to HTTP (`channels.slack.mode="http"`) * copy Slack **Signing Secret** * set Event Subscriptions + Interactivity + Slash command Request URL to the same webhook path (default `/slack/events`) ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { slack: { enabled: true, mode: "http", botToken: "xoxb-...", signingSecret: "your-signing-secret", webhookPath: "/slack/events", }, }, } ``` Per-account HTTP mode is supported. Give each account a distinct `webhookPath` so registrations do not collide. ## Token model * `botToken` + `appToken` are required for Socket Mode. * HTTP mode requires `botToken` + `signingSecret`. * Config tokens override env fallback. * `SLACK_BOT_TOKEN` / `SLACK_APP_TOKEN` env fallback applies only to the default account. * `userToken` (`xoxp-...`) is config-only (no env fallback) and defaults to read-only behavior (`userTokenReadOnly: true`). * Optional: add `chat:write.customize` if you want outgoing messages to use the active agent identity (custom `username` and icon). `icon_emoji` uses `:emoji_name:` syntax. For actions/directory reads, user token can be preferred when configured. For writes, bot token remains preferred; user-token writes are only allowed when `userTokenReadOnly: false` and bot token is unavailable. ## Access control and routing `channels.slack.dmPolicy` controls DM access (legacy: `channels.slack.dm.policy`): * `pairing` (default) * `allowlist` * `open` (requires `channels.slack.allowFrom` to include `"*"`; legacy: `channels.slack.dm.allowFrom`) * `disabled` DM flags: * `dm.enabled` (default true) * `channels.slack.allowFrom` (preferred) * `dm.allowFrom` (legacy) * `dm.groupEnabled` (group DMs default false) * `dm.groupChannels` (optional MPIM allowlist) Multi-account precedence: * `channels.slack.accounts.default.allowFrom` applies only to the `default` account. * Named accounts inherit `channels.slack.allowFrom` when their own `allowFrom` is unset. * Named accounts do not inherit `channels.slack.accounts.default.allowFrom`. Pairing in DMs uses `openclaw pairing approve slack `. `channels.slack.groupPolicy` controls channel handling: * `open` * `allowlist` * `disabled` Channel allowlist lives under `channels.slack.channels`. Runtime note: if `channels.slack` is completely missing (env-only setup), runtime falls back to `groupPolicy="allowlist"` and logs a warning (even if `channels.defaults.groupPolicy` is set). Name/ID resolution: * channel allowlist entries and DM allowlist entries are resolved at startup when token access allows * unresolved entries are kept as configured * inbound authorization matching is ID-first by default; direct username/slug matching requires `channels.slack.dangerouslyAllowNameMatching: true` Channel messages are mention-gated by default. Mention sources: * explicit app mention (`<@botId>`) * mention regex patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`) * implicit reply-to-bot thread behavior Per-channel controls (`channels.slack.channels.`): * `requireMention` * `users` (allowlist) * `allowBots` * `skills` * `systemPrompt` * `tools`, `toolsBySender` * `toolsBySender` key format: `id:`, `e164:`, `username:`, `name:`, or `"*"` wildcard (legacy unprefixed keys still map to `id:` only) ## Commands and slash behavior * Native command auto-mode is **off** for Slack (`commands.native: "auto"` does not enable Slack native commands). * Enable native Slack command handlers with `channels.slack.commands.native: true` (or global `commands.native: true`). * When native commands are enabled, register matching slash commands in Slack (`/` names), with one exception: * register `/agentstatus` for the status command (Slack reserves `/status`) * If native commands are not enabled, you can run a single configured slash command via `channels.slack.slashCommand`. * Native arg menus now adapt their rendering strategy: * up to 5 options: button blocks * 6-100 options: static select menu * more than 100 options: external select with async option filtering when interactivity options handlers are available * if encoded option values exceed Slack limits, the flow falls back to buttons * For long option payloads, Slash command argument menus use a confirm dialog before dispatching a selected value. Default slash command settings: * `enabled: false` * `name: "openclaw"` * `sessionPrefix: "slack:slash"` * `ephemeral: true` Slash sessions use isolated keys: * `agent::slack:slash:` and still route command execution against the target conversation session (`CommandTargetSessionKey`). ## Threading, sessions, and reply tags * DMs route as `direct`; channels as `channel`; MPIMs as `group`. * With default `session.dmScope=main`, Slack DMs collapse to agent main session. * Channel sessions: `agent::slack:channel:`. * Thread replies can create thread session suffixes (`:thread:`) when applicable. * `channels.slack.thread.historyScope` default is `thread`; `thread.inheritParent` default is `false`. * `channels.slack.thread.initialHistoryLimit` controls how many existing thread messages are fetched when a new thread session starts (default `20`; set `0` to disable). Reply threading controls: * `channels.slack.replyToMode`: `off|first|all` (default `off`) * `channels.slack.replyToModeByChatType`: per `direct|group|channel` * legacy fallback for direct chats: `channels.slack.dm.replyToMode` Manual reply tags are supported: * `[[reply_to_current]]` * `[[reply_to:]]` Note: `replyToMode="off"` disables **all** reply threading in Slack, including explicit `[[reply_to_*]]` tags. This differs from Telegram, where explicit tags are still honored in `"off"` mode. The difference reflects the platform threading models: Slack threads hide messages from the channel, while Telegram replies remain visible in the main chat flow. ## Media, chunking, and delivery Slack file attachments are downloaded from Slack-hosted private URLs (token-authenticated request flow) and written to the media store when fetch succeeds and size limits permit. Runtime inbound size cap defaults to `20MB` unless overridden by `channels.slack.mediaMaxMb`. * text chunks use `channels.slack.textChunkLimit` (default 4000) * `channels.slack.chunkMode="newline"` enables paragraph-first splitting * file sends use Slack upload APIs and can include thread replies (`thread_ts`) * outbound media cap follows `channels.slack.mediaMaxMb` when configured; otherwise channel sends use MIME-kind defaults from media pipeline Preferred explicit targets: * `user:` for DMs * `channel:` for channels Slack DMs are opened via Slack conversation APIs when sending to user targets. ## Actions and gates Slack actions are controlled by `channels.slack.actions.*`. Available action groups in current Slack tooling: | Group | Default | | ---------- | ------- | | messages | enabled | | reactions | enabled | | pins | enabled | | memberInfo | enabled | | emojiList | enabled | ## Events and operational behavior * Message edits/deletes/thread broadcasts are mapped into system events. * Reaction add/remove events are mapped into system events. * Member join/leave, channel created/renamed, and pin add/remove events are mapped into system events. * Assistant thread status updates (for "is typing..." indicators in threads) use `assistant.threads.setStatus` and require bot scope `assistant:write`. * `channel_id_changed` can migrate channel config keys when `configWrites` is enabled. * Channel topic/purpose metadata is treated as untrusted context and can be injected into routing context. * Block actions and modal interactions emit structured `Slack interaction: ...` system events with rich payload fields: * block actions: selected values, labels, picker values, and `workflow_*` metadata * modal `view_submission` and `view_closed` events with routed channel metadata and form inputs ## Ack reactions `ackReaction` sends an acknowledgement emoji while OpenClaw is processing an inbound message. Resolution order: * `channels.slack.accounts..ackReaction` * `channels.slack.ackReaction` * `messages.ackReaction` * agent identity emoji fallback (`agents.list[].identity.emoji`, else "👀") Notes: * Slack expects shortcodes (for example `"eyes"`). * Use `""` to disable the reaction for a channel or account. ## Manifest and scope checklist ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "display_information": { "name": "OpenClaw", "description": "Slack connector for OpenClaw" }, "features": { "bot_user": { "display_name": "OpenClaw", "always_online": false }, "app_home": { "messages_tab_enabled": true, "messages_tab_read_only_enabled": false }, "slash_commands": [ { "command": "/openclaw", "description": "Send a message to OpenClaw", "should_escape": false } ] }, "oauth_config": { "scopes": { "bot": [ "chat:write", "channels:history", "channels:read", "groups:history", "im:history", "im:read", "im:write", "mpim:history", "mpim:read", "mpim:write", "users:read", "app_mentions:read", "assistant:write", "reactions:read", "reactions:write", "pins:read", "pins:write", "emoji:read", "commands", "files:read", "files:write" ] } }, "settings": { "socket_mode_enabled": true, "event_subscriptions": { "bot_events": [ "app_mention", "message.channels", "message.groups", "message.im", "message.mpim", "reaction_added", "reaction_removed", "member_joined_channel", "member_left_channel", "channel_rename", "pin_added", "pin_removed" ] } } } ``` If you configure `channels.slack.userToken`, typical read scopes are: * `channels:history`, `groups:history`, `im:history`, `mpim:history` * `channels:read`, `groups:read`, `im:read`, `mpim:read` * `users:read` * `reactions:read` * `pins:read` * `emoji:read` * `search:read` (if you depend on Slack search reads) ## Troubleshooting Check, in order: * `groupPolicy` * channel allowlist (`channels.slack.channels`) * `requireMention` * per-channel `users` allowlist Useful commands: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw channels status --probe openclaw logs --follow openclaw doctor ``` Check: * `channels.slack.dm.enabled` * `channels.slack.dmPolicy` (or legacy `channels.slack.dm.policy`) * pairing approvals / allowlist entries ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw pairing list slack ``` Validate bot + app tokens and Socket Mode enablement in Slack app settings. Validate: * signing secret * webhook path * Slack Request URLs (Events + Interactivity + Slash Commands) * unique `webhookPath` per HTTP account Verify whether you intended: * native command mode (`channels.slack.commands.native: true`) with matching slash commands registered in Slack * or single slash command mode (`channels.slack.slashCommand.enabled: true`) Also check `commands.useAccessGroups` and channel/user allowlists. ## Text streaming OpenClaw supports Slack native text streaming via the Agents and AI Apps API. `channels.slack.streaming` controls live preview behavior: * `off`: disable live preview streaming. * `partial` (default): replace preview text with the latest partial output. * `block`: append chunked preview updates. * `progress`: show progress status text while generating, then send final text. `channels.slack.nativeStreaming` controls Slack's native streaming API (`chat.startStream` / `chat.appendStream` / `chat.stopStream`) when `streaming` is `partial` (default: `true`). Disable native Slack streaming (keep draft preview behavior): ```yaml theme={"theme":{"light":"min-light","dark":"min-dark"}} channels: slack: streaming: partial nativeStreaming: false ``` Legacy keys: * `channels.slack.streamMode` (`replace | status_final | append`) is auto-migrated to `channels.slack.streaming`. * boolean `channels.slack.streaming` is auto-migrated to `channels.slack.nativeStreaming`. ### Requirements 1. Enable **Agents and AI Apps** in your Slack app settings. 2. Ensure the app has the `assistant:write` scope. 3. A reply thread must be available for that message. Thread selection still follows `replyToMode`. ### Behavior * First text chunk starts a stream (`chat.startStream`). * Later text chunks append to the same stream (`chat.appendStream`). * End of reply finalizes stream (`chat.stopStream`). * Media and non-text payloads fall back to normal delivery. * If streaming fails mid-reply, OpenClaw falls back to normal delivery for remaining payloads. ## Configuration reference pointers Primary reference: * [Configuration reference - Slack](/gateway/configuration-reference#slack) High-signal Slack fields: * mode/auth: `mode`, `botToken`, `appToken`, `signingSecret`, `webhookPath`, `accounts.*` * DM access: `dm.enabled`, `dmPolicy`, `allowFrom` (legacy: `dm.policy`, `dm.allowFrom`), `dm.groupEnabled`, `dm.groupChannels` * compatibility toggle: `dangerouslyAllowNameMatching` (break-glass; keep off unless needed) * channel access: `groupPolicy`, `channels.*`, `channels.*.users`, `channels.*.requireMention` * threading/history: `replyToMode`, `replyToModeByChatType`, `thread.*`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit` * delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `streaming`, `nativeStreaming` * ops/features: `configWrites`, `commands.native`, `slashCommand.*`, `actions.*`, `userToken`, `userTokenReadOnly` ## Related * [Pairing](/channels/pairing) * [Channel routing](/channels/channel-routing) * [Troubleshooting](/channels/troubleshooting) * [Configuration](/gateway/configuration) * [Slash commands](/tools/slash-commands) # Synology Chat Source: https://cryptoclawdocs.termix.ai/channels/synology-chat # Synology Chat (plugin) Status: supported via plugin as a direct-message channel using Synology Chat webhooks. The plugin accepts inbound messages from Synology Chat outgoing webhooks and sends replies through a Synology Chat incoming webhook. ## Plugin required Synology Chat is plugin-based and not part of the default core channel install. Install from a local checkout: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install ./extensions/synology-chat ``` Details: [Plugins](/tools/plugin) ## Quick setup 1. Install and enable the Synology Chat plugin. 2. In Synology Chat integrations: * Create an incoming webhook and copy its URL. * Create an outgoing webhook with your secret token. 3. Point the outgoing webhook URL to your OpenClaw gateway: * `https://gateway-host/webhook/synology` by default. * Or your custom `channels.synology-chat.webhookPath`. 4. Configure `channels.synology-chat` in OpenClaw. 5. Restart gateway and send a DM to the Synology Chat bot. Minimal config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { "synology-chat": { enabled: true, token: "synology-outgoing-token", incomingUrl: "https://nas.example.com/webapi/entry.cgi?api=SYNO.Chat.External&method=incoming&version=2&token=...", webhookPath: "/webhook/synology", dmPolicy: "allowlist", allowedUserIds: ["123456"], rateLimitPerMinute: 30, allowInsecureSsl: false, }, }, } ``` ## Environment variables For the default account, you can use env vars: * `SYNOLOGY_CHAT_TOKEN` * `SYNOLOGY_CHAT_INCOMING_URL` * `SYNOLOGY_NAS_HOST` * `SYNOLOGY_ALLOWED_USER_IDS` (comma-separated) * `SYNOLOGY_RATE_LIMIT` * `OPENCLAW_BOT_NAME` Config values override env vars. ## DM policy and access control * `dmPolicy: "allowlist"` is the recommended default. * `allowedUserIds` accepts a list (or comma-separated string) of Synology user IDs. * In `allowlist` mode, an empty `allowedUserIds` list is treated as misconfiguration and the webhook route will not start (use `dmPolicy: "open"` for allow-all). * `dmPolicy: "open"` allows any sender. * `dmPolicy: "disabled"` blocks DMs. * Pairing approvals work with: * `openclaw pairing list synology-chat` * `openclaw pairing approve synology-chat ` ## Outbound delivery Use numeric Synology Chat user IDs as targets. Examples: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw message send --channel synology-chat --target 123456 --text "Hello from OpenClaw" openclaw message send --channel synology-chat --target synology-chat:123456 --text "Hello again" ``` Media sends are supported by URL-based file delivery. ## Multi-account Multiple Synology Chat accounts are supported under `channels.synology-chat.accounts`. Each account can override token, incoming URL, webhook path, DM policy, and limits. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { "synology-chat": { enabled: true, accounts: { default: { token: "token-a", incomingUrl: "https://nas-a.example.com/...token=...", }, alerts: { token: "token-b", incomingUrl: "https://nas-b.example.com/...token=...", webhookPath: "/webhook/synology-alerts", dmPolicy: "allowlist", allowedUserIds: ["987654"], }, }, }, }, } ``` ## Security notes * Keep `token` secret and rotate it if leaked. * Keep `allowInsecureSsl: false` unless you explicitly trust a self-signed local NAS cert. * Inbound webhook requests are token-verified and rate-limited per sender. * Prefer `dmPolicy: "allowlist"` for production. # Telegram Source: https://cryptoclawdocs.termix.ai/channels/telegram # Telegram (Bot API) Status: production-ready for bot DMs + groups via grammY. Long polling is the default mode; webhook mode is optional. Default DM policy for Telegram is pairing. Cross-channel diagnostics and repair playbooks. Full channel config patterns and examples. ## Quick setup Open Telegram and chat with **@BotFather** (confirm the handle is exactly `@BotFather`). Run `/newbot`, follow prompts, and save the token. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { telegram: { enabled: true, botToken: "123:abc", dmPolicy: "pairing", groups: { "*": { requireMention: true } }, }, }, } ``` Env fallback: `TELEGRAM_BOT_TOKEN=...` (default account only). Telegram does **not** use `openclaw channels login telegram`; configure token in config/env, then start gateway. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway openclaw pairing list telegram openclaw pairing approve telegram ``` Pairing codes expire after 1 hour. Add the bot to your group, then set `channels.telegram.groups` and `groupPolicy` to match your access model. Token resolution order is account-aware. In practice, config values win over env fallback, and `TELEGRAM_BOT_TOKEN` only applies to the default account. ## Telegram side settings Telegram bots default to **Privacy Mode**, which limits what group messages they receive. If the bot must see all group messages, either: * disable privacy mode via `/setprivacy`, or * make the bot a group admin. When toggling privacy mode, remove + re-add the bot in each group so Telegram applies the change. Admin status is controlled in Telegram group settings. Admin bots receive all group messages, which is useful for always-on group behavior. * `/setjoingroups` to allow/deny group adds * `/setprivacy` for group visibility behavior ## Access control and activation `channels.telegram.dmPolicy` controls direct message access: * `pairing` (default) * `allowlist` (requires at least one sender ID in `allowFrom`) * `open` (requires `allowFrom` to include `"*"`) * `disabled` `channels.telegram.allowFrom` accepts numeric Telegram user IDs. `telegram:` / `tg:` prefixes are accepted and normalized. `dmPolicy: "allowlist"` with empty `allowFrom` blocks all DMs and is rejected by config validation. The onboarding wizard accepts `@username` input and resolves it to numeric IDs. If you upgraded and your config contains `@username` allowlist entries, run `openclaw doctor --fix` to resolve them (best-effort; requires a Telegram bot token). If you previously relied on pairing-store allowlist files, `openclaw doctor --fix` can recover entries into `channels.telegram.allowFrom` in allowlist flows (for example when `dmPolicy: "allowlist"` has no explicit IDs yet). ### Finding your Telegram user ID Safer (no third-party bot): 1. DM your bot. 2. Run `openclaw logs --follow`. 3. Read `from.id`. Official Bot API method: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl "https://api.telegram.org/bot/getUpdates" ``` Third-party method (less private): `@userinfobot` or `@getidsbot`. Two controls apply together: 1. **Which groups are allowed** (`channels.telegram.groups`) * no `groups` config: * with `groupPolicy: "open"`: any group can pass group-ID checks * with `groupPolicy: "allowlist"` (default): groups are blocked until you add `groups` entries (or `"*"`) * `groups` configured: acts as allowlist (explicit IDs or `"*"`) 2. **Which senders are allowed in groups** (`channels.telegram.groupPolicy`) * `open` * `allowlist` (default) * `disabled` `groupAllowFrom` is used for group sender filtering. If not set, Telegram falls back to `allowFrom`. `groupAllowFrom` entries should be numeric Telegram user IDs (`telegram:` / `tg:` prefixes are normalized). Non-numeric entries are ignored for sender authorization. Security boundary (`2026.2.25+`): group sender auth does **not** inherit DM pairing-store approvals. Pairing stays DM-only. For groups, set `groupAllowFrom` or per-group/per-topic `allowFrom`. Runtime note: if `channels.telegram` is completely missing, runtime defaults to fail-closed `groupPolicy="allowlist"` unless `channels.defaults.groupPolicy` is explicitly set. Example: allow any member in one specific group: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { telegram: { groups: { "-1001234567890": { groupPolicy: "open", requireMention: false, }, }, }, }, } ``` Group replies require mention by default. Mention can come from: * native `@botusername` mention, or * mention patterns in: * `agents.list[].groupChat.mentionPatterns` * `messages.groupChat.mentionPatterns` Session-level command toggles: * `/activation always` * `/activation mention` These update session state only. Use config for persistence. Persistent config example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { telegram: { groups: { "*": { requireMention: false }, }, }, }, } ``` Getting the group chat ID: * forward a group message to `@userinfobot` / `@getidsbot` * or read `chat.id` from `openclaw logs --follow` * or inspect Bot API `getUpdates` ## Runtime behavior * Telegram is owned by the gateway process. * Routing is deterministic: Telegram inbound replies back to Telegram (the model does not pick channels). * Inbound messages normalize into the shared channel envelope with reply metadata and media placeholders. * Group sessions are isolated by group ID. Forum topics append `:topic:` to keep topics isolated. * DM messages can carry `message_thread_id`; OpenClaw routes them with thread-aware session keys and preserves thread ID for replies. * Long polling uses grammY runner with per-chat/per-thread sequencing. Overall runner sink concurrency uses `agents.defaults.maxConcurrent`. * Telegram Bot API has no read-receipt support (`sendReadReceipts` does not apply). ## Feature reference OpenClaw can stream partial replies in real time: * direct chats: Telegram native draft streaming via `sendMessageDraft` * groups/topics: preview message + `editMessageText` Requirement: * `channels.telegram.streaming` is `off | partial | block | progress` (default: `partial`) * `progress` maps to `partial` on Telegram (compat with cross-channel naming) * legacy `channels.telegram.streamMode` and boolean `streaming` values are auto-mapped Telegram enabled `sendMessageDraft` for all bots in Bot API 9.5 (March 1, 2026). For text-only replies: * DM: OpenClaw updates the draft in place (no extra preview message) * group/topic: OpenClaw keeps the same preview message and performs a final edit in place (no second message) For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message. Preview streaming is separate from block streaming. When block streaming is explicitly enabled for Telegram, OpenClaw skips the preview stream to avoid double-streaming. If native draft transport is unavailable/rejected, OpenClaw automatically falls back to `sendMessage` + `editMessageText`. Telegram-only reasoning stream: * `/reasoning stream` sends reasoning to the live preview while generating * final answer is sent without reasoning text Outbound text uses Telegram `parse_mode: "HTML"`. * Markdown-ish text is rendered to Telegram-safe HTML. * Raw model HTML is escaped to reduce Telegram parse failures. * If Telegram rejects parsed HTML, OpenClaw retries as plain text. Link previews are enabled by default and can be disabled with `channels.telegram.linkPreview: false`. Telegram command menu registration is handled at startup with `setMyCommands`. Native command defaults: * `commands.native: "auto"` enables native commands for Telegram Add custom command menu entries: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { telegram: { customCommands: [ { command: "backup", description: "Git backup" }, { command: "generate", description: "Create an image" }, ], }, }, } ``` Rules: * names are normalized (strip leading `/`, lowercase) * valid pattern: `a-z`, `0-9`, `_`, length `1..32` * custom commands cannot override native commands * conflicts/duplicates are skipped and logged Notes: * custom commands are menu entries only; they do not auto-implement behavior * plugin/skill commands can still work when typed even if not shown in Telegram menu If native commands are disabled, built-ins are removed. Custom/plugin commands may still register if configured. Common setup failure: * `setMyCommands failed` usually means outbound DNS/HTTPS to `api.telegram.org` is blocked. ### Device pairing commands (`device-pair` plugin) When the `device-pair` plugin is installed: 1. `/pair` generates setup code 2. paste code in iOS app 3. `/pair approve` approves latest pending request More details: [Pairing](/channels/pairing#pair-via-telegram-recommended-for-ios). Configure inline keyboard scope: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { telegram: { capabilities: { inlineButtons: "allowlist", }, }, }, } ``` Per-account override: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { telegram: { accounts: { main: { capabilities: { inlineButtons: "allowlist", }, }, }, }, }, } ``` Scopes: * `off` * `dm` * `group` * `all` * `allowlist` (default) Legacy `capabilities: ["inlineButtons"]` maps to `inlineButtons: "all"`. Message action example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { action: "send", channel: "telegram", to: "123456789", message: "Choose an option:", buttons: [ [ { text: "Yes", callback_data: "yes" }, { text: "No", callback_data: "no" }, ], [{ text: "Cancel", callback_data: "cancel" }], ], } ``` Callback clicks are passed to the agent as text: `callback_data: ` Telegram tool actions include: * `sendMessage` (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`) * `react` (`chatId`, `messageId`, `emoji`) * `deleteMessage` (`chatId`, `messageId`) * `editMessage` (`chatId`, `messageId`, `content`) * `createForumTopic` (`chatId`, `name`, optional `iconColor`, `iconCustomEmojiId`) Channel message actions expose ergonomic aliases (`send`, `react`, `delete`, `edit`, `sticker`, `sticker-search`, `topic-create`). Gating controls: * `channels.telegram.actions.sendMessage` * `channels.telegram.actions.deleteMessage` * `channels.telegram.actions.reactions` * `channels.telegram.actions.sticker` (default: disabled) Note: `edit` and `topic-create` are currently enabled by default and do not have separate `channels.telegram.actions.*` toggles. Reaction removal semantics: [/tools/reactions](/tools/reactions) Telegram supports explicit reply threading tags in generated output: * `[[reply_to_current]]` replies to the triggering message * `[[reply_to:]]` replies to a specific Telegram message ID `channels.telegram.replyToMode` controls handling: * `off` (default) * `first` * `all` Note: `off` disables implicit reply threading. Explicit `[[reply_to_*]]` tags are still honored. Forum supergroups: * topic session keys append `:topic:` * replies and typing target the topic thread * topic config path: `channels.telegram.groups..topics.` General topic (`threadId=1`) special-case: * message sends omit `message_thread_id` (Telegram rejects `sendMessage(...thread_id=1)`) * typing actions still include `message_thread_id` Topic inheritance: topic entries inherit group settings unless overridden (`requireMention`, `allowFrom`, `skills`, `systemPrompt`, `enabled`, `groupPolicy`). `agentId` is topic-only and does not inherit from group defaults. **Per-topic agent routing**: Each topic can route to a different agent by setting `agentId` in the topic config. This gives each topic its own isolated workspace, memory, and session. Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { telegram: { groups: { "-1001234567890": { topics: { "1": { agentId: "main" }, // General topic → main agent "3": { agentId: "zu" }, // Dev topic → zu agent "5": { agentId: "coder" } // Code review → coder agent } } } } } } ``` Each topic then has its own session key: `agent:zu:telegram:group:-1001234567890:topic:3` Template context includes: * `MessageThreadId` * `IsForum` DM thread behavior: * private chats with `message_thread_id` keep DM routing but use thread-aware session keys/reply targets. ### Audio messages Telegram distinguishes voice notes vs audio files. * default: audio file behavior * tag `[[audio_as_voice]]` in agent reply to force voice-note send Message action example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { action: "send", channel: "telegram", to: "123456789", media: "https://example.com/voice.ogg", asVoice: true, } ``` ### Video messages Telegram distinguishes video files vs video notes. Message action example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { action: "send", channel: "telegram", to: "123456789", media: "https://example.com/video.mp4", asVideoNote: true, } ``` Video notes do not support captions; provided message text is sent separately. ### Stickers Inbound sticker handling: * static WEBP: downloaded and processed (placeholder ``) * animated TGS: skipped * video WEBM: skipped Sticker context fields: * `Sticker.emoji` * `Sticker.setName` * `Sticker.fileId` * `Sticker.fileUniqueId` * `Sticker.cachedDescription` Sticker cache file: * `~/.openclaw/telegram/sticker-cache.json` Stickers are described once (when possible) and cached to reduce repeated vision calls. Enable sticker actions: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { telegram: { actions: { sticker: true, }, }, }, } ``` Send sticker action: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { action: "sticker", channel: "telegram", to: "123456789", fileId: "CAACAgIAAxkBAAI...", } ``` Search cached stickers: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { action: "sticker-search", channel: "telegram", query: "cat waving", limit: 5, } ``` Telegram reactions arrive as `message_reaction` updates (separate from message payloads). When enabled, OpenClaw enqueues system events like: * `Telegram reaction added: 👍 by Alice (@alice) on msg 42` Config: * `channels.telegram.reactionNotifications`: `off | own | all` (default: `own`) * `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` (default: `minimal`) Notes: * `own` means user reactions to bot-sent messages only (best-effort via sent-message cache). * Reaction events still respect Telegram access controls (`dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`); unauthorized senders are dropped. * Telegram does not provide thread IDs in reaction updates. * non-forum groups route to group chat session * forum groups route to the group general-topic session (`:topic:1`), not the exact originating topic `allowed_updates` for polling/webhook include `message_reaction` automatically. `ackReaction` sends an acknowledgement emoji while OpenClaw is processing an inbound message. Resolution order: * `channels.telegram.accounts..ackReaction` * `channels.telegram.ackReaction` * `messages.ackReaction` * agent identity emoji fallback (`agents.list[].identity.emoji`, else "👀") Notes: * Telegram expects unicode emoji (for example "👀"). * Use `""` to disable the reaction for a channel or account. Channel config writes are enabled by default (`configWrites !== false`). Telegram-triggered writes include: * group migration events (`migrate_to_chat_id`) to update `channels.telegram.groups` * `/config set` and `/config unset` (requires command enablement) Disable: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { telegram: { configWrites: false, }, }, } ``` Default: long polling. Webhook mode: * set `channels.telegram.webhookUrl` * set `channels.telegram.webhookSecret` (required when webhook URL is set) * optional `channels.telegram.webhookPath` (default `/telegram-webhook`) * optional `channels.telegram.webhookHost` (default `127.0.0.1`) * optional `channels.telegram.webhookPort` (default `8787`) Default local listener for webhook mode binds to `127.0.0.1:8787`. If your public endpoint differs, place a reverse proxy in front and point `webhookUrl` at the public URL. Set `webhookHost` (for example `0.0.0.0`) when you intentionally need external ingress. * `channels.telegram.textChunkLimit` default is 4000. * `channels.telegram.chunkMode="newline"` prefers paragraph boundaries (blank lines) before length splitting. * `channels.telegram.mediaMaxMb` (default 5) caps inbound Telegram media download/processing size. * `channels.telegram.timeoutSeconds` overrides Telegram API client timeout (if unset, grammY default applies). * group context history uses `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` (default 50); `0` disables. * DM history controls: * `channels.telegram.dmHistoryLimit` * `channels.telegram.dms[""].historyLimit` * `channels.telegram.retry` config applies to Telegram send helpers (CLI/tools/actions) for recoverable outbound API errors. CLI send target can be numeric chat ID or username: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw message send --channel telegram --target 123456789 --message "hi" openclaw message send --channel telegram --target @name --message "hi" ``` ## Troubleshooting * If `requireMention=false`, Telegram privacy mode must allow full visibility. * BotFather: `/setprivacy` -> Disable * then remove + re-add bot to group * `openclaw channels status` warns when config expects unmentioned group messages. * `openclaw channels status --probe` can check explicit numeric group IDs; wildcard `"*"` cannot be membership-probed. * quick session test: `/activation always`. * when `channels.telegram.groups` exists, group must be listed (or include `"*"`) * verify bot membership in group * review logs: `openclaw logs --follow` for skip reasons * authorize your sender identity (pairing and/or numeric `allowFrom`) * command authorization still applies even when group policy is `open` * `setMyCommands failed` usually indicates DNS/HTTPS reachability issues to `api.telegram.org` * Node 22+ + custom fetch/proxy can trigger immediate abort behavior if AbortSignal types mismatch. * Some hosts resolve `api.telegram.org` to IPv6 first; broken IPv6 egress can cause intermittent Telegram API failures. * If logs include `TypeError: fetch failed` or `Network request for 'getUpdates' failed!`, OpenClaw now retries these as recoverable network errors. * On VPS hosts with unstable direct egress/TLS, route Telegram API calls through `channels.telegram.proxy`: ```yaml theme={"theme":{"light":"min-light","dark":"min-dark"}} channels: telegram: proxy: socks5://user:pass@proxy-host:1080 ``` * Node 22+ defaults to `autoSelectFamily=true` (except WSL2) and `dnsResultOrder=ipv4first`. * If your host is WSL2 or explicitly works better with IPv4-only behavior, force family selection: ```yaml theme={"theme":{"light":"min-light","dark":"min-dark"}} channels: telegram: network: autoSelectFamily: false ``` * Environment overrides (temporary): * `OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY=1` * `OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY=1` * `OPENCLAW_TELEGRAM_DNS_RESULT_ORDER=ipv4first` * Validate DNS answers: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} dig +short api.telegram.org A dig +short api.telegram.org AAAA ``` More help: [Channel troubleshooting](/channels/troubleshooting). ## Telegram config reference pointers Primary reference: * `channels.telegram.enabled`: enable/disable channel startup. * `channels.telegram.botToken`: bot token (BotFather). * `channels.telegram.tokenFile`: read token from file path. * `channels.telegram.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). * `channels.telegram.allowFrom`: DM allowlist (numeric Telegram user IDs). `allowlist` requires at least one sender ID. `open` requires `"*"`. `openclaw doctor --fix` can resolve legacy `@username` entries to IDs and can recover allowlist entries from pairing-store files in allowlist migration flows. * `channels.telegram.defaultTo`: default Telegram target used by CLI `--deliver` when no explicit `--reply-to` is provided. * `channels.telegram.groupPolicy`: `open | allowlist | disabled` (default: allowlist). * `channels.telegram.groupAllowFrom`: group sender allowlist (numeric Telegram user IDs). `openclaw doctor --fix` can resolve legacy `@username` entries to IDs. Non-numeric entries are ignored at auth time. Group auth does not use DM pairing-store fallback (`2026.2.25+`). * Multi-account precedence: * When two or more account IDs are configured, set `channels.telegram.defaultAccount` (or include `channels.telegram.accounts.default`) to make default routing explicit. * If neither is set, OpenClaw falls back to the first normalized account ID and `openclaw doctor` warns. * `channels.telegram.accounts.default.allowFrom` and `channels.telegram.accounts.default.groupAllowFrom` apply only to the `default` account. * Named accounts inherit `channels.telegram.allowFrom` and `channels.telegram.groupAllowFrom` when account-level values are unset. * Named accounts do not inherit `channels.telegram.accounts.default.allowFrom` / `groupAllowFrom`. * `channels.telegram.groups`: per-group defaults + allowlist (use `"*"` for global defaults). * `channels.telegram.groups..groupPolicy`: per-group override for groupPolicy (`open | allowlist | disabled`). * `channels.telegram.groups..requireMention`: mention gating default. * `channels.telegram.groups..skills`: skill filter (omit = all skills, empty = none). * `channels.telegram.groups..allowFrom`: per-group sender allowlist override. * `channels.telegram.groups..systemPrompt`: extra system prompt for the group. * `channels.telegram.groups..enabled`: disable the group when `false`. * `channels.telegram.groups..topics..*`: per-topic overrides (group fields + topic-only `agentId`). * `channels.telegram.groups..topics..agentId`: route this topic to a specific agent (overrides group-level and binding routing). * `channels.telegram.groups..topics..groupPolicy`: per-topic override for groupPolicy (`open | allowlist | disabled`). * `channels.telegram.groups..topics..requireMention`: per-topic mention gating override. * `channels.telegram.direct..topics..agentId`: route DM topics to a specific agent (same behavior as forum topics). * `channels.telegram.capabilities.inlineButtons`: `off | dm | group | all | allowlist` (default: allowlist). * `channels.telegram.accounts..capabilities.inlineButtons`: per-account override. * `channels.telegram.commands.nativeSkills`: enable/disable Telegram native skills commands. * `channels.telegram.replyToMode`: `off | first | all` (default: `off`). * `channels.telegram.textChunkLimit`: outbound chunk size (chars). * `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking. * `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true). * `channels.telegram.streaming`: `off | partial | block | progress` (live stream preview; default: `partial`; `progress` maps to `partial`; `block` is legacy preview mode compatibility). In DMs, `partial` uses native `sendMessageDraft` when available. * `channels.telegram.mediaMaxMb`: inbound Telegram media download/processing cap (MB). * `channels.telegram.retry`: retry policy for Telegram send helpers (CLI/tools/actions) on recoverable outbound API errors (attempts, minDelayMs, maxDelayMs, jitter). * `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to enabled on Node 22+, with WSL2 defaulting to disabled. * `channels.telegram.network.dnsResultOrder`: override DNS result order (`ipv4first` or `verbatim`). Defaults to `ipv4first` on Node 22+. * `channels.telegram.proxy`: proxy URL for Bot API calls (SOCKS/HTTP). * `channels.telegram.webhookUrl`: enable webhook mode (requires `channels.telegram.webhookSecret`). * `channels.telegram.webhookSecret`: webhook secret (required when webhookUrl is set). * `channels.telegram.webhookPath`: local webhook path (default `/telegram-webhook`). * `channels.telegram.webhookHost`: local webhook bind host (default `127.0.0.1`). * `channels.telegram.webhookPort`: local webhook bind port (default `8787`). * `channels.telegram.actions.reactions`: gate Telegram tool reactions. * `channels.telegram.actions.sendMessage`: gate Telegram tool message sends. * `channels.telegram.actions.deleteMessage`: gate Telegram tool message deletes. * `channels.telegram.actions.sticker`: gate Telegram sticker actions — send and search (default: false). * `channels.telegram.reactionNotifications`: `off | own | all` — control which reactions trigger system events (default: `own` when not set). * `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` — control agent's reaction capability (default: `minimal` when not set). * [Configuration reference - Telegram](/gateway/configuration-reference#telegram) Telegram-specific high-signal fields: * startup/auth: `enabled`, `botToken`, `tokenFile`, `accounts.*` * access control: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`, `groups.*.topics.*` * command/menu: `commands.native`, `commands.nativeSkills`, `customCommands` * threading/replies: `replyToMode` * streaming: `streaming` (preview), `blockStreaming` * formatting/delivery: `textChunkLimit`, `chunkMode`, `linkPreview`, `responsePrefix` * media/network: `mediaMaxMb`, `timeoutSeconds`, `retry`, `network.autoSelectFamily`, `proxy` * webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost` * actions/capabilities: `capabilities.inlineButtons`, `actions.sendMessage|editMessage|deleteMessage|reactions|sticker` * reactions: `reactionNotifications`, `reactionLevel` * writes/history: `configWrites`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit` ## Related * [Pairing](/channels/pairing) * [Channel routing](/channels/channel-routing) * [Multi-agent routing](/concepts/multi-agent) * [Troubleshooting](/channels/troubleshooting) # Tlon Source: https://cryptoclawdocs.termix.ai/channels/tlon # Tlon (plugin) Tlon is a decentralized messenger built on Urbit. OpenClaw connects to your Urbit ship and can respond to DMs and group chat messages. Group replies require an @ mention by default and can be further restricted via allowlists. Status: supported via plugin. DMs, group mentions, thread replies, rich text formatting, and image uploads are supported. Reactions and polls are not yet supported. ## Plugin required Tlon ships as a plugin and is not bundled with the core install. Install via CLI (npm registry): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install @openclaw/tlon ``` Local checkout (when running from a git repo): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install ./extensions/tlon ``` Details: [Plugins](/tools/plugin) ## Setup 1. Install the Tlon plugin. 2. Gather your ship URL and login code. 3. Configure `channels.tlon`. 4. Restart the gateway. 5. DM the bot or mention it in a group channel. Minimal config (single account): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { tlon: { enabled: true, ship: "~sampel-palnet", url: "https://your-ship-host", code: "lidlut-tabwed-pillex-ridrup", ownerShip: "~your-main-ship", // recommended: your ship, always allowed }, }, } ``` ## Private/LAN ships By default, OpenClaw blocks private/internal hostnames and IP ranges for SSRF protection. If your ship is running on a private network (localhost, LAN IP, or internal hostname), you must explicitly opt in: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { tlon: { url: "http://localhost:8080", allowPrivateNetwork: true, }, }, } ``` This applies to URLs like: * `http://localhost:8080` * `http://192.168.x.x:8080` * `http://my-ship.local:8080` ⚠️ Only enable this if you trust your local network. This setting disables SSRF protections for requests to your ship URL. ## Group channels Auto-discovery is enabled by default. You can also pin channels manually: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { tlon: { groupChannels: ["chat/~host-ship/general", "chat/~host-ship/support"], }, }, } ``` Disable auto-discovery: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { tlon: { autoDiscoverChannels: false, }, }, } ``` ## Access control DM allowlist (empty = no DMs allowed, use `ownerShip` for approval flow): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { tlon: { dmAllowlist: ["~zod", "~nec"], }, }, } ``` Group authorization (restricted by default): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { tlon: { defaultAuthorizedShips: ["~zod"], authorization: { channelRules: { "chat/~host-ship/general": { mode: "restricted", allowedShips: ["~zod", "~nec"], }, "chat/~host-ship/announcements": { mode: "open", }, }, }, }, }, } ``` ## Owner and approval system Set an owner ship to receive approval requests when unauthorized users try to interact: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { tlon: { ownerShip: "~your-main-ship", }, }, } ``` The owner ship is **automatically authorized everywhere** — DM invites are auto-accepted and channel messages are always allowed. You don't need to add the owner to `dmAllowlist` or `defaultAuthorizedShips`. When set, the owner receives DM notifications for: * DM requests from ships not in the allowlist * Mentions in channels without authorization * Group invite requests ## Auto-accept settings Auto-accept DM invites (for ships in dmAllowlist): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { tlon: { autoAcceptDmInvites: true, }, }, } ``` Auto-accept group invites: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { tlon: { autoAcceptGroupInvites: true, }, }, } ``` ## Delivery targets (CLI/cron) Use these with `openclaw message send` or cron delivery: * DM: `~sampel-palnet` or `dm/~sampel-palnet` * Group: `chat/~host-ship/channel` or `group:~host-ship/channel` ## Bundled skill The Tlon plugin includes a bundled skill ([`@tloncorp/tlon-skill`](https://github.com/tloncorp/tlon-skill)) that provides CLI access to Tlon operations: * **Contacts**: get/update profiles, list contacts * **Channels**: list, create, post messages, fetch history * **Groups**: list, create, manage members * **DMs**: send messages, react to messages * **Reactions**: add/remove emoji reactions to posts and DMs * **Settings**: manage plugin permissions via slash commands The skill is automatically available when the plugin is installed. ## Capabilities | Feature | Status | | --------------- | -------------------------------------- | | Direct messages | ✅ Supported | | Groups/channels | ✅ Supported (mention-gated by default) | | Threads | ✅ Supported (auto-replies in thread) | | Rich text | ✅ Markdown converted to Tlon format | | Images | ✅ Uploaded to Tlon storage | | Reactions | ✅ Via [bundled skill](#bundled-skill) | | Polls | ❌ Not yet supported | | Native commands | ✅ Supported (owner-only by default) | ## Troubleshooting Run this ladder first: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw status openclaw gateway status openclaw logs --follow openclaw doctor ``` Common failures: * **DMs ignored**: sender not in `dmAllowlist` and no `ownerShip` configured for approval flow. * **Group messages ignored**: channel not discovered or sender not authorized. * **Connection errors**: check ship URL is reachable; enable `allowPrivateNetwork` for local ships. * **Auth errors**: verify login code is current (codes rotate). ## Configuration reference Full configuration: [Configuration](/gateway/configuration) Provider options: * `channels.tlon.enabled`: enable/disable channel startup. * `channels.tlon.ship`: bot's Urbit ship name (e.g. `~sampel-palnet`). * `channels.tlon.url`: ship URL (e.g. `https://sampel-palnet.tlon.network`). * `channels.tlon.code`: ship login code. * `channels.tlon.allowPrivateNetwork`: allow localhost/LAN URLs (SSRF bypass). * `channels.tlon.ownerShip`: owner ship for approval system (always authorized). * `channels.tlon.dmAllowlist`: ships allowed to DM (empty = none). * `channels.tlon.autoAcceptDmInvites`: auto-accept DMs from allowlisted ships. * `channels.tlon.autoAcceptGroupInvites`: auto-accept all group invites. * `channels.tlon.autoDiscoverChannels`: auto-discover group channels (default: true). * `channels.tlon.groupChannels`: manually pinned channel nests. * `channels.tlon.defaultAuthorizedShips`: ships authorized for all channels. * `channels.tlon.authorization.channelRules`: per-channel auth rules. * `channels.tlon.showModelSignature`: append model name to messages. ## Notes * Group replies require a mention (e.g. `~your-bot-ship`) to respond. * Thread replies: if the inbound message is in a thread, OpenClaw replies in-thread. * Rich text: Markdown formatting (bold, italic, code, headers, lists) is converted to Tlon's native format. * Images: URLs are uploaded to Tlon storage and embedded as image blocks. # Channel Troubleshooting Source: https://cryptoclawdocs.termix.ai/channels/troubleshooting # Channel troubleshooting Use this page when a channel connects but behavior is wrong. ## Command ladder Run these in order first: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw status openclaw gateway status openclaw logs --follow openclaw doctor openclaw channels status --probe ``` Healthy baseline: * `Runtime: running` * `RPC probe: ok` * Channel probe shows connected/ready ## WhatsApp ### WhatsApp failure signatures | Symptom | Fastest check | Fix | | ------------------------------- | --------------------------------------------------- | ------------------------------------------------------- | | Connected but no DM replies | `openclaw pairing list whatsapp` | Approve sender or switch DM policy/allowlist. | | Group messages ignored | Check `requireMention` + mention patterns in config | Mention the bot or relax mention policy for that group. | | Random disconnect/relogin loops | `openclaw channels status --probe` + logs | Re-login and verify credentials directory is healthy. | Full troubleshooting: [/channels/whatsapp#troubleshooting-quick](/channels/whatsapp#troubleshooting-quick) ## Telegram ### Telegram failure signatures | Symptom | Fastest check | Fix | | --------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------- | | `/start` but no usable reply flow | `openclaw pairing list telegram` | Approve pairing or change DM policy. | | Bot online but group stays silent | Verify mention requirement and bot privacy mode | Disable privacy mode for group visibility or mention bot. | | Send failures with network errors | Inspect logs for Telegram API call failures | Fix DNS/IPv6/proxy routing to `api.telegram.org`. | | Upgraded and allowlist blocks you | `openclaw security audit` and config allowlists | Run `openclaw doctor --fix` or replace `@username` with numeric sender IDs. | Full troubleshooting: [/channels/telegram#troubleshooting](/channels/telegram#troubleshooting) ## Discord ### Discord failure signatures | Symptom | Fastest check | Fix | | ------------------------------- | ----------------------------------- | --------------------------------------------------------- | | Bot online but no guild replies | `openclaw channels status --probe` | Allow guild/channel and verify message content intent. | | Group messages ignored | Check logs for mention gating drops | Mention bot or set guild/channel `requireMention: false`. | | DM replies missing | `openclaw pairing list discord` | Approve DM pairing or adjust DM policy. | Full troubleshooting: [/channels/discord#troubleshooting](/channels/discord#troubleshooting) ## Slack ### Slack failure signatures | Symptom | Fastest check | Fix | | -------------------------------------- | ----------------------------------------- | ------------------------------------------------- | | Socket mode connected but no responses | `openclaw channels status --probe` | Verify app token + bot token and required scopes. | | DMs blocked | `openclaw pairing list slack` | Approve pairing or relax DM policy. | | Channel message ignored | Check `groupPolicy` and channel allowlist | Allow the channel or switch policy to `open`. | Full troubleshooting: [/channels/slack#troubleshooting](/channels/slack#troubleshooting) ## iMessage and BlueBubbles ### iMessage and BlueBubbles failure signatures | Symptom | Fastest check | Fix | | -------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------- | | No inbound events | Verify webhook/server reachability and app permissions | Fix webhook URL or BlueBubbles server state. | | Can send but no receive on macOS | Check macOS privacy permissions for Messages automation | Re-grant TCC permissions and restart channel process. | | DM sender blocked | `openclaw pairing list imessage` or `openclaw pairing list bluebubbles` | Approve pairing or update allowlist. | Full troubleshooting: * [/channels/imessage#troubleshooting-macos-privacy-and-security-tcc](/channels/imessage#troubleshooting-macos-privacy-and-security-tcc) * [/channels/bluebubbles#troubleshooting](/channels/bluebubbles#troubleshooting) ## Signal ### Signal failure signatures | Symptom | Fastest check | Fix | | ------------------------------- | ------------------------------------------ | -------------------------------------------------------- | | Daemon reachable but bot silent | `openclaw channels status --probe` | Verify `signal-cli` daemon URL/account and receive mode. | | DM blocked | `openclaw pairing list signal` | Approve sender or adjust DM policy. | | Group replies do not trigger | Check group allowlist and mention patterns | Add sender/group or loosen gating. | Full troubleshooting: [/channels/signal#troubleshooting](/channels/signal#troubleshooting) ## Matrix ### Matrix failure signatures | Symptom | Fastest check | Fix | | ----------------------------------- | -------------------------------------------- | ----------------------------------------------- | | Logged in but ignores room messages | `openclaw channels status --probe` | Check `groupPolicy` and room allowlist. | | DMs do not process | `openclaw pairing list matrix` | Approve sender or adjust DM policy. | | Encrypted rooms fail | Verify crypto module and encryption settings | Enable encryption support and rejoin/sync room. | Full troubleshooting: [/channels/matrix#troubleshooting](/channels/matrix#troubleshooting) # Twitch Source: https://cryptoclawdocs.termix.ai/channels/twitch # Twitch (plugin) Twitch chat support via IRC connection. OpenClaw connects as a Twitch user (bot account) to receive and send messages in channels. ## Plugin required Twitch ships as a plugin and is not bundled with the core install. Install via CLI (npm registry): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install @openclaw/twitch ``` Local checkout (when running from a git repo): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw plugins install ./extensions/twitch ``` Details: [Plugins](/tools/plugin) ## Quick setup (beginner) 1. Create a dedicated Twitch account for the bot (or use an existing account). 2. Generate credentials: [Twitch Token Generator](https://twitchtokengenerator.com/) * Select **Bot Token** * Verify scopes `chat:read` and `chat:write` are selected * Copy the **Client ID** and **Access Token** 3. Find your Twitch user ID: [https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/](https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/) 4. Configure the token: * Env: `OPENCLAW_TWITCH_ACCESS_TOKEN=...` (default account only) * Or config: `channels.twitch.accessToken` * If both are set, config takes precedence (env fallback is default-account only). 5. Start the gateway. **⚠️ Important:** Add access control (`allowFrom` or `allowedRoles`) to prevent unauthorized users from triggering the bot. `requireMention` defaults to `true`. Minimal config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { twitch: { enabled: true, username: "openclaw", // Bot's Twitch account accessToken: "oauth:abc123...", // OAuth Access Token (or use OPENCLAW_TWITCH_ACCESS_TOKEN env var) clientId: "xyz789...", // Client ID from Token Generator channel: "vevisk", // Which Twitch channel's chat to join (required) allowFrom: ["123456789"], // (recommended) Your Twitch user ID only - get it from https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/ }, }, } ``` ## What it is * A Twitch channel owned by the Gateway. * Deterministic routing: replies always go back to Twitch. * Each account maps to an isolated session key `agent::twitch:`. * `username` is the bot's account (who authenticates), `channel` is which chat room to join. ## Setup (detailed) ### Generate credentials Use [Twitch Token Generator](https://twitchtokengenerator.com/): * Select **Bot Token** * Verify scopes `chat:read` and `chat:write` are selected * Copy the **Client ID** and **Access Token** No manual app registration needed. Tokens expire after several hours. ### Configure the bot **Env var (default account only):** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} OPENCLAW_TWITCH_ACCESS_TOKEN=oauth:abc123... ``` **Or config:** ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { twitch: { enabled: true, username: "openclaw", accessToken: "oauth:abc123...", clientId: "xyz789...", channel: "vevisk", }, }, } ``` If both env and config are set, config takes precedence. ### Access control (recommended) ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { twitch: { allowFrom: ["123456789"], // (recommended) Your Twitch user ID only }, }, } ``` Prefer `allowFrom` for a hard allowlist. Use `allowedRoles` instead if you want role-based access. **Available roles:** `"moderator"`, `"owner"`, `"vip"`, `"subscriber"`, `"all"`. **Why user IDs?** Usernames can change, allowing impersonation. User IDs are permanent. Find your Twitch user ID: [https://www.streamweasels.com/tools/convert-twitch-username-%20to-user-id/](https://www.streamweasels.com/tools/convert-twitch-username-%20to-user-id/) (Convert your Twitch username to ID) ## Token refresh (optional) Tokens from [Twitch Token Generator](https://twitchtokengenerator.com/) cannot be automatically refreshed - regenerate when expired. For automatic token refresh, create your own Twitch application at [Twitch Developer Console](https://dev.twitch.tv/console) and add to config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { twitch: { clientSecret: "your_client_secret", refreshToken: "your_refresh_token", }, }, } ``` The bot automatically refreshes tokens before expiration and logs refresh events. ## Multi-account support Use `channels.twitch.accounts` with per-account tokens. See [`gateway/configuration`](/gateway/configuration) for the shared pattern. Example (one bot account in two channels): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { twitch: { accounts: { channel1: { username: "openclaw", accessToken: "oauth:abc123...", clientId: "xyz789...", channel: "vevisk", }, channel2: { username: "openclaw", accessToken: "oauth:def456...", clientId: "uvw012...", channel: "secondchannel", }, }, }, }, } ``` **Note:** Each account needs its own token (one token per channel). ## Access control ### Role-based restrictions ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { twitch: { accounts: { default: { allowedRoles: ["moderator", "vip"], }, }, }, }, } ``` ### Allowlist by User ID (most secure) ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { twitch: { accounts: { default: { allowFrom: ["123456789", "987654321"], }, }, }, }, } ``` ### Role-based access (alternative) `allowFrom` is a hard allowlist. When set, only those user IDs are allowed. If you want role-based access, leave `allowFrom` unset and configure `allowedRoles` instead: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { twitch: { accounts: { default: { allowedRoles: ["moderator"], }, }, }, }, } ``` ### Disable @mention requirement By default, `requireMention` is `true`. To disable and respond to all messages: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { twitch: { accounts: { default: { requireMention: false, }, }, }, }, } ``` ## Troubleshooting First, run diagnostic commands: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw doctor openclaw channels status --probe ``` ### Bot doesn't respond to messages **Check access control:** Ensure your user ID is in `allowFrom`, or temporarily remove `allowFrom` and set `allowedRoles: ["all"]` to test. **Check the bot is in the channel:** The bot must join the channel specified in `channel`. ### Token issues **"Failed to connect" or authentication errors:** * Verify `accessToken` is the OAuth access token value (typically starts with `oauth:` prefix) * Check token has `chat:read` and `chat:write` scopes * If using token refresh, verify `clientSecret` and `refreshToken` are set ### Token refresh not working **Check logs for refresh events:** ``` Using env token source for mybot Access token refreshed for user 123456 (expires in 14400s) ``` If you see "token refresh disabled (no refresh token)": * Ensure `clientSecret` is provided * Ensure `refreshToken` is provided ## Config **Account config:** * `username` - Bot username * `accessToken` - OAuth access token with `chat:read` and `chat:write` * `clientId` - Twitch Client ID (from Token Generator or your app) * `channel` - Channel to join (required) * `enabled` - Enable this account (default: `true`) * `clientSecret` - Optional: For automatic token refresh * `refreshToken` - Optional: For automatic token refresh * `expiresIn` - Token expiry in seconds * `obtainmentTimestamp` - Token obtained timestamp * `allowFrom` - User ID allowlist * `allowedRoles` - Role-based access control (`"moderator" | "owner" | "vip" | "subscriber" | "all"`) * `requireMention` - Require @mention (default: `true`) **Provider options:** * `channels.twitch.enabled` - Enable/disable channel startup * `channels.twitch.username` - Bot username (simplified single-account config) * `channels.twitch.accessToken` - OAuth access token (simplified single-account config) * `channels.twitch.clientId` - Twitch Client ID (simplified single-account config) * `channels.twitch.channel` - Channel to join (simplified single-account config) * `channels.twitch.accounts.` - Multi-account config (all account fields above) Full example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { twitch: { enabled: true, username: "openclaw", accessToken: "oauth:abc123...", clientId: "xyz789...", channel: "vevisk", clientSecret: "secret123...", refreshToken: "refresh456...", allowFrom: ["123456789"], allowedRoles: ["moderator", "vip"], accounts: { default: { username: "mybot", accessToken: "oauth:abc123...", clientId: "xyz789...", channel: "your_channel", enabled: true, clientSecret: "secret123...", refreshToken: "refresh456...", expiresIn: 14400, obtainmentTimestamp: 1706092800000, allowFrom: ["123456789", "987654321"], allowedRoles: ["moderator"], }, }, }, }, } ``` ## Tool actions The agent can call `twitch` with action: * `send` - Send a message to a channel Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { action: "twitch", params: { message: "Hello Twitch!", to: "#mychannel", }, } ``` ## Safety & ops * **Treat tokens like passwords** - Never commit tokens to git * **Use automatic token refresh** for long-running bots * **Use user ID allowlists** instead of usernames for access control * **Monitor logs** for token refresh events and connection status * **Scope tokens minimally** - Only request `chat:read` and `chat:write` * **If stuck**: Restart the gateway after confirming no other process owns the session ## Limits * **500 characters** per message (auto-chunked at word boundaries) * Markdown is stripped before chunking * No rate limiting (uses Twitch's built-in rate limits) # WhatsApp Source: https://cryptoclawdocs.termix.ai/channels/whatsapp # WhatsApp (Web channel) Status: production-ready via WhatsApp Web (Baileys). Gateway owns linked session(s). Default DM policy is pairing for unknown senders. Cross-channel diagnostics and repair playbooks. Full channel config patterns and examples. ## Quick setup ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { dmPolicy: "pairing", allowFrom: ["+15551234567"], groupPolicy: "allowlist", groupAllowFrom: ["+15551234567"], }, }, } ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw channels login --channel whatsapp ``` For a specific account: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw channels login --channel whatsapp --account work ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw pairing list whatsapp openclaw pairing approve whatsapp ``` Pairing requests expire after 1 hour. Pending requests are capped at 3 per channel. OpenClaw recommends running WhatsApp on a separate number when possible. (The channel metadata and onboarding flow are optimized for that setup, but personal-number setups are also supported.) ## Deployment patterns This is the cleanest operational mode: * separate WhatsApp identity for OpenClaw * clearer DM allowlists and routing boundaries * lower chance of self-chat confusion Minimal policy pattern: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { dmPolicy: "allowlist", allowFrom: ["+15551234567"], }, }, } ``` Onboarding supports personal-number mode and writes a self-chat-friendly baseline: * `dmPolicy: "allowlist"` * `allowFrom` includes your personal number * `selfChatMode: true` In runtime, self-chat protections key off the linked self number and `allowFrom`. The messaging platform channel is WhatsApp Web-based (`Baileys`) in current OpenClaw channel architecture. There is no separate Twilio WhatsApp messaging channel in the built-in chat-channel registry. ## Runtime model * Gateway owns the WhatsApp socket and reconnect loop. * Outbound sends require an active WhatsApp listener for the target account. * Status and broadcast chats are ignored (`@status`, `@broadcast`). * Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs to the agent main session). * Group sessions are isolated (`agent::whatsapp:group:`). ## Access control and activation `channels.whatsapp.dmPolicy` controls direct chat access: * `pairing` (default) * `allowlist` * `open` (requires `allowFrom` to include `"*"`) * `disabled` `allowFrom` accepts E.164-style numbers (normalized internally). Multi-account override: `channels.whatsapp.accounts..dmPolicy` (and `allowFrom`) take precedence over channel-level defaults for that account. Runtime behavior details: * pairings are persisted in channel allow-store and merged with configured `allowFrom` * if no allowlist is configured, the linked self number is allowed by default * outbound `fromMe` DMs are never auto-paired Group access has two layers: 1. **Group membership allowlist** (`channels.whatsapp.groups`) * if `groups` is omitted, all groups are eligible * if `groups` is present, it acts as a group allowlist (`"*"` allowed) 2. **Group sender policy** (`channels.whatsapp.groupPolicy` + `groupAllowFrom`) * `open`: sender allowlist bypassed * `allowlist`: sender must match `groupAllowFrom` (or `*`) * `disabled`: block all group inbound Sender allowlist fallback: * if `groupAllowFrom` is unset, runtime falls back to `allowFrom` when available * sender allowlists are evaluated before mention/reply activation Note: if no `channels.whatsapp` block exists at all, runtime group-policy fallback is `allowlist` (with a warning log), even if `channels.defaults.groupPolicy` is set. Group replies require mention by default. Mention detection includes: * explicit WhatsApp mentions of the bot identity * configured mention regex patterns (`agents.list[].groupChat.mentionPatterns`, fallback `messages.groupChat.mentionPatterns`) * implicit reply-to-bot detection (reply sender matches bot identity) Security note: * quote/reply only satisfies mention gating; it does **not** grant sender authorization * with `groupPolicy: "allowlist"`, non-allowlisted senders are still blocked even if they reply to an allowlisted user's message Session-level activation command: * `/activation mention` * `/activation always` `activation` updates session state (not global config). It is owner-gated. ## Personal-number and self-chat behavior When the linked self number is also present in `allowFrom`, WhatsApp self-chat safeguards activate: * skip read receipts for self-chat turns * ignore mention-JID auto-trigger behavior that would otherwise ping yourself * if `messages.responsePrefix` is unset, self-chat replies default to `[{identity.name}]` or `[openclaw]` ## Message normalization and context Incoming WhatsApp messages are wrapped in the shared inbound envelope. If a quoted reply exists, context is appended in this form: ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} [Replying to id:] [/Replying] ``` Reply metadata fields are also populated when available (`ReplyToId`, `ReplyToBody`, `ReplyToSender`, sender JID/E.164). Media-only inbound messages are normalized with placeholders such as: * `` * `` * `` * `` * `` Location and contact payloads are normalized into textual context before routing. For groups, unprocessed messages can be buffered and injected as context when the bot is finally triggered. * default limit: `50` * config: `channels.whatsapp.historyLimit` * fallback: `messages.groupChat.historyLimit` * `0` disables Injection markers: * `[Chat messages since your last reply - for context]` * `[Current message - respond to this]` Read receipts are enabled by default for accepted inbound WhatsApp messages. Disable globally: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { sendReadReceipts: false, }, }, } ``` Per-account override: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { accounts: { work: { sendReadReceipts: false, }, }, }, }, } ``` Self-chat turns skip read receipts even when globally enabled. ## Delivery, chunking, and media * default chunk limit: `channels.whatsapp.textChunkLimit = 4000` * `channels.whatsapp.chunkMode = "length" | "newline"` * `newline` mode prefers paragraph boundaries (blank lines), then falls back to length-safe chunking * supports image, video, audio (PTT voice-note), and document payloads * `audio/ogg` is rewritten to `audio/ogg; codecs=opus` for voice-note compatibility * animated GIF playback is supported via `gifPlayback: true` on video sends * captions are applied to the first media item when sending multi-media reply payloads * media source can be HTTP(S), `file://`, or local paths * inbound media save cap: `channels.whatsapp.mediaMaxMb` (default `50`) * outbound media cap for auto-replies: `agents.defaults.mediaMaxMb` (default `5MB`) * images are auto-optimized (resize/quality sweep) to fit limits * on media send failure, first-item fallback sends text warning instead of dropping the response silently ## Acknowledgment reactions WhatsApp supports immediate ack reactions on inbound receipt via `channels.whatsapp.ackReaction`. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { ackReaction: { emoji: "👀", direct: true, group: "mentions", // always | mentions | never }, }, }, } ``` Behavior notes: * sent immediately after inbound is accepted (pre-reply) * failures are logged but do not block normal reply delivery * group mode `mentions` reacts on mention-triggered turns; group activation `always` acts as bypass for this check * WhatsApp uses `channels.whatsapp.ackReaction` (legacy `messages.ackReaction` is not used here) ## Multi-account and credentials * account ids come from `channels.whatsapp.accounts` * default account selection: `default` if present, otherwise first configured account id (sorted) * account ids are normalized internally for lookup * current auth path: `~/.openclaw/credentials/whatsapp//creds.json` * backup file: `creds.json.bak` * legacy default auth in `~/.openclaw/credentials/` is still recognized/migrated for default-account flows `openclaw channels logout --channel whatsapp [--account ]` clears WhatsApp auth state for that account. In legacy auth directories, `oauth.json` is preserved while Baileys auth files are removed. ## Tools, actions, and config writes * Agent tool support includes WhatsApp reaction action (`react`). * Action gates: * `channels.whatsapp.actions.reactions` * `channels.whatsapp.actions.polls` * Channel-initiated config writes are enabled by default (disable via `channels.whatsapp.configWrites=false`). ## Troubleshooting Symptom: channel status reports not linked. Fix: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw channels login --channel whatsapp openclaw channels status ``` Symptom: linked account with repeated disconnects or reconnect attempts. Fix: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw doctor openclaw logs --follow ``` If needed, re-link with `channels login`. Outbound sends fail fast when no active gateway listener exists for the target account. Make sure gateway is running and the account is linked. Check in this order: * `groupPolicy` * `groupAllowFrom` / `allowFrom` * `groups` allowlist entries * mention gating (`requireMention` + mention patterns) * duplicate keys in `openclaw.json` (JSON5): later entries override earlier ones, so keep a single `groupPolicy` per scope WhatsApp gateway runtime should use Node. Bun is flagged as incompatible for stable WhatsApp/Telegram gateway operation. ## Configuration reference pointers Primary reference: * [Configuration reference - WhatsApp](/gateway/configuration-reference#whatsapp) High-signal WhatsApp fields: * access: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups` * delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction` * multi-account: `accounts..enabled`, `accounts..authDir`, account-level overrides * operations: `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*` * session behavior: `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms..historyLimit` ## Related * [Pairing](/channels/pairing) * [Channel routing](/channels/channel-routing) * [Multi-agent routing](/concepts/multi-agent) * [Troubleshooting](/channels/troubleshooting) # Zalo Source: https://cryptoclawdocs.termix.ai/channels/zalo # Zalo (Bot API) Status: experimental. DMs are supported; group handling is available with explicit group policy controls. ## Plugin required Zalo ships as a plugin and is not bundled with the core install. * Install via CLI: `openclaw plugins install @openclaw/zalo` * Or select **Zalo** during onboarding and confirm the install prompt * Details: [Plugins](/tools/plugin) ## Quick setup (beginner) 1. Install the Zalo plugin: * From a source checkout: `openclaw plugins install ./extensions/zalo` * From npm (if published): `openclaw plugins install @openclaw/zalo` * Or pick **Zalo** in onboarding and confirm the install prompt 2. Set the token: * Env: `ZALO_BOT_TOKEN=...` * Or config: `channels.zalo.botToken: "..."`. 3. Restart the gateway (or finish onboarding). 4. DM access is pairing by default; approve the pairing code on first contact. Minimal config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { zalo: { enabled: true, botToken: "12345689:abc-xyz", dmPolicy: "pairing", }, }, } ``` ## What it is Zalo is a Vietnam-focused messaging app; its Bot API lets the Gateway run a bot for 1:1 conversations. It is a good fit for support or notifications where you want deterministic routing back to Zalo. * A Zalo Bot API channel owned by the Gateway. * Deterministic routing: replies go back to Zalo; the model never chooses channels. * DMs share the agent's main session. * Groups are supported with policy controls (`groupPolicy` + `groupAllowFrom`) and default to fail-closed allowlist behavior. ## Setup (fast path) ### 1) Create a bot token (Zalo Bot Platform) 1. Go to [https://bot.zaloplatforms.com](https://bot.zaloplatforms.com) and sign in. 2. Create a new bot and configure its settings. 3. Copy the bot token (format: `12345689:abc-xyz`). ### 2) Configure the token (env or config) Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { zalo: { enabled: true, botToken: "12345689:abc-xyz", dmPolicy: "pairing", }, }, } ``` Env option: `ZALO_BOT_TOKEN=...` (works for the default account only). Multi-account support: use `channels.zalo.accounts` with per-account tokens and optional `name`. 3. Restart the gateway. Zalo starts when a token is resolved (env or config). 4. DM access defaults to pairing. Approve the code when the bot is first contacted. ## How it works (behavior) * Inbound messages are normalized into the shared channel envelope with media placeholders. * Replies always route back to the same Zalo chat. * Long-polling by default; webhook mode available with `channels.zalo.webhookUrl`. ## Limits * Outbound text is chunked to 2000 characters (Zalo API limit). * Media downloads/uploads are capped by `channels.zalo.mediaMaxMb` (default 5). * Streaming is blocked by default due to the 2000 char limit making streaming less useful. ## Access control (DMs) ### DM access * Default: `channels.zalo.dmPolicy = "pairing"`. Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour). * Approve via: * `openclaw pairing list zalo` * `openclaw pairing approve zalo ` * Pairing is the default token exchange. Details: [Pairing](/channels/pairing) * `channels.zalo.allowFrom` accepts numeric user IDs (no username lookup available). ## Access control (Groups) * `channels.zalo.groupPolicy` controls group inbound handling: `open | allowlist | disabled`. * Default behavior is fail-closed: `allowlist`. * `channels.zalo.groupAllowFrom` restricts which sender IDs can trigger the bot in groups. * If `groupAllowFrom` is unset, Zalo falls back to `allowFrom` for sender checks. * `groupPolicy: "disabled"` blocks all group messages. * `groupPolicy: "open"` allows any group member (mention-gated). * Runtime note: if `channels.zalo` is missing entirely, runtime still falls back to `groupPolicy="allowlist"` for safety. ## Long-polling vs webhook * Default: long-polling (no public URL required). * Webhook mode: set `channels.zalo.webhookUrl` and `channels.zalo.webhookSecret`. * The webhook secret must be 8-256 characters. * Webhook URL must use HTTPS. * Zalo sends events with `X-Bot-Api-Secret-Token` header for verification. * Gateway HTTP handles webhook requests at `channels.zalo.webhookPath` (defaults to the webhook URL path). * Requests must use `Content-Type: application/json` (or `+json` media types). * Duplicate events (`event_name + message_id`) are ignored for a short replay window. * Burst traffic is rate-limited per path/source and may return HTTP 429. **Note:** getUpdates (polling) and webhook are mutually exclusive per Zalo API docs. ## Supported message types * **Text messages**: Full support with 2000 character chunking. * **Image messages**: Download and process inbound images; send images via `sendPhoto`. * **Stickers**: Logged but not fully processed (no agent response). * **Unsupported types**: Logged (e.g., messages from protected users). ## Capabilities | Feature | Status | | --------------- | -------------------------------------------------------- | | Direct messages | ✅ Supported | | Groups | ⚠️ Supported with policy controls (allowlist by default) | | Media (images) | ✅ Supported | | Reactions | ❌ Not supported | | Threads | ❌ Not supported | | Polls | ❌ Not supported | | Native commands | ❌ Not supported | | Streaming | ⚠️ Blocked (2000 char limit) | ## Delivery targets (CLI/cron) * Use a chat id as the target. * Example: `openclaw message send --channel zalo --target 123456789 --message "hi"`. ## Troubleshooting **Bot doesn't respond:** * Check that the token is valid: `openclaw channels status --probe` * Verify the sender is approved (pairing or allowFrom) * Check gateway logs: `openclaw logs --follow` **Webhook not receiving events:** * Ensure webhook URL uses HTTPS * Verify secret token is 8-256 characters * Confirm the gateway HTTP endpoint is reachable on the configured path * Check that getUpdates polling is not running (they're mutually exclusive) ## Configuration reference (Zalo) Full configuration: [Configuration](/gateway/configuration) Provider options: * `channels.zalo.enabled`: enable/disable channel startup. * `channels.zalo.botToken`: bot token from Zalo Bot Platform. * `channels.zalo.tokenFile`: read token from file path. * `channels.zalo.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). * `channels.zalo.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`. The wizard will ask for numeric IDs. * `channels.zalo.groupPolicy`: `open | allowlist | disabled` (default: allowlist). * `channels.zalo.groupAllowFrom`: group sender allowlist (user IDs). Falls back to `allowFrom` when unset. * `channels.zalo.mediaMaxMb`: inbound/outbound media cap (MB, default 5). * `channels.zalo.webhookUrl`: enable webhook mode (HTTPS required). * `channels.zalo.webhookSecret`: webhook secret (8-256 chars). * `channels.zalo.webhookPath`: webhook path on the gateway HTTP server. * `channels.zalo.proxy`: proxy URL for API requests. Multi-account options: * `channels.zalo.accounts..botToken`: per-account token. * `channels.zalo.accounts..tokenFile`: per-account token file. * `channels.zalo.accounts..name`: display name. * `channels.zalo.accounts..enabled`: enable/disable account. * `channels.zalo.accounts..dmPolicy`: per-account DM policy. * `channels.zalo.accounts..allowFrom`: per-account allowlist. * `channels.zalo.accounts..groupPolicy`: per-account group policy. * `channels.zalo.accounts..groupAllowFrom`: per-account group sender allowlist. * `channels.zalo.accounts..webhookUrl`: per-account webhook URL. * `channels.zalo.accounts..webhookSecret`: per-account webhook secret. * `channels.zalo.accounts..webhookPath`: per-account webhook path. * `channels.zalo.accounts..proxy`: per-account proxy URL. # Zalo Personal Source: https://cryptoclawdocs.termix.ai/channels/zalouser # Zalo Personal (unofficial) Status: experimental. This integration automates a **personal Zalo account** via native `zca-js` inside OpenClaw. > **Warning:** This is an unofficial integration and may result in account suspension/ban. Use at your own risk. ## Plugin required Zalo Personal ships as a plugin and is not bundled with the core install. * Install via CLI: `openclaw plugins install @openclaw/zalouser` * Or from a source checkout: `openclaw plugins install ./extensions/zalouser` * Details: [Plugins](/tools/plugin) No external `zca`/`openzca` CLI binary is required. ## Quick setup (beginner) 1. Install the plugin (see above). 2. Login (QR, on the Gateway machine): * `openclaw channels login --channel zalouser` * Scan the QR code with the Zalo mobile app. 3. Enable the channel: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { zalouser: { enabled: true, dmPolicy: "pairing", }, }, } ``` 4. Restart the Gateway (or finish onboarding). 5. DM access defaults to pairing; approve the pairing code on first contact. ## What it is * Runs entirely in-process via `zca-js`. * Uses native event listeners to receive inbound messages. * Sends replies directly through the JS API (text/media/link). * Designed for “personal account” use cases where Zalo Bot API is not available. ## Naming Channel id is `zalouser` to make it explicit this automates a **personal Zalo user account** (unofficial). We keep `zalo` reserved for a potential future official Zalo API integration. ## Finding IDs (directory) Use the directory CLI to discover peers/groups and their IDs: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw directory self --channel zalouser openclaw directory peers list --channel zalouser --query "name" openclaw directory groups list --channel zalouser --query "work" ``` ## Limits * Outbound text is chunked to \~2000 characters (Zalo client limits). * Streaming is blocked by default. ## Access control (DMs) `channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`). `channels.zalouser.allowFrom` accepts user IDs or names. During onboarding, names are resolved to IDs using the plugin's in-process contact lookup. Approve via: * `openclaw pairing list zalouser` * `openclaw pairing approve zalouser ` ## Group access (optional) * Default: `channels.zalouser.groupPolicy = "open"` (groups allowed). Use `channels.defaults.groupPolicy` to override the default when unset. * Restrict to an allowlist with: * `channels.zalouser.groupPolicy = "allowlist"` * `channels.zalouser.groups` (keys are group IDs or names) * Block all groups: `channels.zalouser.groupPolicy = "disabled"`. * The configure wizard can prompt for group allowlists. * On startup, OpenClaw resolves group/user names in allowlists to IDs and logs the mapping; unresolved entries are kept as typed. Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { zalouser: { groupPolicy: "allowlist", groups: { "123456789": { allow: true }, "Work Chat": { allow: true }, }, }, }, } ``` ### Group mention gating * `channels.zalouser.groups..requireMention` controls whether group replies require a mention. * Resolution order: exact group id/name -> normalized group slug -> `*` -> default (`true`). * This applies both to allowlisted groups and open group mode. Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { zalouser: { groupPolicy: "allowlist", groups: { "*": { allow: true, requireMention: true }, "Work Chat": { allow: true, requireMention: false }, }, }, }, } ``` ## Multi-account Accounts map to `zalouser` profiles in OpenClaw state. Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { zalouser: { enabled: true, defaultAccount: "default", accounts: { work: { enabled: true, profile: "work" }, }, }, }, } ``` ## Typing, reactions, and delivery acknowledgements * OpenClaw sends a typing event before dispatching a reply (best-effort). * Message reaction action `react` is supported for `zalouser` in channel actions. * Use `remove: true` to remove a specific reaction emoji from a message. * Reaction semantics: [Reactions](/tools/reactions) * For inbound messages that include event metadata, OpenClaw sends delivered + seen acknowledgements (best-effort). ## Troubleshooting **Login doesn't stick:** * `openclaw channels status --probe` * Re-login: `openclaw channels logout --channel zalouser && openclaw channels login --channel zalouser` **Allowlist/group name didn't resolve:** * Use numeric IDs in `allowFrom`/`groups`, or exact friend/group names. **Upgraded from old CLI-based setup:** * Remove any old external `zca` process assumptions. * The channel now runs fully in OpenClaw without external CLI binaries. # Agent Runtime Source: https://cryptoclawdocs.termix.ai/concepts/agent # Agent Runtime 🤖 OpenClaw runs a single embedded agent runtime derived from **pi-mono**. ## Workspace (required) OpenClaw uses a single agent workspace directory (`agents.defaults.workspace`) as the agent’s **only** working directory (`cwd`) for tools and context. Recommended: use `openclaw setup` to create `~/.openclaw/openclaw.json` if missing and initialize the workspace files. Full workspace layout + backup guide: [Agent workspace](/concepts/agent-workspace) If `agents.defaults.sandbox` is enabled, non-main sessions can override this with per-session workspaces under `agents.defaults.sandbox.workspaceRoot` (see [Gateway configuration](/gateway/configuration)). ## Bootstrap files (injected) Inside `agents.defaults.workspace`, OpenClaw expects these user-editable files: * `AGENTS.md` — operating instructions + “memory” * `SOUL.md` — persona, boundaries, tone * `TOOLS.md` — user-maintained tool notes (e.g. `imsg`, `sag`, conventions) * `BOOTSTRAP.md` — one-time first-run ritual (deleted after completion) * `IDENTITY.md` — agent name/vibe/emoji * `USER.md` — user profile + preferred address On the first turn of a new session, OpenClaw injects the contents of these files directly into the agent context. Blank files are skipped. Large files are trimmed and truncated with a marker so prompts stay lean (read the file for full content). If a file is missing, OpenClaw injects a single “missing file” marker line (and `openclaw setup` will create a safe default template). `BOOTSTRAP.md` is only created for a **brand new workspace** (no other bootstrap files present). If you delete it after completing the ritual, it should not be recreated on later restarts. To disable bootstrap file creation entirely (for pre-seeded workspaces), set: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agent: { skipBootstrap: true } } ``` ## Built-in tools Core tools (read/exec/edit/write and related system tools) are always available, subject to tool policy. `apply_patch` is optional and gated by `tools.exec.applyPatch`. `TOOLS.md` does **not** control which tools exist; it’s guidance for how *you* want them used. ## Skills OpenClaw loads skills from three locations (workspace wins on name conflict): * Bundled (shipped with the install) * Managed/local: `~/.openclaw/skills` * Workspace: `/skills` Skills can be gated by config/env (see `skills` in [Gateway configuration](/gateway/configuration)). ## pi-mono integration OpenClaw reuses pieces of the pi-mono codebase (models/tools), but **session management, discovery, and tool wiring are OpenClaw-owned**. * No pi-coding agent runtime. * No `~/.pi/agent` or `/.pi` settings are consulted. ## Sessions Session transcripts are stored as JSONL at: * `~/.openclaw/agents//sessions/.jsonl` The session ID is stable and chosen by OpenClaw. Legacy Pi/Tau session folders are **not** read. ## Steering while streaming When queue mode is `steer`, inbound messages are injected into the current run. The queue is checked **after each tool call**; if a queued message is present, remaining tool calls from the current assistant message are skipped (error tool results with "Skipped due to queued user message."), then the queued user message is injected before the next assistant response. When queue mode is `followup` or `collect`, inbound messages are held until the current turn ends, then a new agent turn starts with the queued payloads. See [Queue](/concepts/queue) for mode + debounce/cap behavior. Block streaming sends completed assistant blocks as soon as they finish; it is **off by default** (`agents.defaults.blockStreamingDefault: "off"`). Tune the boundary via `agents.defaults.blockStreamingBreak` (`text_end` vs `message_end`; defaults to text\_end). Control soft block chunking with `agents.defaults.blockStreamingChunk` (defaults to 800–1200 chars; prefers paragraph breaks, then newlines; sentences last). Coalesce streamed chunks with `agents.defaults.blockStreamingCoalesce` to reduce single-line spam (idle-based merging before send). Non-Telegram channels require explicit `*.blockStreaming: true` to enable block replies. Verbose tool summaries are emitted at tool start (no debounce); Control UI streams tool output via agent events when available. More details: [Streaming + chunking](/concepts/streaming). ## Model refs Model refs in config (for example `agents.defaults.model` and `agents.defaults.models`) are parsed by splitting on the **first** `/`. * Use `provider/model` when configuring models. * If the model ID itself contains `/` (OpenRouter-style), include the provider prefix (example: `openrouter/moonshotai/kimi-k2`). * If you omit the provider, OpenClaw treats the input as an alias or a model for the **default provider** (only works when there is no `/` in the model ID). ## Configuration (minimal) At minimum, set: * `agents.defaults.workspace` * `channels.whatsapp.allowFrom` (strongly recommended) *** *Next: [Group Chats](/channels/group-messages)* 🦞 # Agent Loop Source: https://cryptoclawdocs.termix.ai/concepts/agent-loop # Agent Loop (OpenClaw) An agentic loop is the full “real” run of an agent: intake → context assembly → model inference → tool execution → streaming replies → persistence. It’s the authoritative path that turns a message into actions and a final reply, while keeping session state consistent. In OpenClaw, a loop is a single, serialized run per session that emits lifecycle and stream events as the model thinks, calls tools, and streams output. This doc explains how that authentic loop is wired end-to-end. ## Entry points * Gateway RPC: `agent` and `agent.wait`. * CLI: `agent` command. ## How it works (high-level) 1. `agent` RPC validates params, resolves session (sessionKey/sessionId), persists session metadata, returns `{ runId, acceptedAt }` immediately. 2. `agentCommand` runs the agent: * resolves model + thinking/verbose defaults * loads skills snapshot * calls `runEmbeddedPiAgent` (pi-agent-core runtime) * emits **lifecycle end/error** if the embedded loop does not emit one 3. `runEmbeddedPiAgent`: * serializes runs via per-session + global queues * resolves model + auth profile and builds the pi session * subscribes to pi events and streams assistant/tool deltas * enforces timeout -> aborts run if exceeded * returns payloads + usage metadata 4. `subscribeEmbeddedPiSession` bridges pi-agent-core events to OpenClaw `agent` stream: * tool events => `stream: "tool"` * assistant deltas => `stream: "assistant"` * lifecycle events => `stream: "lifecycle"` (`phase: "start" | "end" | "error"`) 5. `agent.wait` uses `waitForAgentJob`: * waits for **lifecycle end/error** for `runId` * returns `{ status: ok|error|timeout, startedAt, endedAt, error? }` ## Queueing + concurrency * Runs are serialized per session key (session lane) and optionally through a global lane. * This prevents tool/session races and keeps session history consistent. * Messaging channels can choose queue modes (collect/steer/followup) that feed this lane system. See [Command Queue](/concepts/queue). ## Session + workspace preparation * Workspace is resolved and created; sandboxed runs may redirect to a sandbox workspace root. * Skills are loaded (or reused from a snapshot) and injected into env and prompt. * Bootstrap/context files are resolved and injected into the system prompt report. * A session write lock is acquired; `SessionManager` is opened and prepared before streaming. ## Prompt assembly + system prompt * System prompt is built from OpenClaw’s base prompt, skills prompt, bootstrap context, and per-run overrides. * Model-specific limits and compaction reserve tokens are enforced. * See [System prompt](/concepts/system-prompt) for what the model sees. ## Hook points (where you can intercept) OpenClaw has two hook systems: * **Internal hooks** (Gateway hooks): event-driven scripts for commands and lifecycle events. * **Plugin hooks**: extension points inside the agent/tool lifecycle and gateway pipeline. ### Internal hooks (Gateway hooks) * **`agent:bootstrap`**: runs while building bootstrap files before the system prompt is finalized. Use this to add/remove bootstrap context files. * **Command hooks**: `/new`, `/reset`, `/stop`, and other command events (see Hooks doc). See [Hooks](/automation/hooks) for setup and examples. ### Plugin hooks (agent + gateway lifecycle) These run inside the agent loop or gateway pipeline: * **`before_model_resolve`**: runs pre-session (no `messages`) to deterministically override provider/model before model resolution. * **`before_prompt_build`**: runs after session load (with `messages`) to inject `prependContext`/`systemPrompt` before prompt submission. * **`before_agent_start`**: legacy compatibility hook that may run in either phase; prefer the explicit hooks above. * **`agent_end`**: inspect the final message list and run metadata after completion. * **`before_compaction` / `after_compaction`**: observe or annotate compaction cycles. * **`before_tool_call` / `after_tool_call`**: intercept tool params/results. * **`tool_result_persist`**: synchronously transform tool results before they are written to the session transcript. * **`message_received` / `message_sending` / `message_sent`**: inbound + outbound message hooks. * **`session_start` / `session_end`**: session lifecycle boundaries. * **`gateway_start` / `gateway_stop`**: gateway lifecycle events. See [Plugins](/tools/plugin#plugin-hooks) for the hook API and registration details. ## Streaming + partial replies * Assistant deltas are streamed from pi-agent-core and emitted as `assistant` events. * Block streaming can emit partial replies either on `text_end` or `message_end`. * Reasoning streaming can be emitted as a separate stream or as block replies. * See [Streaming](/concepts/streaming) for chunking and block reply behavior. ## Tool execution + messaging tools * Tool start/update/end events are emitted on the `tool` stream. * Tool results are sanitized for size and image payloads before logging/emitting. * Messaging tool sends are tracked to suppress duplicate assistant confirmations. ## Reply shaping + suppression * Final payloads are assembled from: * assistant text (and optional reasoning) * inline tool summaries (when verbose + allowed) * assistant error text when the model errors * `NO_REPLY` is treated as a silent token and filtered from outgoing payloads. * Messaging tool duplicates are removed from the final payload list. * If no renderable payloads remain and a tool errored, a fallback tool error reply is emitted (unless a messaging tool already sent a user-visible reply). ## Compaction + retries * Auto-compaction emits `compaction` stream events and can trigger a retry. * On retry, in-memory buffers and tool summaries are reset to avoid duplicate output. * See [Compaction](/concepts/compaction) for the compaction pipeline. ## Event streams (today) * `lifecycle`: emitted by `subscribeEmbeddedPiSession` (and as a fallback by `agentCommand`) * `assistant`: streamed deltas from pi-agent-core * `tool`: streamed tool events from pi-agent-core ## Chat channel handling * Assistant deltas are buffered into chat `delta` messages. * A chat `final` is emitted on **lifecycle end/error**. ## Timeouts * `agent.wait` default: 30s (just the wait). `timeoutMs` param overrides. * Agent runtime: `agents.defaults.timeoutSeconds` default 600s; enforced in `runEmbeddedPiAgent` abort timer. ## Where things can end early * Agent timeout (abort) * AbortSignal (cancel) * Gateway disconnect or RPC timeout * `agent.wait` timeout (wait-only, does not stop agent) # Agent Workspace Source: https://cryptoclawdocs.termix.ai/concepts/agent-workspace # Agent workspace The workspace is the agent's home. It is the only working directory used for file tools and for workspace context. Keep it private and treat it as memory. This is separate from `~/.openclaw/`, which stores config, credentials, and sessions. **Important:** the workspace is the **default cwd**, not a hard sandbox. Tools resolve relative paths against the workspace, but absolute paths can still reach elsewhere on the host unless sandboxing is enabled. If you need isolation, use [`agents.defaults.sandbox`](/gateway/sandboxing) (and/or per‑agent sandbox config). When sandboxing is enabled and `workspaceAccess` is not `"rw"`, tools operate inside a sandbox workspace under `~/.openclaw/sandboxes`, not your host workspace. ## Default location * Default: `~/.openclaw/workspace` * If `OPENCLAW_PROFILE` is set and not `"default"`, the default becomes `~/.openclaw/workspace-`. * Override in `~/.openclaw/openclaw.json`: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agent: { workspace: "~/.openclaw/workspace", }, } ``` `openclaw onboard`, `openclaw configure`, or `openclaw setup` will create the workspace and seed the bootstrap files if they are missing. Sandbox seed copies only accept regular in-workspace files; symlink/hardlink aliases that resolve outside the source workspace are ignored. If you already manage the workspace files yourself, you can disable bootstrap file creation: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agent: { skipBootstrap: true } } ``` ## Extra workspace folders Older installs may have created `~/openclaw`. Keeping multiple workspace directories around can cause confusing auth or state drift, because only one workspace is active at a time. **Recommendation:** keep a single active workspace. If you no longer use the extra folders, archive or move them to Trash (for example `trash ~/openclaw`). If you intentionally keep multiple workspaces, make sure `agents.defaults.workspace` points to the active one. `openclaw doctor` warns when it detects extra workspace directories. ## Workspace file map (what each file means) These are the standard files OpenClaw expects inside the workspace: * `AGENTS.md` * Operating instructions for the agent and how it should use memory. * Loaded at the start of every session. * Good place for rules, priorities, and "how to behave" details. * `SOUL.md` * Persona, tone, and boundaries. * Loaded every session. * `USER.md` * Who the user is and how to address them. * Loaded every session. * `IDENTITY.md` * The agent's name, vibe, and emoji. * Created/updated during the bootstrap ritual. * `TOOLS.md` * Notes about your local tools and conventions. * Does not control tool availability; it is only guidance. * `HEARTBEAT.md` * Optional tiny checklist for heartbeat runs. * Keep it short to avoid token burn. * `BOOT.md` * Optional startup checklist executed on gateway restart when internal hooks are enabled. * Keep it short; use the message tool for outbound sends. * `BOOTSTRAP.md` * One-time first-run ritual. * Only created for a brand-new workspace. * Delete it after the ritual is complete. * `memory/YYYY-MM-DD.md` * Daily memory log (one file per day). * Recommended to read today + yesterday on session start. * `MEMORY.md` (optional) * Curated long-term memory. * Only load in the main, private session (not shared/group contexts). See [Memory](/concepts/memory) for the workflow and automatic memory flush. * `skills/` (optional) * Workspace-specific skills. * Overrides managed/bundled skills when names collide. * `canvas/` (optional) * Canvas UI files for node displays (for example `canvas/index.html`). If any bootstrap file is missing, OpenClaw injects a "missing file" marker into the session and continues. Large bootstrap files are truncated when injected; adjust limits with `agents.defaults.bootstrapMaxChars` (default: 20000) and `agents.defaults.bootstrapTotalMaxChars` (default: 150000). `openclaw setup` can recreate missing defaults without overwriting existing files. ## What is NOT in the workspace These live under `~/.openclaw/` and should NOT be committed to the workspace repo: * `~/.openclaw/openclaw.json` (config) * `~/.openclaw/credentials/` (OAuth tokens, API keys) * `~/.openclaw/agents//sessions/` (session transcripts + metadata) * `~/.openclaw/skills/` (managed skills) If you need to migrate sessions or config, copy them separately and keep them out of version control. ## Git backup (recommended, private) Treat the workspace as private memory. Put it in a **private** git repo so it is backed up and recoverable. Run these steps on the machine where the Gateway runs (that is where the workspace lives). ### 1) Initialize the repo If git is installed, brand-new workspaces are initialized automatically. If this workspace is not already a repo, run: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} cd ~/.openclaw/workspace git init git add AGENTS.md SOUL.md TOOLS.md IDENTITY.md USER.md HEARTBEAT.md memory/ git commit -m "Add agent workspace" ``` ### 2) Add a private remote (beginner-friendly options) Option A: GitHub web UI 1. Create a new **private** repository on GitHub. 2. Do not initialize with a README (avoids merge conflicts). 3. Copy the HTTPS remote URL. 4. Add the remote and push: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} git branch -M main git remote add origin git push -u origin main ``` Option B: GitHub CLI (`gh`) ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} gh auth login gh repo create openclaw-workspace --private --source . --remote origin --push ``` Option C: GitLab web UI 1. Create a new **private** repository on GitLab. 2. Do not initialize with a README (avoids merge conflicts). 3. Copy the HTTPS remote URL. 4. Add the remote and push: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} git branch -M main git remote add origin git push -u origin main ``` ### 3) Ongoing updates ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} git status git add . git commit -m "Update memory" git push ``` ## Do not commit secrets Even in a private repo, avoid storing secrets in the workspace: * API keys, OAuth tokens, passwords, or private credentials. * Anything under `~/.openclaw/`. * Raw dumps of chats or sensitive attachments. If you must store sensitive references, use placeholders and keep the real secret elsewhere (password manager, environment variables, or `~/.openclaw/`). Suggested `.gitignore` starter: ```gitignore theme={"theme":{"light":"min-light","dark":"min-dark"}} .DS_Store .env **/*.key **/*.pem **/secrets* ``` ## Moving the workspace to a new machine 1. Clone the repo to the desired path (default `~/.openclaw/workspace`). 2. Set `agents.defaults.workspace` to that path in `~/.openclaw/openclaw.json`. 3. Run `openclaw setup --workspace ` to seed any missing files. 4. If you need sessions, copy `~/.openclaw/agents//sessions/` from the old machine separately. ## Advanced notes * Multi-agent routing can use different workspaces per agent. See [Channel routing](/channels/channel-routing) for routing configuration. * If `agents.defaults.sandbox` is enabled, non-main sessions can use per-session sandbox workspaces under `agents.defaults.sandbox.workspaceRoot`. # Gateway Architecture Source: https://cryptoclawdocs.termix.ai/concepts/architecture # Gateway architecture Last updated: 2026-01-22 ## Overview * A single long‑lived **Gateway** owns all messaging surfaces (WhatsApp via Baileys, Telegram via grammY, Slack, Discord, Signal, iMessage, WebChat). * Control-plane clients (macOS app, CLI, web UI, automations) connect to the Gateway over **WebSocket** on the configured bind host (default `127.0.0.1:18789`). * **Nodes** (macOS/iOS/Android/headless) also connect over **WebSocket**, but declare `role: node` with explicit caps/commands. * One Gateway per host; it is the only place that opens a WhatsApp session. * The **canvas host** is served by the Gateway HTTP server under: * `/__openclaw__/canvas/` (agent-editable HTML/CSS/JS) * `/__openclaw__/a2ui/` (A2UI host) It uses the same port as the Gateway (default `18789`). ## Components and flows ### Gateway (daemon) * Maintains provider connections. * Exposes a typed WS API (requests, responses, server‑push events). * Validates inbound frames against JSON Schema. * Emits events like `agent`, `chat`, `presence`, `health`, `heartbeat`, `cron`. ### Clients (mac app / CLI / web admin) * One WS connection per client. * Send requests (`health`, `status`, `send`, `agent`, `system-presence`). * Subscribe to events (`tick`, `agent`, `presence`, `shutdown`). ### Nodes (macOS / iOS / Android / headless) * Connect to the **same WS server** with `role: node`. * Provide a device identity in `connect`; pairing is **device‑based** (role `node`) and approval lives in the device pairing store. * Expose commands like `canvas.*`, `camera.*`, `screen.record`, `location.get`. Protocol details: * [Gateway protocol](/gateway/protocol) ### WebChat * Static UI that uses the Gateway WS API for chat history and sends. * In remote setups, connects through the same SSH/Tailscale tunnel as other clients. ## Connection lifecycle (single client) ```mermaid theme={"theme":{"light":"min-light","dark":"min-dark"}} sequenceDiagram participant Client participant Gateway Client->>Gateway: req:connect Gateway-->>Client: res (ok) Note right of Gateway: or res error + close Note left of Client: payload=hello-ok
snapshot: presence + health Gateway-->>Client: event:presence Gateway-->>Client: event:tick Client->>Gateway: req:agent Gateway-->>Client: res:agent
ack {runId, status:"accepted"} Gateway-->>Client: event:agent
(streaming) Gateway-->>Client: res:agent
final {runId, status, summary} ``` ## Wire protocol (summary) * Transport: WebSocket, text frames with JSON payloads. * First frame **must** be `connect`. * After handshake: * Requests: `{type:"req", id, method, params}` → `{type:"res", id, ok, payload|error}` * Events: `{type:"event", event, payload, seq?, stateVersion?}` * If `OPENCLAW_GATEWAY_TOKEN` (or `--token`) is set, `connect.params.auth.token` must match or the socket closes. * Idempotency keys are required for side‑effecting methods (`send`, `agent`) to safely retry; the server keeps a short‑lived dedupe cache. * Nodes must include `role: "node"` plus caps/commands/permissions in `connect`. ## Pairing + local trust * All WS clients (operators + nodes) include a **device identity** on `connect`. * New device IDs require pairing approval; the Gateway issues a **device token** for subsequent connects. * **Local** connects (loopback or the gateway host’s own tailnet address) can be auto‑approved to keep same‑host UX smooth. * All connects must sign the `connect.challenge` nonce. * Signature payload `v3` also binds `platform` + `deviceFamily`; the gateway pins paired metadata on reconnect and requires repair pairing for metadata changes. * **Non‑local** connects still require explicit approval. * Gateway auth (`gateway.auth.*`) still applies to **all** connections, local or remote. Details: [Gateway protocol](/gateway/protocol), [Pairing](/channels/pairing), [Security](/gateway/security). ## Protocol typing and codegen * TypeBox schemas define the protocol. * JSON Schema is generated from those schemas. * Swift models are generated from the JSON Schema. ## Remote access * Preferred: Tailscale or VPN. * Alternative: SSH tunnel ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ssh -N -L 18789:127.0.0.1:18789 user@host ``` * The same handshake + auth token apply over the tunnel. * TLS + optional pinning can be enabled for WS in remote setups. ## Operations snapshot * Start: `openclaw gateway` (foreground, logs to stdout). * Health: `health` over WS (also included in `hello-ok`). * Supervision: launchd/systemd for auto‑restart. ## Invariants * Exactly one Gateway controls a single Baileys session per host. * Handshake is mandatory; any non‑JSON or non‑connect first frame is a hard close. * Events are not replayed; clients must refresh on gaps. # Compaction Source: https://cryptoclawdocs.termix.ai/concepts/compaction # Context Window & Compaction Every model has a **context window** (max tokens it can see). Long-running chats accumulate messages and tool results; once the window is tight, OpenClaw **compacts** older history to stay within limits. ## What compaction is Compaction **summarizes older conversation** into a compact summary entry and keeps recent messages intact. The summary is stored in the session history, so future requests use: * The compaction summary * Recent messages after the compaction point Compaction **persists** in the session’s JSONL history. ## Configuration Use the `agents.defaults.compaction` setting in your `openclaw.json` to configure compaction behavior (mode, target tokens, etc.). Compaction summarization preserves opaque identifiers by default (`identifierPolicy: "strict"`). You can override this with `identifierPolicy: "off"` or provide custom text with `identifierPolicy: "custom"` and `identifierInstructions`. ## Auto-compaction (default on) When a session nears or exceeds the model’s context window, OpenClaw triggers auto-compaction and may retry the original request using the compacted context. You’ll see: * `🧹 Auto-compaction complete` in verbose mode * `/status` showing `🧹 Compactions: ` Before compaction, OpenClaw can run a **silent memory flush** turn to store durable notes to disk. See [Memory](/concepts/memory) for details and config. ## Manual compaction Use `/compact` (optionally with instructions) to force a compaction pass: ``` /compact Focus on decisions and open questions ``` ## Context window source Context window is model-specific. OpenClaw uses the model definition from the configured provider catalog to determine limits. ## Compaction vs pruning * **Compaction**: summarises and **persists** in JSONL. * **Session pruning**: trims old **tool results** only, **in-memory**, per request. See [/concepts/session-pruning](/concepts/session-pruning) for pruning details. ## OpenAI server-side compaction OpenClaw also supports OpenAI Responses server-side compaction hints for compatible direct OpenAI models. This is separate from local OpenClaw compaction and can run alongside it. * Local compaction: OpenClaw summarizes and persists into session JSONL. * Server-side compaction: OpenAI compacts context on the provider side when `store` + `context_management` are enabled. See [OpenAI provider](/providers/openai) for model params and overrides. ## Tips * Use `/compact` when sessions feel stale or context is bloated. * Large tool outputs are already truncated; pruning can further reduce tool-result buildup. * If you need a fresh slate, `/new` or `/reset` starts a new session id. # Context Source: https://cryptoclawdocs.termix.ai/concepts/context # Context “Context” is **everything OpenClaw sends to the model for a run**. It is bounded by the model’s **context window** (token limit). Beginner mental model: * **System prompt** (OpenClaw-built): rules, tools, skills list, time/runtime, and injected workspace files. * **Conversation history**: your messages + the assistant’s messages for this session. * **Tool calls/results + attachments**: command output, file reads, images/audio, etc. Context is *not the same thing* as “memory”: memory can be stored on disk and reloaded later; context is what’s inside the model’s current window. ## Quick start (inspect context) * `/status` → quick “how full is my window?” view + session settings. * `/context list` → what’s injected + rough sizes (per file + totals). * `/context detail` → deeper breakdown: per-file, per-tool schema sizes, per-skill entry sizes, and system prompt size. * `/usage tokens` → append per-reply usage footer to normal replies. * `/compact` → summarize older history into a compact entry to free window space. See also: [Slash commands](/tools/slash-commands), [Token use & costs](/reference/token-use), [Compaction](/concepts/compaction). ## Example output Values vary by model, provider, tool policy, and what’s in your workspace. ### `/context list` ``` 🧠 Context breakdown Workspace: Bootstrap max/file: 20,000 chars Sandbox: mode=non-main sandboxed=false System prompt (run): 38,412 chars (~9,603 tok) (Project Context 23,901 chars (~5,976 tok)) Injected workspace files: - AGENTS.md: OK | raw 1,742 chars (~436 tok) | injected 1,742 chars (~436 tok) - SOUL.md: OK | raw 912 chars (~228 tok) | injected 912 chars (~228 tok) - TOOLS.md: TRUNCATED | raw 54,210 chars (~13,553 tok) | injected 20,962 chars (~5,241 tok) - IDENTITY.md: OK | raw 211 chars (~53 tok) | injected 211 chars (~53 tok) - USER.md: OK | raw 388 chars (~97 tok) | injected 388 chars (~97 tok) - HEARTBEAT.md: MISSING | raw 0 | injected 0 - BOOTSTRAP.md: OK | raw 0 chars (~0 tok) | injected 0 chars (~0 tok) Skills list (system prompt text): 2,184 chars (~546 tok) (12 skills) Tools: read, edit, write, exec, process, browser, message, sessions_send, … Tool list (system prompt text): 1,032 chars (~258 tok) Tool schemas (JSON): 31,988 chars (~7,997 tok) (counts toward context; not shown as text) Tools: (same as above) Session tokens (cached): 14,250 total / ctx=32,000 ``` ### `/context detail` ``` 🧠 Context breakdown (detailed) … Top skills (prompt entry size): - frontend-design: 412 chars (~103 tok) - oracle: 401 chars (~101 tok) … (+10 more skills) Top tools (schema size): - browser: 9,812 chars (~2,453 tok) - exec: 6,240 chars (~1,560 tok) … (+N more tools) ``` ## What counts toward the context window Everything the model receives counts, including: * System prompt (all sections). * Conversation history. * Tool calls + tool results. * Attachments/transcripts (images/audio/files). * Compaction summaries and pruning artifacts. * Provider “wrappers” or hidden headers (not visible, still counted). ## How OpenClaw builds the system prompt The system prompt is **OpenClaw-owned** and rebuilt each run. It includes: * Tool list + short descriptions. * Skills list (metadata only; see below). * Workspace location. * Time (UTC + converted user time if configured). * Runtime metadata (host/OS/model/thinking). * Injected workspace bootstrap files under **Project Context**. Full breakdown: [System Prompt](/concepts/system-prompt). ## Injected workspace files (Project Context) By default, OpenClaw injects a fixed set of workspace files (if present): * `AGENTS.md` * `SOUL.md` * `TOOLS.md` * `IDENTITY.md` * `USER.md` * `HEARTBEAT.md` * `BOOTSTRAP.md` (first-run only) Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (default `20000` chars). OpenClaw also enforces a total bootstrap injection cap across files with `agents.defaults.bootstrapTotalMaxChars` (default `150000` chars). `/context` shows **raw vs injected** sizes and whether truncation happened. When truncation occurs, the runtime can inject an in-prompt warning block under Project Context. Configure this with `agents.defaults.bootstrapPromptTruncationWarning` (`off`, `once`, `always`; default `once`). ## Skills: what’s injected vs loaded on-demand The system prompt includes a compact **skills list** (name + description + location). This list has real overhead. Skill instructions are *not* included by default. The model is expected to `read` the skill’s `SKILL.md` **only when needed**. ## Tools: there are two costs Tools affect context in two ways: 1. **Tool list text** in the system prompt (what you see as “Tooling”). 2. **Tool schemas** (JSON). These are sent to the model so it can call tools. They count toward context even though you don’t see them as plain text. `/context detail` breaks down the biggest tool schemas so you can see what dominates. ## Commands, directives, and “inline shortcuts” Slash commands are handled by the Gateway. There are a few different behaviors: * **Standalone commands**: a message that is only `/...` runs as a command. * **Directives**: `/think`, `/verbose`, `/reasoning`, `/elevated`, `/model`, `/queue` are stripped before the model sees the message. * Directive-only messages persist session settings. * Inline directives in a normal message act as per-message hints. * **Inline shortcuts** (allowlisted senders only): certain `/...` tokens inside a normal message can run immediately (example: “hey /status”), and are stripped before the model sees the remaining text. Details: [Slash commands](/tools/slash-commands). ## Sessions, compaction, and pruning (what persists) What persists across messages depends on the mechanism: * **Normal history** persists in the session transcript until compacted/pruned by policy. * **Compaction** persists a summary into the transcript and keeps recent messages intact. * **Pruning** removes old tool results from the *in-memory* prompt for a run, but does not rewrite the transcript. Docs: [Session](/concepts/session), [Compaction](/concepts/compaction), [Session pruning](/concepts/session-pruning). ## What `/context` actually reports `/context` prefers the latest **run-built** system prompt report when available: * `System prompt (run)` = captured from the last embedded (tool-capable) run and persisted in the session store. * `System prompt (estimate)` = computed on the fly when no run report exists (or when running via a CLI backend that doesn’t generate the report). Either way, it reports sizes and top contributors; it does **not** dump the full system prompt or tool schemas. # Features Source: https://cryptoclawdocs.termix.ai/concepts/features ## Highlights WhatsApp, Telegram, Discord, and iMessage with a single Gateway. Add Mattermost and more with extensions. Multi-agent routing with isolated sessions. Images, audio, and documents in and out. Web Control UI and macOS companion app. iOS and Android nodes with pairing, voice/chat, and rich device commands. ## Full list * WhatsApp integration via WhatsApp Web (Baileys) * Telegram bot support (grammY) * Discord bot support (channels.discord.js) * Mattermost bot support (plugin) * iMessage integration via local imsg CLI (macOS) * Agent bridge for Pi in RPC mode with tool streaming * Streaming and chunking for long responses * Multi-agent routing for isolated sessions per workspace or sender * Subscription auth for Anthropic and OpenAI via OAuth * Sessions: direct chats collapse into shared `main`; groups are isolated * Group chat support with mention based activation * Media support for images, audio, and documents * Optional voice note transcription hook * WebChat and macOS menu bar app * iOS node with pairing, Canvas, camera, screen recording, location, and voice features * Android node with pairing, Connect tab, chat sessions, voice tab, Canvas/camera/screen, plus device, notifications, contacts/calendar, motion, photos, SMS, and app update commands Legacy Claude, Codex, Gemini, and Opencode paths have been removed. Pi is the only coding agent path. # Memory Source: https://cryptoclawdocs.termix.ai/concepts/memory # Memory OpenClaw memory is **plain Markdown in the agent workspace**. The files are the source of truth; the model only "remembers" what gets written to disk. Memory search tools are provided by the active memory plugin (default: `memory-core`). Disable memory plugins with `plugins.slots.memory = "none"`. ## Memory files (Markdown) The default workspace layout uses two memory layers: * `memory/YYYY-MM-DD.md` * Daily log (append-only). * Read today + yesterday at session start. * `MEMORY.md` (optional) * Curated long-term memory. * **Only load in the main, private session** (never in group contexts). These files live under the workspace (`agents.defaults.workspace`, default `~/.openclaw/workspace`). See [Agent workspace](/concepts/agent-workspace) for the full layout. ## Memory tools OpenClaw exposes two agent-facing tools for these Markdown files: * `memory_search` — semantic recall over indexed snippets. * `memory_get` — targeted read of a specific Markdown file/line range. `memory_get` now **degrades gracefully when a file doesn't exist** (for example, today's daily log before the first write). Both the builtin manager and the QMD backend return `{ text: "", path }` instead of throwing `ENOENT`, so agents can handle "nothing recorded yet" and continue their workflow without wrapping the tool call in try/catch logic. ## When to write memory * Decisions, preferences, and durable facts go to `MEMORY.md`. * Day-to-day notes and running context go to `memory/YYYY-MM-DD.md`. * If someone says "remember this," write it down (do not keep it in RAM). * This area is still evolving. It helps to remind the model to store memories; it will know what to do. * If you want something to stick, **ask the bot to write it** into memory. ## Automatic memory flush (pre-compaction ping) When a session is **close to auto-compaction**, OpenClaw triggers a **silent, agentic turn** that reminds the model to write durable memory **before** the context is compacted. The default prompts explicitly say the model *may reply*, but usually `NO_REPLY` is the correct response so the user never sees this turn. This is controlled by `agents.defaults.compaction.memoryFlush`: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { defaults: { compaction: { reserveTokensFloor: 20000, memoryFlush: { enabled: true, softThresholdTokens: 4000, systemPrompt: "Session nearing compaction. Store durable memories now.", prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.", }, }, }, }, } ``` Details: * **Soft threshold**: flush triggers when the session token estimate crosses `contextWindow - reserveTokensFloor - softThresholdTokens`. * **Silent** by default: prompts include `NO_REPLY` so nothing is delivered. * **Two prompts**: a user prompt plus a system prompt append the reminder. * **One flush per compaction cycle** (tracked in `sessions.json`). * **Workspace must be writable**: if the session runs sandboxed with `workspaceAccess: "ro"` or `"none"`, the flush is skipped. For the full compaction lifecycle, see [Session management + compaction](/reference/session-management-compaction). ## Vector memory search OpenClaw can build a small vector index over `MEMORY.md` and `memory/*.md` so semantic queries can find related notes even when wording differs. Defaults: * Enabled by default. * Watches memory files for changes (debounced). * Configure memory search under `agents.defaults.memorySearch` (not top-level `memorySearch`). * Uses remote embeddings by default. If `memorySearch.provider` is not set, OpenClaw auto-selects: 1. `local` if a `memorySearch.local.modelPath` is configured and the file exists. 2. `openai` if an OpenAI key can be resolved. 3. `gemini` if a Gemini key can be resolved. 4. `voyage` if a Voyage key can be resolved. 5. `mistral` if a Mistral key can be resolved. 6. Otherwise memory search stays disabled until configured. * Local mode uses node-llama-cpp and may require `pnpm approve-builds`. * Uses sqlite-vec (when available) to accelerate vector search inside SQLite. * `memorySearch.provider = "ollama"` is also supported for local/self-hosted Ollama embeddings (`/api/embeddings`), but it is not auto-selected. Remote embeddings **require** an API key for the embedding provider. OpenClaw resolves keys from auth profiles, `models.providers.*.apiKey`, or environment variables. Codex OAuth only covers chat/completions and does **not** satisfy embeddings for memory search. For Gemini, use `GEMINI_API_KEY` or `models.providers.google.apiKey`. For Voyage, use `VOYAGE_API_KEY` or `models.providers.voyage.apiKey`. For Mistral, use `MISTRAL_API_KEY` or `models.providers.mistral.apiKey`. Ollama typically does not require a real API key (a placeholder like `OLLAMA_API_KEY=ollama-local` is enough when needed by local policy). When using a custom OpenAI-compatible endpoint, set `memorySearch.remote.apiKey` (and optional `memorySearch.remote.headers`). ### QMD backend (experimental) Set `memory.backend = "qmd"` to swap the built-in SQLite indexer for [QMD](https://github.com/tobi/qmd): a local-first search sidecar that combines BM25 + vectors + reranking. Markdown stays the source of truth; OpenClaw shells out to QMD for retrieval. Key points: **Prereqs** * Disabled by default. Opt in per-config (`memory.backend = "qmd"`). * Install the QMD CLI separately (`bun install -g https://github.com/tobi/qmd` or grab a release) and make sure the `qmd` binary is on the gateway’s `PATH`. * QMD needs an SQLite build that allows extensions (`brew install sqlite` on macOS). * QMD runs fully locally via Bun + `node-llama-cpp` and auto-downloads GGUF models from HuggingFace on first use (no separate Ollama daemon required). * The gateway runs QMD in a self-contained XDG home under `~/.openclaw/agents//qmd/` by setting `XDG_CONFIG_HOME` and `XDG_CACHE_HOME`. * OS support: macOS and Linux work out of the box once Bun + SQLite are installed. Windows is best supported via WSL2. **How the sidecar runs** * The gateway writes a self-contained QMD home under `~/.openclaw/agents//qmd/` (config + cache + sqlite DB). * Collections are created via `qmd collection add` from `memory.qmd.paths` (plus default workspace memory files), then `qmd update` + `qmd embed` run on boot and on a configurable interval (`memory.qmd.update.interval`, default 5 m). * The gateway now initializes the QMD manager on startup, so periodic update timers are armed even before the first `memory_search` call. * Boot refresh now runs in the background by default so chat startup is not blocked; set `memory.qmd.update.waitForBootSync = true` to keep the previous blocking behavior. * Searches run via `memory.qmd.searchMode` (default `qmd search --json`; also supports `vsearch` and `query`). If the selected mode rejects flags on your QMD build, OpenClaw retries with `qmd query`. If QMD fails or the binary is missing, OpenClaw automatically falls back to the builtin SQLite manager so memory tools keep working. * OpenClaw does not expose QMD embed batch-size tuning today; batch behavior is controlled by QMD itself. * **First search may be slow**: QMD may download local GGUF models (reranker/query expansion) on the first `qmd query` run. * OpenClaw sets `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` automatically when it runs QMD. * If you want to pre-download models manually (and warm the same index OpenClaw uses), run a one-off query with the agent’s XDG dirs. OpenClaw’s QMD state lives under your **state dir** (defaults to `~/.openclaw`). You can point `qmd` at the exact same index by exporting the same XDG vars OpenClaw uses: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Pick the same state dir OpenClaw uses STATE_DIR="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}" export XDG_CONFIG_HOME="$STATE_DIR/agents/main/qmd/xdg-config" export XDG_CACHE_HOME="$STATE_DIR/agents/main/qmd/xdg-cache" # (Optional) force an index refresh + embeddings qmd update qmd embed # Warm up / trigger first-time model downloads qmd query "test" -c memory-root --json >/dev/null 2>&1 ``` **Config surface (`memory.qmd.*`)** * `command` (default `qmd`): override the executable path. * `searchMode` (default `search`): pick which QMD command backs `memory_search` (`search`, `vsearch`, `query`). * `includeDefaultMemory` (default `true`): auto-index `MEMORY.md` + `memory/**/*.md`. * `paths[]`: add extra directories/files (`path`, optional `pattern`, optional stable `name`). * `sessions`: opt into session JSONL indexing (`enabled`, `retentionDays`, `exportDir`). * `update`: controls refresh cadence and maintenance execution: (`interval`, `debounceMs`, `onBoot`, `waitForBootSync`, `embedInterval`, `commandTimeoutMs`, `updateTimeoutMs`, `embedTimeoutMs`). * `limits`: clamp recall payload (`maxResults`, `maxSnippetChars`, `maxInjectedChars`, `timeoutMs`). * `scope`: same schema as [`session.sendPolicy`](/gateway/configuration#session). Default is DM-only (`deny` all, `allow` direct chats); loosen it to surface QMD hits in groups/channels. * `match.keyPrefix` matches the **normalized** session key (lowercased, with any leading `agent::` stripped). Example: `discord:channel:`. * `match.rawKeyPrefix` matches the **raw** session key (lowercased), including `agent::`. Example: `agent:main:discord:`. * Legacy: `match.keyPrefix: "agent:..."` is still treated as a raw-key prefix, but prefer `rawKeyPrefix` for clarity. * When `scope` denies a search, OpenClaw logs a warning with the derived `channel`/`chatType` so empty results are easier to debug. * Snippets sourced outside the workspace show up as `qmd//` in `memory_search` results; `memory_get` understands that prefix and reads from the configured QMD collection root. * When `memory.qmd.sessions.enabled = true`, OpenClaw exports sanitized session transcripts (User/Assistant turns) into a dedicated QMD collection under `~/.openclaw/agents//qmd/sessions/`, so `memory_search` can recall recent conversations without touching the builtin SQLite index. * `memory_search` snippets now include a `Source: ` footer when `memory.citations` is `auto`/`on`; set `memory.citations = "off"` to keep the path metadata internal (the agent still receives the path for `memory_get`, but the snippet text omits the footer and the system prompt warns the agent not to cite it). **Example** ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} memory: { backend: "qmd", citations: "auto", qmd: { includeDefaultMemory: true, update: { interval: "5m", debounceMs: 15000 }, limits: { maxResults: 6, timeoutMs: 4000 }, scope: { default: "deny", rules: [ { action: "allow", match: { chatType: "direct" } }, // Normalized session-key prefix (strips `agent::`). { action: "deny", match: { keyPrefix: "discord:channel:" } }, // Raw session-key prefix (includes `agent::`). { action: "deny", match: { rawKeyPrefix: "agent:main:discord:" } }, ] }, paths: [ { name: "docs", path: "~/notes", pattern: "**/*.md" } ] } } ``` **Citations & fallback** * `memory.citations` applies regardless of backend (`auto`/`on`/`off`). * When `qmd` runs, we tag `status().backend = "qmd"` so diagnostics show which engine served the results. If the QMD subprocess exits or JSON output can’t be parsed, the search manager logs a warning and returns the builtin provider (existing Markdown embeddings) until QMD recovers. ### Additional memory paths If you want to index Markdown files outside the default workspace layout, add explicit paths: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} agents: { defaults: { memorySearch: { extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"] } } } ``` Notes: * Paths can be absolute or workspace-relative. * Directories are scanned recursively for `.md` files. * Only Markdown files are indexed. * Symlinks are ignored (files or directories). ### Gemini embeddings (native) Set the provider to `gemini` to use the Gemini embeddings API directly: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} agents: { defaults: { memorySearch: { provider: "gemini", model: "gemini-embedding-001", remote: { apiKey: "YOUR_GEMINI_API_KEY" } } } } ``` Notes: * `remote.baseUrl` is optional (defaults to the Gemini API base URL). * `remote.headers` lets you add extra headers if needed. * Default model: `gemini-embedding-001`. If you want to use a **custom OpenAI-compatible endpoint** (OpenRouter, vLLM, or a proxy), you can use the `remote` configuration with the OpenAI provider: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} agents: { defaults: { memorySearch: { provider: "openai", model: "text-embedding-3-small", remote: { baseUrl: "https://api.example.com/v1/", apiKey: "YOUR_OPENAI_COMPAT_API_KEY", headers: { "X-Custom-Header": "value" } } } } } ``` If you don't want to set an API key, use `memorySearch.provider = "local"` or set `memorySearch.fallback = "none"`. Fallbacks: * `memorySearch.fallback` can be `openai`, `gemini`, `voyage`, `mistral`, `ollama`, `local`, or `none`. * The fallback provider is only used when the primary embedding provider fails. Batch indexing (OpenAI + Gemini + Voyage): * Disabled by default. Set `agents.defaults.memorySearch.remote.batch.enabled = true` to enable for large-corpus indexing (OpenAI, Gemini, and Voyage). * Default behavior waits for batch completion; tune `remote.batch.wait`, `remote.batch.pollIntervalMs`, and `remote.batch.timeoutMinutes` if needed. * Set `remote.batch.concurrency` to control how many batch jobs we submit in parallel (default: 2). * Batch mode applies when `memorySearch.provider = "openai"` or `"gemini"` and uses the corresponding API key. * Gemini batch jobs use the async embeddings batch endpoint and require Gemini Batch API availability. Why OpenAI batch is fast + cheap: * For large backfills, OpenAI is typically the fastest option we support because we can submit many embedding requests in a single batch job and let OpenAI process them asynchronously. * OpenAI offers discounted pricing for Batch API workloads, so large indexing runs are usually cheaper than sending the same requests synchronously. * See the OpenAI Batch API docs and pricing for details: * [https://platform.openai.com/docs/api-reference/batch](https://platform.openai.com/docs/api-reference/batch) * [https://platform.openai.com/pricing](https://platform.openai.com/pricing) Config example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} agents: { defaults: { memorySearch: { provider: "openai", model: "text-embedding-3-small", fallback: "openai", remote: { batch: { enabled: true, concurrency: 2 } }, sync: { watch: true } } } } ``` Tools: * `memory_search` — returns snippets with file + line ranges. * `memory_get` — read memory file content by path. Local mode: * Set `agents.defaults.memorySearch.provider = "local"`. * Provide `agents.defaults.memorySearch.local.modelPath` (GGUF or `hf:` URI). * Optional: set `agents.defaults.memorySearch.fallback = "none"` to avoid remote fallback. ### How the memory tools work * `memory_search` semantically searches Markdown chunks (\~400 token target, 80-token overlap) from `MEMORY.md` + `memory/**/*.md`. It returns snippet text (capped \~700 chars), file path, line range, score, provider/model, and whether we fell back from local → remote embeddings. No full file payload is returned. * `memory_get` reads a specific memory Markdown file (workspace-relative), optionally from a starting line and for N lines. Paths outside `MEMORY.md` / `memory/` are rejected. * Both tools are enabled only when `memorySearch.enabled` resolves true for the agent. ### What gets indexed (and when) * File type: Markdown only (`MEMORY.md`, `memory/**/*.md`). * Index storage: per-agent SQLite at `~/.openclaw/memory/.sqlite` (configurable via `agents.defaults.memorySearch.store.path`, supports `{agentId}` token). * Freshness: watcher on `MEMORY.md` + `memory/` marks the index dirty (debounce 1.5s). Sync is scheduled on session start, on search, or on an interval and runs asynchronously. Session transcripts use delta thresholds to trigger background sync. * Reindex triggers: the index stores the embedding **provider/model + endpoint fingerprint + chunking params**. If any of those change, OpenClaw automatically resets and reindexes the entire store. ### Hybrid search (BM25 + vector) When enabled, OpenClaw combines: * **Vector similarity** (semantic match, wording can differ) * **BM25 keyword relevance** (exact tokens like IDs, env vars, code symbols) If full-text search is unavailable on your platform, OpenClaw falls back to vector-only search. #### Why hybrid? Vector search is great at “this means the same thing”: * “Mac Studio gateway host” vs “the machine running the gateway” * “debounce file updates” vs “avoid indexing on every write” But it can be weak at exact, high-signal tokens: * IDs (`a828e60`, `b3b9895a…`) * code symbols (`memorySearch.query.hybrid`) * error strings ("sqlite-vec unavailable") BM25 (full-text) is the opposite: strong at exact tokens, weaker at paraphrases. Hybrid search is the pragmatic middle ground: **use both retrieval signals** so you get good results for both "natural language" queries and "needle in a haystack" queries. #### How we merge results (the current design) Implementation sketch: 1. Retrieve a candidate pool from both sides: * **Vector**: top `maxResults * candidateMultiplier` by cosine similarity. * **BM25**: top `maxResults * candidateMultiplier` by FTS5 BM25 rank (lower is better). 2. Convert BM25 rank into a 0..1-ish score: * `textScore = 1 / (1 + max(0, bm25Rank))` 3. Union candidates by chunk id and compute a weighted score: * `finalScore = vectorWeight * vectorScore + textWeight * textScore` Notes: * `vectorWeight` + `textWeight` is normalized to 1.0 in config resolution, so weights behave as percentages. * If embeddings are unavailable (or the provider returns a zero-vector), we still run BM25 and return keyword matches. * If FTS5 can't be created, we keep vector-only search (no hard failure). This isn't "IR-theory perfect", but it's simple, fast, and tends to improve recall/precision on real notes. If we want to get fancier later, common next steps are Reciprocal Rank Fusion (RRF) or score normalization (min/max or z-score) before mixing. #### Post-processing pipeline After merging vector and keyword scores, two optional post-processing stages refine the result list before it reaches the agent: ``` Vector + Keyword → Weighted Merge → Temporal Decay → Sort → MMR → Top-K Results ``` Both stages are **off by default** and can be enabled independently. #### MMR re-ranking (diversity) When hybrid search returns results, multiple chunks may contain similar or overlapping content. For example, searching for "home network setup" might return five nearly identical snippets from different daily notes that all mention the same router configuration. **MMR (Maximal Marginal Relevance)** re-ranks the results to balance relevance with diversity, ensuring the top results cover different aspects of the query instead of repeating the same information. How it works: 1. Results are scored by their original relevance (vector + BM25 weighted score). 2. MMR iteratively selects results that maximize: `λ × relevance − (1−λ) × max_similarity_to_selected`. 3. Similarity between results is measured using Jaccard text similarity on tokenized content. The `lambda` parameter controls the trade-off: * `lambda = 1.0` → pure relevance (no diversity penalty) * `lambda = 0.0` → maximum diversity (ignores relevance) * Default: `0.7` (balanced, slight relevance bias) **Example — query: "home network setup"** Given these memory files: ``` memory/2026-02-10.md → "Configured Omada router, set VLAN 10 for IoT devices" memory/2026-02-08.md → "Configured Omada router, moved IoT to VLAN 10" memory/2026-02-05.md → "Set up AdGuard DNS on 192.168.10.2" memory/network.md → "Router: Omada ER605, AdGuard: 192.168.10.2, VLAN 10: IoT" ``` Without MMR — top 3 results: ``` 1. memory/2026-02-10.md (score: 0.92) ← router + VLAN 2. memory/2026-02-08.md (score: 0.89) ← router + VLAN (near-duplicate!) 3. memory/network.md (score: 0.85) ← reference doc ``` With MMR (λ=0.7) — top 3 results: ``` 1. memory/2026-02-10.md (score: 0.92) ← router + VLAN 2. memory/network.md (score: 0.85) ← reference doc (diverse!) 3. memory/2026-02-05.md (score: 0.78) ← AdGuard DNS (diverse!) ``` The near-duplicate from Feb 8 drops out, and the agent gets three distinct pieces of information. **When to enable:** If you notice `memory_search` returning redundant or near-duplicate snippets, especially with daily notes that often repeat similar information across days. #### Temporal decay (recency boost) Agents with daily notes accumulate hundreds of dated files over time. Without decay, a well-worded note from six months ago can outrank yesterday's update on the same topic. **Temporal decay** applies an exponential multiplier to scores based on the age of each result, so recent memories naturally rank higher while old ones fade: ``` decayedScore = score × e^(-λ × ageInDays) ``` where `λ = ln(2) / halfLifeDays`. With the default half-life of 30 days: * Today's notes: **100%** of original score * 7 days ago: **\~84%** * 30 days ago: **50%** * 90 days ago: **12.5%** * 180 days ago: **\~1.6%** **Evergreen files are never decayed:** * `MEMORY.md` (root memory file) * Non-dated files in `memory/` (e.g., `memory/projects.md`, `memory/network.md`) * These contain durable reference information that should always rank normally. **Dated daily files** (`memory/YYYY-MM-DD.md`) use the date extracted from the filename. Other sources (e.g., session transcripts) fall back to file modification time (`mtime`). **Example — query: "what's Rod's work schedule?"** Given these memory files (today is Feb 10): ``` memory/2025-09-15.md → "Rod works Mon-Fri, standup at 10am, pairing at 2pm" (148 days old) memory/2026-02-10.md → "Rod has standup at 14:15, 1:1 with Zeb at 14:45" (today) memory/2026-02-03.md → "Rod started new team, standup moved to 14:15" (7 days old) ``` Without decay: ``` 1. memory/2025-09-15.md (score: 0.91) ← best semantic match, but stale! 2. memory/2026-02-10.md (score: 0.82) 3. memory/2026-02-03.md (score: 0.80) ``` With decay (halfLife=30): ``` 1. memory/2026-02-10.md (score: 0.82 × 1.00 = 0.82) ← today, no decay 2. memory/2026-02-03.md (score: 0.80 × 0.85 = 0.68) ← 7 days, mild decay 3. memory/2025-09-15.md (score: 0.91 × 0.03 = 0.03) ← 148 days, nearly gone ``` The stale September note drops to the bottom despite having the best raw semantic match. **When to enable:** If your agent has months of daily notes and you find that old, stale information outranks recent context. A half-life of 30 days works well for daily-note-heavy workflows; increase it (e.g., 90 days) if you reference older notes frequently. #### Configuration Both features are configured under `memorySearch.query.hybrid`: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} agents: { defaults: { memorySearch: { query: { hybrid: { enabled: true, vectorWeight: 0.7, textWeight: 0.3, candidateMultiplier: 4, // Diversity: reduce redundant results mmr: { enabled: true, // default: false lambda: 0.7 // 0 = max diversity, 1 = max relevance }, // Recency: boost newer memories temporalDecay: { enabled: true, // default: false halfLifeDays: 30 // score halves every 30 days } } } } } } ``` You can enable either feature independently: * **MMR only** — useful when you have many similar notes but age doesn't matter. * **Temporal decay only** — useful when recency matters but your results are already diverse. * **Both** — recommended for agents with large, long-running daily note histories. ### Embedding cache OpenClaw can cache **chunk embeddings** in SQLite so reindexing and frequent updates (especially session transcripts) don't re-embed unchanged text. Config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} agents: { defaults: { memorySearch: { cache: { enabled: true, maxEntries: 50000 } } } } ``` ### Session memory search (experimental) You can optionally index **session transcripts** and surface them via `memory_search`. This is gated behind an experimental flag. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} agents: { defaults: { memorySearch: { experimental: { sessionMemory: true }, sources: ["memory", "sessions"] } } } ``` Notes: * Session indexing is **opt-in** (off by default). * Session updates are debounced and **indexed asynchronously** once they cross delta thresholds (best-effort). * `memory_search` never blocks on indexing; results can be slightly stale until background sync finishes. * Results still include snippets only; `memory_get` remains limited to memory files. * Session indexing is isolated per agent (only that agent’s session logs are indexed). * Session logs live on disk (`~/.openclaw/agents//sessions/*.jsonl`). Any process/user with filesystem access can read them, so treat disk access as the trust boundary. For stricter isolation, run agents under separate OS users or hosts. Delta thresholds (defaults shown): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} agents: { defaults: { memorySearch: { sync: { sessions: { deltaBytes: 100000, // ~100 KB deltaMessages: 50 // JSONL lines } } } } } ``` ### SQLite vector acceleration (sqlite-vec) When the sqlite-vec extension is available, OpenClaw stores embeddings in a SQLite virtual table (`vec0`) and performs vector distance queries in the database. This keeps search fast without loading every embedding into JS. Configuration (optional): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} agents: { defaults: { memorySearch: { store: { vector: { enabled: true, extensionPath: "/path/to/sqlite-vec" } } } } } ``` Notes: * `enabled` defaults to true; when disabled, search falls back to in-process cosine similarity over stored embeddings. * If the sqlite-vec extension is missing or fails to load, OpenClaw logs the error and continues with the JS fallback (no vector table). * `extensionPath` overrides the bundled sqlite-vec path (useful for custom builds or non-standard install locations). ### Local embedding auto-download * Default local embedding model: `hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf` (\~0.6 GB). * When `memorySearch.provider = "local"`, `node-llama-cpp` resolves `modelPath`; if the GGUF is missing it **auto-downloads** to the cache (or `local.modelCacheDir` if set), then loads it. Downloads resume on retry. * Native build requirement: run `pnpm approve-builds`, pick `node-llama-cpp`, then `pnpm rebuild node-llama-cpp`. * Fallback: if local setup fails and `memorySearch.fallback = "openai"`, we automatically switch to remote embeddings (`openai/text-embedding-3-small` unless overridden) and record the reason. ### Custom OpenAI-compatible endpoint example ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} agents: { defaults: { memorySearch: { provider: "openai", model: "text-embedding-3-small", remote: { baseUrl: "https://api.example.com/v1/", apiKey: "YOUR_REMOTE_API_KEY", headers: { "X-Organization": "org-id", "X-Project": "project-id" } } } } } ``` Notes: * `remote.*` takes precedence over `models.providers.openai.*`. * `remote.headers` merge with OpenAI headers; remote wins on key conflicts. Omit `remote.headers` to use the OpenAI defaults. # Messages Source: https://cryptoclawdocs.termix.ai/concepts/messages # Messages This page ties together how OpenClaw handles inbound messages, sessions, queueing, streaming, and reasoning visibility. ## Message flow (high level) ``` Inbound message -> routing/bindings -> session key -> queue (if a run is active) -> agent run (streaming + tools) -> outbound replies (channel limits + chunking) ``` Key knobs live in configuration: * `messages.*` for prefixes, queueing, and group behavior. * `agents.defaults.*` for block streaming and chunking defaults. * Channel overrides (`channels.whatsapp.*`, `channels.telegram.*`, etc.) for caps and streaming toggles. See [Configuration](/gateway/configuration) for full schema. ## Inbound dedupe Channels can redeliver the same message after reconnects. OpenClaw keeps a short-lived cache keyed by channel/account/peer/session/message id so duplicate deliveries do not trigger another agent run. ## Inbound debouncing Rapid consecutive messages from the **same sender** can be batched into a single agent turn via `messages.inbound`. Debouncing is scoped per channel + conversation and uses the most recent message for reply threading/IDs. Config (global default + per-channel overrides): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { messages: { inbound: { debounceMs: 2000, byChannel: { whatsapp: 5000, slack: 1500, discord: 1500, }, }, }, } ``` Notes: * Debounce applies to **text-only** messages; media/attachments flush immediately. * Control commands bypass debouncing so they remain standalone. ## Sessions and devices Sessions are owned by the gateway, not by clients. * Direct chats collapse into the agent main session key. * Groups/channels get their own session keys. * The session store and transcripts live on the gateway host. Multiple devices/channels can map to the same session, but history is not fully synced back to every client. Recommendation: use one primary device for long conversations to avoid divergent context. The Control UI and TUI always show the gateway-backed session transcript, so they are the source of truth. Details: [Session management](/concepts/session). ## Inbound bodies and history context OpenClaw separates the **prompt body** from the **command body**: * `Body`: prompt text sent to the agent. This may include channel envelopes and optional history wrappers. * `CommandBody`: raw user text for directive/command parsing. * `RawBody`: legacy alias for `CommandBody` (kept for compatibility). When a channel supplies history, it uses a shared wrapper: * `[Chat messages since your last reply - for context]` * `[Current message - respond to this]` For **non-direct chats** (groups/channels/rooms), the **current message body** is prefixed with the sender label (same style used for history entries). This keeps real-time and queued/history messages consistent in the agent prompt. History buffers are **pending-only**: they include group messages that did *not* trigger a run (for example, mention-gated messages) and **exclude** messages already in the session transcript. Directive stripping only applies to the **current message** section so history remains intact. Channels that wrap history should set `CommandBody` (or `RawBody`) to the original message text and keep `Body` as the combined prompt. History buffers are configurable via `messages.groupChat.historyLimit` (global default) and per-channel overrides like `channels.slack.historyLimit` or `channels.telegram.accounts..historyLimit` (set `0` to disable). ## Queueing and followups If a run is already active, inbound messages can be queued, steered into the current run, or collected for a followup turn. * Configure via `messages.queue` (and `messages.queue.byChannel`). * Modes: `interrupt`, `steer`, `followup`, `collect`, plus backlog variants. Details: [Queueing](/concepts/queue). ## Streaming, chunking, and batching Block streaming sends partial replies as the model produces text blocks. Chunking respects channel text limits and avoids splitting fenced code. Key settings: * `agents.defaults.blockStreamingDefault` (`on|off`, default off) * `agents.defaults.blockStreamingBreak` (`text_end|message_end`) * `agents.defaults.blockStreamingChunk` (`minChars|maxChars|breakPreference`) * `agents.defaults.blockStreamingCoalesce` (idle-based batching) * `agents.defaults.humanDelay` (human-like pause between block replies) * Channel overrides: `*.blockStreaming` and `*.blockStreamingCoalesce` (non-Telegram channels require explicit `*.blockStreaming: true`) Details: [Streaming + chunking](/concepts/streaming). ## Reasoning visibility and tokens OpenClaw can expose or hide model reasoning: * `/reasoning on|off|stream` controls visibility. * Reasoning content still counts toward token usage when produced by the model. * Telegram supports reasoning stream into the draft bubble. Details: [Thinking + reasoning directives](/tools/thinking) and [Token use](/reference/token-use). ## Prefixes, threading, and replies Outbound message formatting is centralized in `messages`: * `messages.responsePrefix`, `channels..responsePrefix`, and `channels..accounts..responsePrefix` (outbound prefix cascade), plus `channels.whatsapp.messagePrefix` (WhatsApp inbound prefix) * Reply threading via `replyToMode` and per-channel defaults Details: [Configuration](/gateway/configuration#messages) and channel docs. # Multi-Agent Routing Source: https://cryptoclawdocs.termix.ai/concepts/multi-agent # Multi-Agent Routing Goal: multiple *isolated* agents (separate workspace + `agentDir` + sessions), plus multiple channel accounts (e.g. two WhatsApps) in one running Gateway. Inbound is routed to an agent via bindings. ## What is “one agent”? An **agent** is a fully scoped brain with its own: * **Workspace** (files, AGENTS.md/SOUL.md/USER.md, local notes, persona rules). * **State directory** (`agentDir`) for auth profiles, model registry, and per-agent config. * **Session store** (chat history + routing state) under `~/.openclaw/agents//sessions`. Auth profiles are **per-agent**. Each agent reads from its own: ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} ~/.openclaw/agents//agent/auth-profiles.json ``` Main agent credentials are **not** shared automatically. Never reuse `agentDir` across agents (it causes auth/session collisions). If you want to share creds, copy `auth-profiles.json` into the other agent's `agentDir`. Skills are per-agent via each workspace’s `skills/` folder, with shared skills available from `~/.openclaw/skills`. See [Skills: per-agent vs shared](/tools/skills#per-agent-vs-shared-skills). The Gateway can host **one agent** (default) or **many agents** side-by-side. **Workspace note:** each agent’s workspace is the **default cwd**, not a hard sandbox. Relative paths resolve inside the workspace, but absolute paths can reach other host locations unless sandboxing is enabled. See [Sandboxing](/gateway/sandboxing). ## Paths (quick map) * Config: `~/.openclaw/openclaw.json` (or `OPENCLAW_CONFIG_PATH`) * State dir: `~/.openclaw` (or `OPENCLAW_STATE_DIR`) * Workspace: `~/.openclaw/workspace` (or `~/.openclaw/workspace-`) * Agent dir: `~/.openclaw/agents//agent` (or `agents.list[].agentDir`) * Sessions: `~/.openclaw/agents//sessions` ### Single-agent mode (default) If you do nothing, OpenClaw runs a single agent: * `agentId` defaults to **`main`**. * Sessions are keyed as `agent:main:`. * Workspace defaults to `~/.openclaw/workspace` (or `~/.openclaw/workspace-` when `OPENCLAW_PROFILE` is set). * State defaults to `~/.openclaw/agents/main/agent`. ## Agent helper Use the agent wizard to add a new isolated agent: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw agents add work ``` Then add `bindings` (or let the wizard do it) to route inbound messages. Verify with: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw agents list --bindings ``` ## Quick start Use the wizard or create workspaces manually: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw agents add coding openclaw agents add social ``` Each agent gets its own workspace with `SOUL.md`, `AGENTS.md`, and optional `USER.md`, plus a dedicated `agentDir` and session store under `~/.openclaw/agents/`. Create one account per agent on your preferred channels: * Discord: one bot per agent, enable Message Content Intent, copy each token. * Telegram: one bot per agent via BotFather, copy each token. * WhatsApp: link each phone number per account. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw channels login --channel whatsapp --account work ``` See channel guides: [Discord](/channels/discord), [Telegram](/channels/telegram), [WhatsApp](/channels/whatsapp). Add agents under `agents.list`, channel accounts under `channels..accounts`, and connect them with `bindings` (examples below). ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway restart openclaw agents list --bindings openclaw channels status --probe ``` ## Multiple agents = multiple people, multiple personalities With **multiple agents**, each `agentId` becomes a **fully isolated persona**: * **Different phone numbers/accounts** (per channel `accountId`). * **Different personalities** (per-agent workspace files like `AGENTS.md` and `SOUL.md`). * **Separate auth + sessions** (no cross-talk unless explicitly enabled). This lets **multiple people** share one Gateway server while keeping their AI “brains” and data isolated. ## One WhatsApp number, multiple people (DM split) You can route **different WhatsApp DMs** to different agents while staying on **one WhatsApp account**. Match on sender E.164 (like `+15551234567`) with `peer.kind: "direct"`. Replies still come from the same WhatsApp number (no per‑agent sender identity). Important detail: direct chats collapse to the agent’s **main session key**, so true isolation requires **one agent per person**. Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { list: [ { id: "alex", workspace: "~/.openclaw/workspace-alex" }, { id: "mia", workspace: "~/.openclaw/workspace-mia" }, ], }, bindings: [ { agentId: "alex", match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551230001" } }, }, { agentId: "mia", match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551230002" } }, }, ], channels: { whatsapp: { dmPolicy: "allowlist", allowFrom: ["+15551230001", "+15551230002"], }, }, } ``` Notes: * DM access control is **global per WhatsApp account** (pairing/allowlist), not per agent. * For shared groups, bind the group to one agent or use [Broadcast groups](/channels/broadcast-groups). ## Routing rules (how messages pick an agent) Bindings are **deterministic** and **most-specific wins**: 1. `peer` match (exact DM/group/channel id) 2. `parentPeer` match (thread inheritance) 3. `guildId + roles` (Discord role routing) 4. `guildId` (Discord) 5. `teamId` (Slack) 6. `accountId` match for a channel 7. channel-level match (`accountId: "*"`) 8. fallback to default agent (`agents.list[].default`, else first list entry, default: `main`) If multiple bindings match in the same tier, the first one in config order wins. If a binding sets multiple match fields (for example `peer` + `guildId`), all specified fields are required (`AND` semantics). Important account-scope detail: * A binding that omits `accountId` matches the default account only. * Use `accountId: "*"` for a channel-wide fallback across all accounts. * If you later add the same binding for the same agent with an explicit account id, OpenClaw upgrades the existing channel-only binding to account-scoped instead of duplicating it. ## Multiple accounts / phone numbers Channels that support **multiple accounts** (e.g. WhatsApp) use `accountId` to identify each login. Each `accountId` can be routed to a different agent, so one server can host multiple phone numbers without mixing sessions. If you want a channel-wide default account when `accountId` is omitted, set `channels..defaultAccount` (optional). When unset, OpenClaw falls back to `default` if present, otherwise the first configured account id (sorted). Common channels supporting this pattern include: * `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage` * `irc`, `line`, `googlechat`, `mattermost`, `matrix`, `nextcloud-talk` * `bluebubbles`, `zalo`, `zalouser`, `nostr`, `feishu` ## Concepts * `agentId`: one “brain” (workspace, per-agent auth, per-agent session store). * `accountId`: one channel account instance (e.g. WhatsApp account `"personal"` vs `"biz"`). * `binding`: routes inbound messages to an `agentId` by `(channel, accountId, peer)` and optionally guild/team ids. * Direct chats collapse to `agent::` (per-agent “main”; `session.mainKey`). ## Platform examples ### Discord bots per agent Each Discord bot account maps to a unique `accountId`. Bind each account to an agent and keep allowlists per bot. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { list: [ { id: "main", workspace: "~/.openclaw/workspace-main" }, { id: "coding", workspace: "~/.openclaw/workspace-coding" }, ], }, bindings: [ { agentId: "main", match: { channel: "discord", accountId: "default" } }, { agentId: "coding", match: { channel: "discord", accountId: "coding" } }, ], channels: { discord: { groupPolicy: "allowlist", accounts: { default: { token: "DISCORD_BOT_TOKEN_MAIN", guilds: { "123456789012345678": { channels: { "222222222222222222": { allow: true, requireMention: false }, }, }, }, }, coding: { token: "DISCORD_BOT_TOKEN_CODING", guilds: { "123456789012345678": { channels: { "333333333333333333": { allow: true, requireMention: false }, }, }, }, }, }, }, }, } ``` Notes: * Invite each bot to the guild and enable Message Content Intent. * Tokens live in `channels.discord.accounts..token` (default account can use `DISCORD_BOT_TOKEN`). ### Telegram bots per agent ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { list: [ { id: "main", workspace: "~/.openclaw/workspace-main" }, { id: "alerts", workspace: "~/.openclaw/workspace-alerts" }, ], }, bindings: [ { agentId: "main", match: { channel: "telegram", accountId: "default" } }, { agentId: "alerts", match: { channel: "telegram", accountId: "alerts" } }, ], channels: { telegram: { accounts: { default: { botToken: "123456:ABC...", dmPolicy: "pairing", }, alerts: { botToken: "987654:XYZ...", dmPolicy: "allowlist", allowFrom: ["tg:123456789"], }, }, }, }, } ``` Notes: * Create one bot per agent with BotFather and copy each token. * Tokens live in `channels.telegram.accounts..botToken` (default account can use `TELEGRAM_BOT_TOKEN`). ### WhatsApp numbers per agent Link each account before starting the gateway: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw channels login --channel whatsapp --account personal openclaw channels login --channel whatsapp --account biz ``` `~/.openclaw/openclaw.json` (JSON5): ```js theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { list: [ { id: "home", default: true, name: "Home", workspace: "~/.openclaw/workspace-home", agentDir: "~/.openclaw/agents/home/agent", }, { id: "work", name: "Work", workspace: "~/.openclaw/workspace-work", agentDir: "~/.openclaw/agents/work/agent", }, ], }, // Deterministic routing: first match wins (most-specific first). bindings: [ { agentId: "home", match: { channel: "whatsapp", accountId: "personal" } }, { agentId: "work", match: { channel: "whatsapp", accountId: "biz" } }, // Optional per-peer override (example: send a specific group to work agent). { agentId: "work", match: { channel: "whatsapp", accountId: "personal", peer: { kind: "group", id: "1203630...@g.us" }, }, }, ], // Off by default: agent-to-agent messaging must be explicitly enabled + allowlisted. tools: { agentToAgent: { enabled: false, allow: ["home", "work"], }, }, channels: { whatsapp: { accounts: { personal: { // Optional override. Default: ~/.openclaw/credentials/whatsapp/personal // authDir: "~/.openclaw/credentials/whatsapp/personal", }, biz: { // Optional override. Default: ~/.openclaw/credentials/whatsapp/biz // authDir: "~/.openclaw/credentials/whatsapp/biz", }, }, }, }, } ``` ## Example: WhatsApp daily chat + Telegram deep work Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opus agent. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { list: [ { id: "chat", name: "Everyday", workspace: "~/.openclaw/workspace-chat", model: "anthropic/claude-sonnet-4-5", }, { id: "opus", name: "Deep Work", workspace: "~/.openclaw/workspace-opus", model: "anthropic/claude-opus-4-6", }, ], }, bindings: [ { agentId: "chat", match: { channel: "whatsapp" } }, { agentId: "opus", match: { channel: "telegram" } }, ], } ``` Notes: * If you have multiple accounts for a channel, add `accountId` to the binding (for example `{ channel: "whatsapp", accountId: "personal" }`). * To route a single DM/group to Opus while keeping the rest on chat, add a `match.peer` binding for that peer; peer matches always win over channel-wide rules. ## Example: same channel, one peer to Opus Keep WhatsApp on the fast agent, but route one DM to Opus: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { list: [ { id: "chat", name: "Everyday", workspace: "~/.openclaw/workspace-chat", model: "anthropic/claude-sonnet-4-5", }, { id: "opus", name: "Deep Work", workspace: "~/.openclaw/workspace-opus", model: "anthropic/claude-opus-4-6", }, ], }, bindings: [ { agentId: "opus", match: { channel: "whatsapp", peer: { kind: "direct", id: "+15551234567" } }, }, { agentId: "chat", match: { channel: "whatsapp" } }, ], } ``` Peer bindings always win, so keep them above the channel-wide rule. ## Family agent bound to a WhatsApp group Bind a dedicated family agent to a single WhatsApp group, with mention gating and a tighter tool policy: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { list: [ { id: "family", name: "Family", workspace: "~/.openclaw/workspace-family", identity: { name: "Family Bot" }, groupChat: { mentionPatterns: ["@family", "@familybot", "@Family Bot"], }, sandbox: { mode: "all", scope: "agent", }, tools: { allow: [ "exec", "read", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status", ], deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"], }, }, ], }, bindings: [ { agentId: "family", match: { channel: "whatsapp", peer: { kind: "group", id: "120363999999999999@g.us" }, }, }, ], } ``` Notes: * Tool allow/deny lists are **tools**, not skills. If a skill needs to run a binary, ensure `exec` is allowed and the binary exists in the sandbox. * For stricter gating, set `agents.list[].groupChat.mentionPatterns` and keep group allowlists enabled for the channel. ## Per-Agent Sandbox and Tool Configuration Starting with v2026.1.6, each agent can have its own sandbox and tool restrictions: ```js theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { list: [ { id: "personal", workspace: "~/.openclaw/workspace-personal", sandbox: { mode: "off", // No sandbox for personal agent }, // No tool restrictions - all tools available }, { id: "family", workspace: "~/.openclaw/workspace-family", sandbox: { mode: "all", // Always sandboxed scope: "agent", // One container per agent docker: { // Optional one-time setup after container creation setupCommand: "apt-get update && apt-get install -y git curl", }, }, tools: { allow: ["read"], // Only read tool deny: ["exec", "write", "edit", "apply_patch"], // Deny others }, }, ], }, } ``` Note: `setupCommand` lives under `sandbox.docker` and runs once on container creation. Per-agent `sandbox.docker.*` overrides are ignored when the resolved scope is `"shared"`. **Benefits:** * **Security isolation**: Restrict tools for untrusted agents * **Resource control**: Sandbox specific agents while keeping others on host * **Flexible policies**: Different permissions per agent Note: `tools.elevated` is **global** and sender-based; it is not configurable per agent. If you need per-agent boundaries, use `agents.list[].tools` to deny `exec`. For group targeting, use `agents.list[].groupChat.mentionPatterns` so @mentions map cleanly to the intended agent. See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for detailed examples. # OAuth Source: https://cryptoclawdocs.termix.ai/concepts/oauth # OAuth OpenClaw supports “subscription auth” via OAuth for providers that offer it (notably **OpenAI Codex (ChatGPT OAuth)**). For Anthropic subscriptions, use the **setup-token** flow. Anthropic subscription use outside Claude Code has been restricted for some users in the past, so treat it as a user-choice risk and verify current Anthropic policy yourself. OpenAI Codex OAuth is explicitly supported for use in external tools like OpenClaw. This page explains: For Anthropic in production, API key auth is the safer recommended path over subscription setup-token auth. * how the OAuth **token exchange** works (PKCE) * where tokens are **stored** (and why) * how to handle **multiple accounts** (profiles + per-session overrides) OpenClaw also supports **provider plugins** that ship their own OAuth or API‑key flows. Run them via: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw models auth login --provider ``` ## The token sink (why it exists) OAuth providers commonly mint a **new refresh token** during login/refresh flows. Some providers (or OAuth clients) can invalidate older refresh tokens when a new one is issued for the same user/app. Practical symptom: * you log in via OpenClaw *and* via Claude Code / Codex CLI → one of them randomly gets “logged out” later To reduce that, OpenClaw treats `auth-profiles.json` as a **token sink**: * the runtime reads credentials from **one place** * we can keep multiple profiles and route them deterministically ## Storage (where tokens live) Secrets are stored **per-agent**: * Auth profiles (OAuth + API keys + optional value-level refs): `~/.openclaw/agents//agent/auth-profiles.json` * Legacy compatibility file: `~/.openclaw/agents//agent/auth.json` (static `api_key` entries are scrubbed when discovered) Legacy import-only file (still supported, but not the main store): * `~/.openclaw/credentials/oauth.json` (imported into `auth-profiles.json` on first use) All of the above also respect `$OPENCLAW_STATE_DIR` (state dir override). Full reference: [/gateway/configuration](/gateway/configuration#auth-storage-oauth--api-keys) For static secret refs and runtime snapshot activation behavior, see [Secrets Management](/gateway/secrets). ## Anthropic setup-token (subscription auth) Anthropic setup-token support is technical compatibility, not a policy guarantee. Anthropic has blocked some subscription usage outside Claude Code in the past. Decide for yourself whether to use subscription auth, and verify Anthropic's current terms. Run `claude setup-token` on any machine, then paste it into OpenClaw: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw models auth setup-token --provider anthropic ``` If you generated the token elsewhere, paste it manually: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw models auth paste-token --provider anthropic ``` Verify: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw models status ``` ## OAuth exchange (how login works) OpenClaw’s interactive login flows are implemented in `@mariozechner/pi-ai` and wired into the wizards/commands. ### Anthropic setup-token Flow shape: 1. run `claude setup-token` 2. paste the token into OpenClaw 3. store as a token auth profile (no refresh) The wizard path is `openclaw onboard` → auth choice `setup-token` (Anthropic). ### OpenAI Codex (ChatGPT OAuth) OpenAI Codex OAuth is explicitly supported for use outside the Codex CLI, including OpenClaw workflows. Flow shape (PKCE): 1. generate PKCE verifier/challenge + random `state` 2. open `https://auth.openai.com/oauth/authorize?...` 3. try to capture callback on `http://127.0.0.1:1455/auth/callback` 4. if callback can’t bind (or you’re remote/headless), paste the redirect URL/code 5. exchange at `https://auth.openai.com/oauth/token` 6. extract `accountId` from the access token and store `{ access, refresh, expires, accountId }` Wizard path is `openclaw onboard` → auth choice `openai-codex`. ## Refresh + expiry Profiles store an `expires` timestamp. At runtime: * if `expires` is in the future → use the stored access token * if expired → refresh (under a file lock) and overwrite the stored credentials The refresh flow is automatic; you generally don't need to manage tokens manually. ## Multiple accounts (profiles) + routing Two patterns: ### 1) Preferred: separate agents If you want “personal” and “work” to never interact, use isolated agents (separate sessions + credentials + workspace): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw agents add work openclaw agents add personal ``` Then configure auth per-agent (wizard) and route chats to the right agent. ### 2) Advanced: multiple profiles in one agent `auth-profiles.json` supports multiple profile IDs for the same provider. Pick which profile is used: * globally via config ordering (`auth.order`) * per-session via `/model ...@` Example (session override): * `/model Opus@anthropic:work` How to see what profile IDs exist: * `openclaw channels list --json` (shows `auth[]`) Related docs: * [/concepts/model-failover](/concepts/model-failover) (rotation + cooldown rules) * [/tools/slash-commands](/tools/slash-commands) (command surface) # Presence Source: https://cryptoclawdocs.termix.ai/concepts/presence # Presence OpenClaw “presence” is a lightweight, best‑effort view of: * the **Gateway** itself, and * **clients connected to the Gateway** (mac app, WebChat, CLI, etc.) Presence is used primarily to render the macOS app’s **Instances** tab and to provide quick operator visibility. ## Presence fields (what shows up) Presence entries are structured objects with fields like: * `instanceId` (optional but strongly recommended): stable client identity (usually `connect.client.instanceId`) * `host`: human‑friendly host name * `ip`: best‑effort IP address * `version`: client version string * `deviceFamily` / `modelIdentifier`: hardware hints * `mode`: `ui`, `webchat`, `cli`, `backend`, `probe`, `test`, `node`, ... * `lastInputSeconds`: “seconds since last user input” (if known) * `reason`: `self`, `connect`, `node-connected`, `periodic`, ... * `ts`: last update timestamp (ms since epoch) ## Producers (where presence comes from) Presence entries are produced by multiple sources and **merged**. ### 1) Gateway self entry The Gateway always seeds a “self” entry at startup so UIs show the gateway host even before any clients connect. ### 2) WebSocket connect Every WS client begins with a `connect` request. On successful handshake the Gateway upserts a presence entry for that connection. #### Why one‑off CLI commands don’t show up The CLI often connects for short, one‑off commands. To avoid spamming the Instances list, `client.mode === "cli"` is **not** turned into a presence entry. ### 3) `system-event` beacons Clients can send richer periodic beacons via the `system-event` method. The mac app uses this to report host name, IP, and `lastInputSeconds`. ### 4) Node connects (role: node) When a node connects over the Gateway WebSocket with `role: node`, the Gateway upserts a presence entry for that node (same flow as other WS clients). ## Merge + dedupe rules (why `instanceId` matters) Presence entries are stored in a single in‑memory map: * Entries are keyed by a **presence key**. * The best key is a stable `instanceId` (from `connect.client.instanceId`) that survives restarts. * Keys are case‑insensitive. If a client reconnects without a stable `instanceId`, it may show up as a **duplicate** row. ## TTL and bounded size Presence is intentionally ephemeral: * **TTL:** entries older than 5 minutes are pruned * **Max entries:** 200 (oldest dropped first) This keeps the list fresh and avoids unbounded memory growth. ## Remote/tunnel caveat (loopback IPs) When a client connects over an SSH tunnel / local port forward, the Gateway may see the remote address as `127.0.0.1`. To avoid overwriting a good client‑reported IP, loopback remote addresses are ignored. ## Consumers ### macOS Instances tab The macOS app renders the output of `system-presence` and applies a small status indicator (Active/Idle/Stale) based on the age of the last update. ## Debugging tips * To see the raw list, call `system-presence` against the Gateway. * If you see duplicates: * confirm clients send a stable `client.instanceId` in the handshake * confirm periodic beacons use the same `instanceId` * check whether the connection‑derived entry is missing `instanceId` (duplicates are expected) # Command Queue Source: https://cryptoclawdocs.termix.ai/concepts/queue # Command Queue (2026-01-16) We serialize inbound auto-reply runs (all channels) through a tiny in-process queue to prevent multiple agent runs from colliding, while still allowing safe parallelism across sessions. ## Why * Auto-reply runs can be expensive (LLM calls) and can collide when multiple inbound messages arrive close together. * Serializing avoids competing for shared resources (session files, logs, CLI stdin) and reduces the chance of upstream rate limits. ## How it works * A lane-aware FIFO queue drains each lane with a configurable concurrency cap (default 1 for unconfigured lanes; main defaults to 4, subagent to 8). * `runEmbeddedPiAgent` enqueues by **session key** (lane `session:`) to guarantee only one active run per session. * Each session run is then queued into a **global lane** (`main` by default) so overall parallelism is capped by `agents.defaults.maxConcurrent`. * When verbose logging is enabled, queued runs emit a short notice if they waited more than \~2s before starting. * Typing indicators still fire immediately on enqueue (when supported by the channel) so user experience is unchanged while we wait our turn. ## Queue modes (per channel) Inbound messages can steer the current run, wait for a followup turn, or do both: * `steer`: inject immediately into the current run (cancels pending tool calls after the next tool boundary). If not streaming, falls back to followup. * `followup`: enqueue for the next agent turn after the current run ends. * `collect`: coalesce all queued messages into a **single** followup turn (default). If messages target different channels/threads, they drain individually to preserve routing. * `steer-backlog` (aka `steer+backlog`): steer now **and** preserve the message for a followup turn. * `interrupt` (legacy): abort the active run for that session, then run the newest message. * `queue` (legacy alias): same as `steer`. Steer-backlog means you can get a followup response after the steered run, so streaming surfaces can look like duplicates. Prefer `collect`/`steer` if you want one response per inbound message. Send `/queue collect` as a standalone command (per-session) or set `messages.queue.byChannel.discord: "collect"`. Defaults (when unset in config): * All surfaces → `collect` Configure globally or per channel via `messages.queue`: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { messages: { queue: { mode: "collect", debounceMs: 1000, cap: 20, drop: "summarize", byChannel: { discord: "collect" }, }, }, } ``` ## Queue options Options apply to `followup`, `collect`, and `steer-backlog` (and to `steer` when it falls back to followup): * `debounceMs`: wait for quiet before starting a followup turn (prevents “continue, continue”). * `cap`: max queued messages per session. * `drop`: overflow policy (`old`, `new`, `summarize`). Summarize keeps a short bullet list of dropped messages and injects it as a synthetic followup prompt. Defaults: `debounceMs: 1000`, `cap: 20`, `drop: summarize`. ## Per-session overrides * Send `/queue ` as a standalone command to store the mode for the current session. * Options can be combined: `/queue collect debounce:2s cap:25 drop:summarize` * `/queue default` or `/queue reset` clears the session override. ## Scope and guarantees * Applies to auto-reply agent runs across all inbound channels that use the gateway reply pipeline (WhatsApp web, Telegram, Slack, Discord, Signal, iMessage, webchat, etc.). * Default lane (`main`) is process-wide for inbound + main heartbeats; set `agents.defaults.maxConcurrent` to allow multiple sessions in parallel. * Additional lanes may exist (e.g. `cron`, `subagent`) so background jobs can run in parallel without blocking inbound replies. * Per-session lanes guarantee that only one agent run touches a given session at a time. * No external dependencies or background worker threads; pure TypeScript + promises. ## Troubleshooting * If commands seem stuck, enable verbose logs and look for “queued for …ms” lines to confirm the queue is draining. * If you need queue depth, enable verbose logs and watch for queue timing lines. # Retry Policy Source: https://cryptoclawdocs.termix.ai/concepts/retry # Retry policy ## Goals * Retry per HTTP request, not per multi-step flow. * Preserve ordering by retrying only the current step. * Avoid duplicating non-idempotent operations. ## Defaults * Attempts: 3 * Max delay cap: 30000 ms * Jitter: 0.1 (10 percent) * Provider defaults: * Telegram min delay: 400 ms * Discord min delay: 500 ms ## Behavior ### Discord * Retries only on rate-limit errors (HTTP 429). * Uses Discord `retry_after` when available, otherwise exponential backoff. ### Telegram * Retries on transient errors (429, timeout, connect/reset/closed, temporarily unavailable). * Uses `retry_after` when available, otherwise exponential backoff. * Markdown parse errors are not retried; they fall back to plain text. ## Configuration Set retry policy per provider in `~/.openclaw/openclaw.json`: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { telegram: { retry: { attempts: 3, minDelayMs: 400, maxDelayMs: 30000, jitter: 0.1, }, }, discord: { retry: { attempts: 3, minDelayMs: 500, maxDelayMs: 30000, jitter: 0.1, }, }, }, } ``` ## Notes * Retries apply per request (message send, media upload, reaction, poll, sticker). * Composite flows do not retry completed steps. # Session Management Source: https://cryptoclawdocs.termix.ai/concepts/session # Session Management OpenClaw treats **one direct-chat session per agent** as primary. Direct chats collapse to `agent::` (default `main`), while group/channel chats get their own keys. `session.mainKey` is honored. Use `session.dmScope` to control how **direct messages** are grouped: * `main` (default): all DMs share the main session for continuity. * `per-peer`: isolate by sender id across channels. * `per-channel-peer`: isolate by channel + sender (recommended for multi-user inboxes). * `per-account-channel-peer`: isolate by account + channel + sender (recommended for multi-account inboxes). Use `session.identityLinks` to map provider-prefixed peer ids to a canonical identity so the same person shares a DM session across channels when using `per-peer`, `per-channel-peer`, or `per-account-channel-peer`. ## Secure DM mode (recommended for multi-user setups) > **Security Warning:** If your agent can receive DMs from **multiple people**, you should strongly consider enabling secure DM mode. Without it, all users share the same conversation context, which can leak private information between users. **Example of the problem with default settings:** * Alice (``) messages your agent about a private topic (for example, a medical appointment) * Bob (``) messages your agent asking "What were we talking about?" * Because both DMs share the same session, the model may answer Bob using Alice's prior context. **The fix:** Set `dmScope` to isolate sessions per user: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} // ~/.openclaw/openclaw.json { session: { // Secure DM mode: isolate DM context per channel + sender. dmScope: "per-channel-peer", }, } ``` **When to enable this:** * You have pairing approvals for more than one sender * You use a DM allowlist with multiple entries * You set `dmPolicy: "open"` * Multiple phone numbers or accounts can message your agent Notes: * Default is `dmScope: "main"` for continuity (all DMs share the main session). This is fine for single-user setups. * Local CLI onboarding writes `session.dmScope: "per-channel-peer"` by default when unset (existing explicit values are preserved). * For multi-account inboxes on the same channel, prefer `per-account-channel-peer`. * If the same person contacts you on multiple channels, use `session.identityLinks` to collapse their DM sessions into one canonical identity. * You can verify your DM settings with `openclaw security audit` (see [security](/cli/security)). ## Gateway is the source of truth All session state is **owned by the gateway** (the “master” OpenClaw). UI clients (macOS app, WebChat, etc.) must query the gateway for session lists and token counts instead of reading local files. * In **remote mode**, the session store you care about lives on the remote gateway host, not your Mac. * Token counts shown in UIs come from the gateway’s store fields (`inputTokens`, `outputTokens`, `totalTokens`, `contextTokens`). Clients do not parse JSONL transcripts to “fix up” totals. ## Where state lives * On the **gateway host**: * Store file: `~/.openclaw/agents//sessions/sessions.json` (per agent). * Transcripts: `~/.openclaw/agents//sessions/.jsonl` (Telegram topic sessions use `.../-topic-.jsonl`). * The store is a map `sessionKey -> { sessionId, updatedAt, ... }`. Deleting entries is safe; they are recreated on demand. * Group entries may include `displayName`, `channel`, `subject`, `room`, and `space` to label sessions in UIs. * Session entries include `origin` metadata (label + routing hints) so UIs can explain where a session came from. * OpenClaw does **not** read legacy Pi/Tau session folders. ## Maintenance OpenClaw applies session-store maintenance to keep `sessions.json` and transcript artifacts bounded over time. ### Defaults * `session.maintenance.mode`: `warn` * `session.maintenance.pruneAfter`: `30d` * `session.maintenance.maxEntries`: `500` * `session.maintenance.rotateBytes`: `10mb` * `session.maintenance.resetArchiveRetention`: defaults to `pruneAfter` (`30d`) * `session.maintenance.maxDiskBytes`: unset (disabled) * `session.maintenance.highWaterBytes`: defaults to `80%` of `maxDiskBytes` when budgeting is enabled ### How it works Maintenance runs during session-store writes, and you can trigger it on demand with `openclaw sessions cleanup`. * `mode: "warn"`: reports what would be evicted but does not mutate entries/transcripts. * `mode: "enforce"`: applies cleanup in this order: 1. prune stale entries older than `pruneAfter` 2. cap entry count to `maxEntries` (oldest first) 3. archive transcript files for removed entries that are no longer referenced 4. purge old `*.deleted.` and `*.reset.` archives by retention policy 5. rotate `sessions.json` when it exceeds `rotateBytes` 6. if `maxDiskBytes` is set, enforce disk budget toward `highWaterBytes` (oldest artifacts first, then oldest sessions) ### Performance caveat for large stores Large session stores are common in high-volume setups. Maintenance work is write-path work, so very large stores can increase write latency. What increases cost most: * very high `session.maintenance.maxEntries` values * long `pruneAfter` windows that keep stale entries around * many transcript/archive artifacts in `~/.openclaw/agents//sessions/` * enabling disk budgets (`maxDiskBytes`) without reasonable pruning/cap limits What to do: * use `mode: "enforce"` in production so growth is bounded automatically * set both time and count limits (`pruneAfter` + `maxEntries`), not just one * set `maxDiskBytes` + `highWaterBytes` for hard upper bounds in large deployments * keep `highWaterBytes` meaningfully below `maxDiskBytes` (default is 80%) * run `openclaw sessions cleanup --dry-run --json` after config changes to verify projected impact before enforcing * for frequent active sessions, pass `--active-key` when running manual cleanup ### Customize examples Use a conservative enforce policy: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { session: { maintenance: { mode: "enforce", pruneAfter: "45d", maxEntries: 800, rotateBytes: "20mb", resetArchiveRetention: "14d", }, }, } ``` Enable a hard disk budget for the sessions directory: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { session: { maintenance: { mode: "enforce", maxDiskBytes: "1gb", highWaterBytes: "800mb", }, }, } ``` Tune for larger installs (example): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { session: { maintenance: { mode: "enforce", pruneAfter: "14d", maxEntries: 2000, rotateBytes: "25mb", maxDiskBytes: "2gb", highWaterBytes: "1.6gb", }, }, } ``` Preview or force maintenance from CLI: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw sessions cleanup --dry-run openclaw sessions cleanup --enforce ``` ## Session pruning OpenClaw trims **old tool results** from the in-memory context right before LLM calls by default. This does **not** rewrite JSONL history. See [/concepts/session-pruning](/concepts/session-pruning). ## Pre-compaction memory flush When a session nears auto-compaction, OpenClaw can run a **silent memory flush** turn that reminds the model to write durable notes to disk. This only runs when the workspace is writable. See [Memory](/concepts/memory) and [Compaction](/concepts/compaction). ## Mapping transports → session keys * Direct chats follow `session.dmScope` (default `main`). * `main`: `agent::` (continuity across devices/channels). * Multiple phone numbers and channels can map to the same agent main key; they act as transports into one conversation. * `per-peer`: `agent::dm:`. * `per-channel-peer`: `agent:::dm:`. * `per-account-channel-peer`: `agent::::dm:` (accountId defaults to `default`). * If `session.identityLinks` matches a provider-prefixed peer id (for example `telegram:123`), the canonical key replaces `` so the same person shares a session across channels. * Group chats isolate state: `agent:::group:` (rooms/channels use `agent:::channel:`). * Telegram forum topics append `:topic:` to the group id for isolation. * Legacy `group:` keys are still recognized for migration. * Inbound contexts may still use `group:`; the channel is inferred from `Provider` and normalized to the canonical `agent:::group:` form. * Other sources: * Cron jobs: `cron:` * Webhooks: `hook:` (unless explicitly set by the hook) * Node runs: `node-` ## Lifecycle * Reset policy: sessions are reused until they expire, and expiry is evaluated on the next inbound message. * Daily reset: defaults to **4:00 AM local time on the gateway host**. A session is stale once its last update is earlier than the most recent daily reset time. * Idle reset (optional): `idleMinutes` adds a sliding idle window. When both daily and idle resets are configured, **whichever expires first** forces a new session. * Legacy idle-only: if you set `session.idleMinutes` without any `session.reset`/`resetByType` config, OpenClaw stays in idle-only mode for backward compatibility. * Per-type overrides (optional): `resetByType` lets you override the policy for `direct`, `group`, and `thread` sessions (thread = Slack/Discord threads, Telegram topics, Matrix threads when provided by the connector). * Per-channel overrides (optional): `resetByChannel` overrides the reset policy for a channel (applies to all session types for that channel and takes precedence over `reset`/`resetByType`). * Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. `/new ` accepts a model alias, `provider/model`, or provider name (fuzzy match) to set the new session model. If `/new` or `/reset` is sent alone, OpenClaw runs a short “hello” greeting turn to confirm the reset. * Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them. * Isolated cron jobs always mint a fresh `sessionId` per run (no idle reuse). ## Send policy (optional) Block delivery for specific session types without listing individual ids. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { session: { sendPolicy: { rules: [ { action: "deny", match: { channel: "discord", chatType: "group" } }, { action: "deny", match: { keyPrefix: "cron:" } }, // Match the raw session key (including the `agent::` prefix). { action: "deny", match: { rawKeyPrefix: "agent:main:discord:" } }, ], default: "allow", }, }, } ``` Runtime override (owner only): * `/send on` → allow for this session * `/send off` → deny for this session * `/send inherit` → clear override and use config rules Send these as standalone messages so they register. ## Configuration (optional rename example) ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} // ~/.openclaw/openclaw.json { session: { scope: "per-sender", // keep group keys separate dmScope: "main", // DM continuity (set per-channel-peer/per-account-channel-peer for shared inboxes) identityLinks: { alice: ["telegram:123456789", "discord:987654321012345678"], }, reset: { // Defaults: mode=daily, atHour=4 (gateway host local time). // If you also set idleMinutes, whichever expires first wins. mode: "daily", atHour: 4, idleMinutes: 120, }, resetByType: { thread: { mode: "daily", atHour: 4 }, direct: { mode: "idle", idleMinutes: 240 }, group: { mode: "idle", idleMinutes: 120 }, }, resetByChannel: { discord: { mode: "idle", idleMinutes: 10080 }, }, resetTriggers: ["/new", "/reset"], store: "~/.openclaw/agents/{agentId}/sessions/sessions.json", mainKey: "main", }, } ``` ## Inspecting * `openclaw status` — shows store path and recent sessions. * `openclaw sessions --json` — dumps every entry (filter with `--active `). * `openclaw gateway call sessions.list --params '{}'` — fetch sessions from the running gateway (use `--url`/`--token` for remote gateway access). * Send `/status` as a standalone message in chat to see whether the agent is reachable, how much of the session context is used, current thinking/verbose toggles, and when your WhatsApp web creds were last refreshed (helps spot relink needs). * Send `/context list` or `/context detail` to see what’s in the system prompt and injected workspace files (and the biggest context contributors). * Send `/stop` (or standalone abort phrases like `stop`, `stop action`, `stop run`, `stop openclaw`) to abort the current run, clear queued followups for that session, and stop any sub-agent runs spawned from it (the reply includes the stopped count). * Send `/compact` (optional instructions) as a standalone message to summarize older context and free up window space. See [/concepts/compaction](/concepts/compaction). * JSONL transcripts can be opened directly to review full turns. ## Tips * Keep the primary key dedicated to 1:1 traffic; let groups keep their own keys. * When automating cleanup, delete individual keys instead of the whole store to preserve context elsewhere. ## Session origin metadata Each session entry records where it came from (best-effort) in `origin`: * `label`: human label (resolved from conversation label + group subject/channel) * `provider`: normalized channel id (including extensions) * `from`/`to`: raw routing ids from the inbound envelope * `accountId`: provider account id (when multi-account) * `threadId`: thread/topic id when the channel supports it The origin fields are populated for direct messages, channels, and groups. If a connector only updates delivery routing (for example, to keep a DM main session fresh), it should still provide inbound context so the session keeps its explainer metadata. Extensions can do this by sending `ConversationLabel`, `GroupSubject`, `GroupChannel`, `GroupSpace`, and `SenderName` in the inbound context and calling `recordSessionMetaFromInbound` (or passing the same context to `updateLastRoute`). # Session Pruning Source: https://cryptoclawdocs.termix.ai/concepts/session-pruning # Session Pruning Session pruning trims **old tool results** from the in-memory context right before each LLM call. It does **not** rewrite the on-disk session history (`*.jsonl`). ## When it runs * When `mode: "cache-ttl"` is enabled and the last Anthropic call for the session is older than `ttl`. * Only affects the messages sent to the model for that request. * Only active for Anthropic API calls (and OpenRouter Anthropic models). * For best results, match `ttl` to your model `cacheRetention` policy (`short` = 5m, `long` = 1h). * After a prune, the TTL window resets so subsequent requests keep cache until `ttl` expires again. ## Smart defaults (Anthropic) * **OAuth or setup-token** profiles: enable `cache-ttl` pruning and set heartbeat to `1h`. * **API key** profiles: enable `cache-ttl` pruning, set heartbeat to `30m`, and default `cacheRetention: "short"` on Anthropic models. * If you set any of these values explicitly, OpenClaw does **not** override them. ## What this improves (cost + cache behavior) * **Why prune:** Anthropic prompt caching only applies within the TTL. If a session goes idle past the TTL, the next request re-caches the full prompt unless you trim it first. * **What gets cheaper:** pruning reduces the **cacheWrite** size for that first request after the TTL expires. * **Why the TTL reset matters:** once pruning runs, the cache window resets, so follow‑up requests can reuse the freshly cached prompt instead of re-caching the full history again. * **What it does not do:** pruning doesn’t add tokens or “double” costs; it only changes what gets cached on that first post‑TTL request. ## What can be pruned * Only `toolResult` messages. * User + assistant messages are **never** modified. * The last `keepLastAssistants` assistant messages are protected; tool results after that cutoff are not pruned. * If there aren’t enough assistant messages to establish the cutoff, pruning is skipped. * Tool results containing **image blocks** are skipped (never trimmed/cleared). ## Context window estimation Pruning uses an estimated context window (chars ≈ tokens × 4). The base window is resolved in this order: 1. `models.providers.*.models[].contextWindow` override. 2. Model definition `contextWindow` (from the model registry). 3. Default `200000` tokens. If `agents.defaults.contextTokens` is set, it is treated as a cap (min) on the resolved window. ## Mode ### cache-ttl * Pruning only runs if the last Anthropic call is older than `ttl` (default `5m`). * When it runs: same soft-trim + hard-clear behavior as before. ## Soft vs hard pruning * **Soft-trim**: only for oversized tool results. * Keeps head + tail, inserts `...`, and appends a note with the original size. * Skips results with image blocks. * **Hard-clear**: replaces the entire tool result with `hardClear.placeholder`. ## Tool selection * `tools.allow` / `tools.deny` support `*` wildcards. * Deny wins. * Matching is case-insensitive. * Empty allow list => all tools allowed. ## Interaction with other limits * Built-in tools already truncate their own output; session pruning is an extra layer that prevents long-running chats from accumulating too much tool output in the model context. * Compaction is separate: compaction summarizes and persists, pruning is transient per request. See [/concepts/compaction](/concepts/compaction). ## Defaults (when enabled) * `ttl`: `"5m"` * `keepLastAssistants`: `3` * `softTrimRatio`: `0.3` * `hardClearRatio`: `0.5` * `minPrunableToolChars`: `50000` * `softTrim`: `{ maxChars: 4000, headChars: 1500, tailChars: 1500 }` * `hardClear`: `{ enabled: true, placeholder: "[Old tool result content cleared]" }` ## Examples Default (off): ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { defaults: { contextPruning: { mode: "off" } } }, } ``` Enable TTL-aware pruning: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { defaults: { contextPruning: { mode: "cache-ttl", ttl: "5m" } } }, } ``` Restrict pruning to specific tools: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { defaults: { contextPruning: { mode: "cache-ttl", tools: { allow: ["exec", "read"], deny: ["*image*"] }, }, }, }, } ``` See config reference: [Gateway Configuration](/gateway/configuration) # Session Tools Source: https://cryptoclawdocs.termix.ai/concepts/session-tool # Session Tools Goal: small, hard-to-misuse tool set so agents can list sessions, fetch history, and send to another session. ## Tool Names * `sessions_list` * `sessions_history` * `sessions_send` * `sessions_spawn` ## Key Model * Main direct chat bucket is always the literal key `"main"` (resolved to the current agent’s main key). * Group chats use `agent:::group:` or `agent:::channel:` (pass the full key). * Cron jobs use `cron:`. * Hooks use `hook:` unless explicitly set. * Node sessions use `node-` unless explicitly set. `global` and `unknown` are reserved values and are never listed. If `session.scope = "global"`, we alias it to `main` for all tools so callers never see `global`. ## sessions\_list List sessions as an array of rows. Parameters: * `kinds?: string[]` filter: any of `"main" | "group" | "cron" | "hook" | "node" | "other"` * `limit?: number` max rows (default: server default, clamp e.g. 200) * `activeMinutes?: number` only sessions updated within N minutes * `messageLimit?: number` 0 = no messages (default 0); >0 = include last N messages Behavior: * `messageLimit > 0` fetches `chat.history` per session and includes the last N messages. * Tool results are filtered out in list output; use `sessions_history` for tool messages. * When running in a **sandboxed** agent session, session tools default to **spawned-only visibility** (see below). Row shape (JSON): * `key`: session key (string) * `kind`: `main | group | cron | hook | node | other` * `channel`: `whatsapp | telegram | discord | signal | imessage | webchat | internal | unknown` * `displayName` (group display label if available) * `updatedAt` (ms) * `sessionId` * `model`, `contextTokens`, `totalTokens` * `thinkingLevel`, `verboseLevel`, `systemSent`, `abortedLastRun` * `sendPolicy` (session override if set) * `lastChannel`, `lastTo` * `deliveryContext` (normalized `{ channel, to, accountId }` when available) * `transcriptPath` (best-effort path derived from store dir + sessionId) * `messages?` (only when `messageLimit > 0`) ## sessions\_history Fetch transcript for one session. Parameters: * `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`) * `limit?: number` max messages (server clamps) * `includeTools?: boolean` (default false) Behavior: * `includeTools=false` filters `role: "toolResult"` messages. * Returns messages array in the raw transcript format. * When given a `sessionId`, OpenClaw resolves it to the corresponding session key (missing ids error). ## sessions\_send Send a message into another session. Parameters: * `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`) * `message` (required) * `timeoutSeconds?: number` (default >0; 0 = fire-and-forget) Behavior: * `timeoutSeconds = 0`: enqueue and return `{ runId, status: "accepted" }`. * `timeoutSeconds > 0`: wait up to N seconds for completion, then return `{ runId, status: "ok", reply }`. * If wait times out: `{ runId, status: "timeout", error }`. Run continues; call `sessions_history` later. * If the run fails: `{ runId, status: "error", error }`. * Announce delivery runs after the primary run completes and is best-effort; `status: "ok"` does not guarantee the announce was delivered. * Waits via gateway `agent.wait` (server-side) so reconnects don't drop the wait. * Agent-to-agent message context is injected for the primary run. * Inter-session messages are persisted with `message.provenance.kind = "inter_session"` so transcript readers can distinguish routed agent instructions from external user input. * After the primary run completes, OpenClaw runs a **reply-back loop**: * Round 2+ alternates between requester and target agents. * Reply exactly `REPLY_SKIP` to stop the ping‑pong. * Max turns is `session.agentToAgent.maxPingPongTurns` (0–5, default 5). * Once the loop ends, OpenClaw runs the **agent‑to‑agent announce step** (target agent only): * Reply exactly `ANNOUNCE_SKIP` to stay silent. * Any other reply is sent to the target channel. * Announce step includes the original request + round‑1 reply + latest ping‑pong reply. ## Channel Field * For groups, `channel` is the channel recorded on the session entry. * For direct chats, `channel` maps from `lastChannel`. * For cron/hook/node, `channel` is `internal`. * If missing, `channel` is `unknown`. ## Security / Send Policy Policy-based blocking by channel/chat type (not per session id). ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "session": { "sendPolicy": { "rules": [ { "match": { "channel": "discord", "chatType": "group" }, "action": "deny" } ], "default": "allow" } } } ``` Runtime override (per session entry): * `sendPolicy: "allow" | "deny"` (unset = inherit config) * Settable via `sessions.patch` or owner-only `/send on|off|inherit` (standalone message). Enforcement points: * `chat.send` / `agent` (gateway) * auto-reply delivery logic ## sessions\_spawn Spawn a sub-agent run in an isolated session and announce the result back to the requester chat channel. Parameters: * `task` (required) * `label?` (optional; used for logs/UI) * `agentId?` (optional; spawn under another agent id if allowed) * `model?` (optional; overrides the sub-agent model; invalid values error) * `thinking?` (optional; overrides thinking level for the sub-agent run) * `runTimeoutSeconds?` (defaults to `agents.defaults.subagents.runTimeoutSeconds` when set, otherwise `0`; when set, aborts the sub-agent run after N seconds) * `thread?` (default false; request thread-bound routing for this spawn when supported by the channel/plugin) * `mode?` (`run|session`; defaults to `run`, but defaults to `session` when `thread=true`; `mode="session"` requires `thread=true`) * `cleanup?` (`delete|keep`, default `keep`) * `sandbox?` (`inherit|require`, default `inherit`; `require` rejects spawn unless the target child runtime is sandboxed) * `attachments?` (optional array of inline files; subagent runtime only, ACP rejects). Each entry: `{ name, content, encoding?: "utf8" | "base64", mimeType? }`. Files are materialized into the child workspace at `.openclaw/attachments//`. Returns a receipt with sha256 per file. * `attachAs?` (optional; `{ mountPath? }` hint reserved for future mount implementations) Allowlist: * `agents.list[].subagents.allowAgents`: list of agent ids allowed via `agentId` (`["*"]` to allow any). Default: only the requester agent. * Sandbox inheritance guard: if the requester session is sandboxed, `sessions_spawn` rejects targets that would run unsandboxed. Discovery: * Use `agents_list` to discover which agent ids are allowed for `sessions_spawn`. Behavior: * Starts a new `agent::subagent:` session with `deliver: false`. * Sub-agents default to the full tool set **minus session tools** (configurable via `tools.subagents.tools`). * Sub-agents are not allowed to call `sessions_spawn` (no sub-agent → sub-agent spawning). * Always non-blocking: returns `{ status: "accepted", runId, childSessionKey }` immediately. * With `thread=true`, channel plugins can bind delivery/routing to a thread target (Discord support is controlled by `session.threadBindings.*` and `channels.discord.threadBindings.*`). * After completion, OpenClaw runs a sub-agent **announce step** and posts the result to the requester chat channel. * If the assistant final reply is empty, the latest `toolResult` from sub-agent history is included as `Result`. * Reply exactly `ANNOUNCE_SKIP` during the announce step to stay silent. * Announce replies are normalized to `Status`/`Result`/`Notes`; `Status` comes from runtime outcome (not model text). * Sub-agent sessions are auto-archived after `agents.defaults.subagents.archiveAfterMinutes` (default: 60). * Announce replies include a stats line (runtime, tokens, sessionKey/sessionId, transcript path, and optional cost). ## Sandbox Session Visibility Session tools can be scoped to reduce cross-session access. Default behavior: * `tools.sessions.visibility` defaults to `tree` (current session + spawned subagent sessions). * For sandboxed sessions, `agents.defaults.sandbox.sessionToolsVisibility` can hard-clamp visibility. Config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { tools: { sessions: { // "self" | "tree" | "agent" | "all" // default: "tree" visibility: "tree", }, }, agents: { defaults: { sandbox: { // default: "spawned" sessionToolsVisibility: "spawned", // or "all" }, }, }, } ``` Notes: * `self`: only the current session key. * `tree`: current session + sessions spawned by the current session. * `agent`: any session belonging to the current agent id. * `all`: any session (cross-agent access still requires `tools.agentToAgent`). * When a session is sandboxed and `sessionToolsVisibility="spawned"`, OpenClaw clamps visibility to `tree` even if you set `tools.sessions.visibility="all"`. # Streaming and Chunking Source: https://cryptoclawdocs.termix.ai/concepts/streaming # Streaming + chunking OpenClaw has two separate streaming layers: * **Block streaming (channels):** emit completed **blocks** as the assistant writes. These are normal channel messages (not token deltas). * **Preview streaming (Telegram/Discord/Slack):** update a temporary **preview message** while generating. There is **no true token-delta streaming** to channel messages today. Preview streaming is message-based (send + edits/appends). ## Block streaming (channel messages) Block streaming sends assistant output in coarse chunks as it becomes available. ``` Model output └─ text_delta/events ├─ (blockStreamingBreak=text_end) │ └─ chunker emits blocks as buffer grows └─ (blockStreamingBreak=message_end) └─ chunker flushes at message_end └─ channel send (block replies) ``` Legend: * `text_delta/events`: model stream events (may be sparse for non-streaming models). * `chunker`: `EmbeddedBlockChunker` applying min/max bounds + break preference. * `channel send`: actual outbound messages (block replies). **Controls:** * `agents.defaults.blockStreamingDefault`: `"on"`/`"off"` (default off). * Channel overrides: `*.blockStreaming` (and per-account variants) to force `"on"`/`"off"` per channel. * `agents.defaults.blockStreamingBreak`: `"text_end"` or `"message_end"`. * `agents.defaults.blockStreamingChunk`: `{ minChars, maxChars, breakPreference? }`. * `agents.defaults.blockStreamingCoalesce`: `{ minChars?, maxChars?, idleMs? }` (merge streamed blocks before send). * Channel hard cap: `*.textChunkLimit` (e.g., `channels.whatsapp.textChunkLimit`). * Channel chunk mode: `*.chunkMode` (`length` default, `newline` splits on blank lines (paragraph boundaries) before length chunking). * Discord soft cap: `channels.discord.maxLinesPerMessage` (default 17) splits tall replies to avoid UI clipping. **Boundary semantics:** * `text_end`: stream blocks as soon as chunker emits; flush on each `text_end`. * `message_end`: wait until assistant message finishes, then flush buffered output. `message_end` still uses the chunker if the buffered text exceeds `maxChars`, so it can emit multiple chunks at the end. ## Chunking algorithm (low/high bounds) Block chunking is implemented by `EmbeddedBlockChunker`: * **Low bound:** don’t emit until buffer >= `minChars` (unless forced). * **High bound:** prefer splits before `maxChars`; if forced, split at `maxChars`. * **Break preference:** `paragraph` → `newline` → `sentence` → `whitespace` → hard break. * **Code fences:** never split inside fences; when forced at `maxChars`, close + reopen the fence to keep Markdown valid. `maxChars` is clamped to the channel `textChunkLimit`, so you can’t exceed per-channel caps. ## Coalescing (merge streamed blocks) When block streaming is enabled, OpenClaw can **merge consecutive block chunks** before sending them out. This reduces “single-line spam” while still providing progressive output. * Coalescing waits for **idle gaps** (`idleMs`) before flushing. * Buffers are capped by `maxChars` and will flush if they exceed it. * `minChars` prevents tiny fragments from sending until enough text accumulates (final flush always sends remaining text). * Joiner is derived from `blockStreamingChunk.breakPreference` (`paragraph` → `\n\n`, `newline` → `\n`, `sentence` → space). * Channel overrides are available via `*.blockStreamingCoalesce` (including per-account configs). * Default coalesce `minChars` is bumped to 1500 for Signal/Slack/Discord unless overridden. ## Human-like pacing between blocks When block streaming is enabled, you can add a **randomized pause** between block replies (after the first block). This makes multi-bubble responses feel more natural. * Config: `agents.defaults.humanDelay` (override per agent via `agents.list[].humanDelay`). * Modes: `off` (default), `natural` (800–2500ms), `custom` (`minMs`/`maxMs`). * Applies only to **block replies**, not final replies or tool summaries. ## “Stream chunks or everything” This maps to: * **Stream chunks:** `blockStreamingDefault: "on"` + `blockStreamingBreak: "text_end"` (emit as you go). Non-Telegram channels also need `*.blockStreaming: true`. * **Stream everything at end:** `blockStreamingBreak: "message_end"` (flush once, possibly multiple chunks if very long). * **No block streaming:** `blockStreamingDefault: "off"` (only final reply). **Channel note:** Block streaming is **off unless** `*.blockStreaming` is explicitly set to `true`. Channels can stream a live preview (`channels..streaming`) without block replies. Config location reminder: the `blockStreaming*` defaults live under `agents.defaults`, not the root config. ## Preview streaming modes Canonical key: `channels..streaming` Modes: * `off`: disable preview streaming. * `partial`: single preview that is replaced with latest text. * `block`: preview updates in chunked/appended steps. * `progress`: progress/status preview during generation, final answer at completion. ### Channel mapping | Channel | `off` | `partial` | `block` | `progress` | | -------- | ----- | --------- | ------- | ----------------- | | Telegram | ✅ | ✅ | ✅ | maps to `partial` | | Discord | ✅ | ✅ | ✅ | maps to `partial` | | Slack | ✅ | ✅ | ✅ | ✅ | Slack-only: * `channels.slack.nativeStreaming` toggles Slack native streaming API calls when `streaming=partial` (default: `true`). Legacy key migration: * Telegram: `streamMode` + boolean `streaming` auto-migrate to `streaming` enum. * Discord: `streamMode` + boolean `streaming` auto-migrate to `streaming` enum. * Slack: `streamMode` auto-migrates to `streaming` enum; boolean `streaming` auto-migrates to `nativeStreaming`. ### Runtime behavior Telegram: * Uses Bot API `sendMessageDraft` in DMs when available, and `sendMessage` + `editMessageText` for group/topic preview updates. * Preview streaming is skipped when Telegram block streaming is explicitly enabled (to avoid double-streaming). * `/reasoning stream` can write reasoning to preview. Discord: * Uses send + edit preview messages. * `block` mode uses draft chunking (`draftChunk`). * Preview streaming is skipped when Discord block streaming is explicitly enabled. Slack: * `partial` can use Slack native streaming (`chat.startStream`/`append`/`stop`) when available. * `block` uses append-style draft previews. * `progress` uses status preview text, then final answer. # System Prompt Source: https://cryptoclawdocs.termix.ai/concepts/system-prompt # System Prompt OpenClaw builds a custom system prompt for every agent run. The prompt is **OpenClaw-owned** and does not use the pi-coding-agent default prompt. The prompt is assembled by OpenClaw and injected into each agent run. ## Structure The prompt is intentionally compact and uses fixed sections: * **Tooling**: current tool list + short descriptions. * **Safety**: short guardrail reminder to avoid power-seeking behavior or bypassing oversight. * **Skills** (when available): tells the model how to load skill instructions on demand. * **OpenClaw Self-Update**: how to run `config.apply` and `update.run`. * **Workspace**: working directory (`agents.defaults.workspace`). * **Documentation**: local path to OpenClaw docs (repo or npm package) and when to read them. * **Workspace Files (injected)**: indicates bootstrap files are included below. * **Sandbox** (when enabled): indicates sandboxed runtime, sandbox paths, and whether elevated exec is available. * **Current Date & Time**: user-local time, timezone, and time format. * **Reply Tags**: optional reply tag syntax for supported providers. * **Heartbeats**: heartbeat prompt and ack behavior. * **Runtime**: host, OS, node, model, repo root (when detected), thinking level (one line). * **Reasoning**: current visibility level + /reasoning toggle hint. Safety guardrails in the system prompt are advisory. They guide model behavior but do not enforce policy. Use tool policy, exec approvals, sandboxing, and channel allowlists for hard enforcement; operators can disable these by design. ## Prompt modes OpenClaw can render smaller system prompts for sub-agents. The runtime sets a `promptMode` for each run (not a user-facing config): * `full` (default): includes all sections above. * `minimal`: used for sub-agents; omits **Skills**, **Memory Recall**, **OpenClaw Self-Update**, **Model Aliases**, **User Identity**, **Reply Tags**, **Messaging**, **Silent Replies**, and **Heartbeats**. Tooling, **Safety**, Workspace, Sandbox, Current Date & Time (when known), Runtime, and injected context stay available. * `none`: returns only the base identity line. When `promptMode=minimal`, extra injected prompts are labeled **Subagent Context** instead of **Group Chat Context**. ## Workspace bootstrap injection Bootstrap files are trimmed and appended under **Project Context** so the model sees identity and profile context without needing explicit reads: * `AGENTS.md` * `SOUL.md` * `TOOLS.md` * `IDENTITY.md` * `USER.md` * `HEARTBEAT.md` * `BOOTSTRAP.md` (only on brand-new workspaces) * `MEMORY.md` and/or `memory.md` (when present in the workspace; either or both may be injected) All of these files are **injected into the context window** on every turn, which means they consume tokens. Keep them concise — especially `MEMORY.md`, which can grow over time and lead to unexpectedly high context usage and more frequent compaction. > **Note:** `memory/*.md` daily files are **not** injected automatically. They > are accessed on demand via the `memory_search` and `memory_get` tools, so they > do not count against the context window unless the model explicitly reads them. Large files are truncated with a marker. The max per-file size is controlled by `agents.defaults.bootstrapMaxChars` (default: 20000). Total injected bootstrap content across files is capped by `agents.defaults.bootstrapTotalMaxChars` (default: 150000). Missing files inject a short missing-file marker. When truncation occurs, OpenClaw can inject a warning block in Project Context; control this with `agents.defaults.bootstrapPromptTruncationWarning` (`off`, `once`, `always`; default: `once`). Sub-agent sessions only inject `AGENTS.md` and `TOOLS.md` (other bootstrap files are filtered out to keep the sub-agent context small). Internal hooks can intercept this step via `agent:bootstrap` to mutate or replace the injected bootstrap files (for example swapping `SOUL.md` for an alternate persona). To inspect how much each injected file contributes (raw vs injected, truncation, plus tool schema overhead), use `/context list` or `/context detail`. See [Context](/concepts/context). ## Time handling The system prompt includes a dedicated **Current Date & Time** section when the user timezone is known. To keep the prompt cache-stable, it now only includes the **time zone** (no dynamic clock or time format). Use `session_status` when the agent needs the current time; the status card includes a timestamp line. Configure with: * `agents.defaults.userTimezone` * `agents.defaults.timeFormat` (`auto` | `12` | `24`) See [Date & Time](/date-time) for full behavior details. ## Skills When eligible skills exist, OpenClaw injects a compact **available skills list** (`formatSkillsForPrompt`) that includes the **file path** for each skill. The prompt instructs the model to use `read` to load the SKILL.md at the listed location (workspace, managed, or bundled). If no skills are eligible, the Skills section is omitted. ``` ... ... ... ``` This keeps the base prompt small while still enabling targeted skill usage. ## Documentation When available, the system prompt includes a **Documentation** section that points to the local OpenClaw docs directory (either `docs/` in the repo workspace or the bundled npm package docs) and also notes the public mirror, source repo, community Discord, and ClawHub ([https://clawhub.com](https://clawhub.com)) for skills discovery. The prompt instructs the model to consult local docs first for OpenClaw behavior, commands, configuration, or architecture, and to run `openclaw status` itself when possible (asking the user only when it lacks access). # OpenClaw Source: https://cryptoclawdocs.termix.ai/index # OpenClaw 🦞

OpenClaw OpenClaw

> *"EXFOLIATE! EXFOLIATE!"* — A space lobster, probably

Any OS gateway for AI agents across WhatsApp, Telegram, Discord, iMessage, and more.
Send a message, get an agent response from your pocket. Plugins add Mattermost and more.

Install OpenClaw and bring up the Gateway in minutes. Guided setup with `openclaw onboard` and pairing flows. Launch the browser dashboard for chat, config, and sessions. ## What is OpenClaw? OpenClaw is a **self-hosted gateway** that connects your favorite chat apps — WhatsApp, Telegram, Discord, iMessage, and more — to AI coding agents like Pi. You run a single Gateway process on your own machine (or a server), and it becomes the bridge between your messaging apps and an always-available AI assistant. **Who is it for?** Developers and power users who want a personal AI assistant they can message from anywhere — without giving up control of their data or relying on a hosted service. **What makes it different?** * **Self-hosted**: runs on your hardware, your rules * **Multi-channel**: one Gateway serves WhatsApp, Telegram, Discord, and more simultaneously * **Agent-native**: built for coding agents with tool use, sessions, memory, and multi-agent routing * **Open source**: MIT licensed, community-driven **What do you need?** Node 22+, an API key from your chosen provider, and 5 minutes. For best quality and security, use the strongest latest-generation model available. ## How it works ```mermaid theme={"theme":{"light":"min-light","dark":"min-dark"}} flowchart LR A["Chat apps + plugins"] --> B["Gateway"] B --> C["Pi agent"] B --> D["CLI"] B --> E["Web Control UI"] B --> F["macOS app"] B --> G["iOS and Android nodes"] ``` The Gateway is the single source of truth for sessions, routing, and channel connections. ## Key capabilities WhatsApp, Telegram, Discord, and iMessage with a single Gateway process. Add Mattermost and more with extension packages. Isolated sessions per agent, workspace, or sender. Send and receive images, audio, and documents. Browser dashboard for chat, config, sessions, and nodes. Pair iOS and Android nodes for Canvas, camera/screen, and voice-enabled workflows. ## Quick start ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} npm install -g openclaw@latest ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw onboard --install-daemon ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw channels login openclaw gateway --port 18789 ``` Need the full install and dev setup? See [Quick start](/start/quickstart). ## Dashboard Open the browser Control UI after the Gateway starts. * Local default: [http://127.0.0.1:18789/](http://127.0.0.1:18789/) * Remote access: [Web surfaces](/web) and [Tailscale](/gateway/tailscale)

OpenClaw

## Configuration (optional) Config lives at `~/.openclaw/openclaw.json`. * If you **do nothing**, OpenClaw uses the bundled Pi binary in RPC mode with per-sender sessions. * If you want to lock it down, start with `channels.whatsapp.allowFrom` and (for groups) mention rules. Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { allowFrom: ["+15555550123"], groups: { "*": { requireMention: true } }, }, }, messages: { groupChat: { mentionPatterns: ["@openclaw"] } }, } ``` ## Start here All docs and guides, organized by use case. Core Gateway settings, tokens, and provider config. SSH and tailnet access patterns. Channel-specific setup for WhatsApp, Telegram, Discord, and more. iOS and Android nodes with pairing, Canvas, camera/screen, and device actions. Common fixes and troubleshooting entry point. ## Learn more Complete channel, routing, and media capabilities. Workspace isolation and per-agent sessions. Tokens, allowlists, and safety controls. Gateway diagnostics and common errors. Project origins, contributors, and license. # Ansible Source: https://cryptoclawdocs.termix.ai/install/ansible # Ansible Installation The recommended way to deploy OpenClaw to production servers is via **[openclaw-ansible](https://github.com/openclaw/openclaw-ansible)** — an automated installer with security-first architecture. ## Quick Start One-command install: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL https://raw.githubusercontent.com/openclaw/openclaw-ansible/main/install.sh | bash ``` > **📦 Full guide: [github.com/openclaw/openclaw-ansible](https://github.com/openclaw/openclaw-ansible)** > > The openclaw-ansible repo is the source of truth for Ansible deployment. This page is a quick overview. ## What You Get * 🔒 **Firewall-first security**: UFW + Docker isolation (only SSH + Tailscale accessible) * 🔐 **Tailscale VPN**: Secure remote access without exposing services publicly * 🐳 **Docker**: Isolated sandbox containers, localhost-only bindings * 🛡️ **Defense in depth**: 4-layer security architecture * 🚀 **One-command setup**: Complete deployment in minutes * 🔧 **Systemd integration**: Auto-start on boot with hardening ## Requirements * **OS**: Debian 11+ or Ubuntu 20.04+ * **Access**: Root or sudo privileges * **Network**: Internet connection for package installation * **Ansible**: 2.14+ (installed automatically by quick-start script) ## What Gets Installed The Ansible playbook installs and configures: 1. **Tailscale** (mesh VPN for secure remote access) 2. **UFW firewall** (SSH + Tailscale ports only) 3. **Docker CE + Compose V2** (for agent sandboxes) 4. **Node.js 22.x + pnpm** (runtime dependencies) 5. **OpenClaw** (host-based, not containerized) 6. **Systemd service** (auto-start with security hardening) Note: The gateway runs **directly on the host** (not in Docker), but agent sandboxes use Docker for isolation. See [Sandboxing](/gateway/sandboxing) for details. ## Post-Install Setup After installation completes, switch to the openclaw user: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} sudo -i -u openclaw ``` The post-install script will guide you through: 1. **Onboarding wizard**: Configure OpenClaw settings 2. **Provider login**: Connect WhatsApp/Telegram/Discord/Signal 3. **Gateway testing**: Verify the installation 4. **Tailscale setup**: Connect to your VPN mesh ### Quick commands ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Check service status sudo systemctl status openclaw # View live logs sudo journalctl -u openclaw -f # Restart gateway sudo systemctl restart openclaw # Provider login (run as openclaw user) sudo -i -u openclaw openclaw channels login ``` ## Security Architecture ### 4-Layer Defense 1. **Firewall (UFW)**: Only SSH (22) + Tailscale (41641/udp) exposed publicly 2. **VPN (Tailscale)**: Gateway accessible only via VPN mesh 3. **Docker Isolation**: DOCKER-USER iptables chain prevents external port exposure 4. **Systemd Hardening**: NoNewPrivileges, PrivateTmp, unprivileged user ### Verification Test external attack surface: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} nmap -p- YOUR_SERVER_IP ``` Should show **only port 22** (SSH) open. All other services (gateway, Docker) are locked down. ### Docker Availability Docker is installed for **agent sandboxes** (isolated tool execution), not for running the gateway itself. The gateway binds to localhost only and is accessible via Tailscale VPN. See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for sandbox configuration. ## Manual Installation If you prefer manual control over the automation: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # 1. Install prerequisites sudo apt update && sudo apt install -y ansible git # 2. Clone repository git clone https://github.com/openclaw/openclaw-ansible.git cd openclaw-ansible # 3. Install Ansible collections ansible-galaxy collection install -r requirements.yml # 4. Run playbook ./run-playbook.sh # Or run directly (then manually execute /tmp/openclaw-setup.sh after) # ansible-playbook playbook.yml --ask-become-pass ``` ## Updating OpenClaw The Ansible installer sets up OpenClaw for manual updates. See [Updating](/install/updating) for the standard update flow. To re-run the Ansible playbook (e.g., for configuration changes): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} cd openclaw-ansible ./run-playbook.sh ``` Note: This is idempotent and safe to run multiple times. ## Troubleshooting ### Firewall blocks my connection If you're locked out: * Ensure you can access via Tailscale VPN first * SSH access (port 22) is always allowed * The gateway is **only** accessible via Tailscale by design ### Service won't start ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Check logs sudo journalctl -u openclaw -n 100 # Verify permissions sudo ls -la /opt/openclaw # Test manual start sudo -i -u openclaw cd ~/openclaw pnpm start ``` ### Docker sandbox issues ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Verify Docker is running sudo systemctl status docker # Check sandbox image sudo docker images | grep openclaw-sandbox # Build sandbox image if missing cd /opt/openclaw/openclaw sudo -u openclaw ./scripts/sandbox-setup.sh ``` ### Provider login fails Make sure you're running as the `openclaw` user: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} sudo -i -u openclaw openclaw channels login ``` ## Advanced Configuration For detailed security architecture and troubleshooting: * [Security Architecture](https://github.com/openclaw/openclaw-ansible/blob/main/docs/security.md) * [Technical Details](https://github.com/openclaw/openclaw-ansible/blob/main/docs/architecture.md) * [Troubleshooting Guide](https://github.com/openclaw/openclaw-ansible/blob/main/docs/troubleshooting.md) ## Related * [openclaw-ansible](https://github.com/openclaw/openclaw-ansible) — full deployment guide * [Docker](/install/docker) — containerized gateway setup * [Sandboxing](/gateway/sandboxing) — agent sandbox configuration * [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) — per-agent isolation # Bun (Experimental) Source: https://cryptoclawdocs.termix.ai/install/bun # Bun (experimental) Goal: run this repo with **Bun** (optional, not recommended for WhatsApp/Telegram) without diverging from pnpm workflows. ⚠️ **Not recommended for Gateway runtime** (WhatsApp/Telegram bugs). Use Node for production. ## Status * Bun is an optional local runtime for running TypeScript directly (`bun run …`, `bun --watch …`). * `pnpm` is the default for builds and remains fully supported (and used by some docs tooling). * Bun cannot use `pnpm-lock.yaml` and will ignore it. ## Install Default: ```sh theme={"theme":{"light":"min-light","dark":"min-dark"}} bun install ``` Note: `bun.lock`/`bun.lockb` are gitignored, so there’s no repo churn either way. If you want *no lockfile writes*: ```sh theme={"theme":{"light":"min-light","dark":"min-dark"}} bun install --no-save ``` ## Build / Test (Bun) ```sh theme={"theme":{"light":"min-light","dark":"min-dark"}} bun run build bun run vitest run ``` ## Bun lifecycle scripts (blocked by default) Bun may block dependency lifecycle scripts unless explicitly trusted (`bun pm untrusted` / `bun pm trust`). For this repo, the commonly blocked scripts are not required: * `@whiskeysockets/baileys` `preinstall`: checks Node major >= 20 (we run Node 22+). * `protobufjs` `postinstall`: emits warnings about incompatible version schemes (no build artifacts). If you hit a real runtime issue that requires these scripts, trust them explicitly: ```sh theme={"theme":{"light":"min-light","dark":"min-dark"}} bun pm trust @whiskeysockets/baileys protobufjs ``` ## Caveats * Some scripts still hardcode pnpm (e.g. `docs:build`, `ui:*`, `protocol:check`). Run those via pnpm for now. # Development Channels Source: https://cryptoclawdocs.termix.ai/install/development-channels # Development channels Last updated: 2026-01-21 OpenClaw ships three update channels: * **stable**: npm dist-tag `latest`. * **beta**: npm dist-tag `beta` (builds under test). * **dev**: moving head of `main` (git). npm dist-tag: `dev` (when published). We ship builds to **beta**, test them, then **promote a vetted build to `latest`** without changing the version number — dist-tags are the source of truth for npm installs. ## Switching channels Git checkout: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw update --channel stable openclaw update --channel beta openclaw update --channel dev ``` * `stable`/`beta` check out the latest matching tag (often the same tag). * `dev` switches to `main` and rebases on the upstream. npm/pnpm global install: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw update --channel stable openclaw update --channel beta openclaw update --channel dev ``` This updates via the corresponding npm dist-tag (`latest`, `beta`, `dev`). When you **explicitly** switch channels with `--channel`, OpenClaw also aligns the install method: * `dev` ensures a git checkout (default `~/openclaw`, override with `OPENCLAW_GIT_DIR`), updates it, and installs the global CLI from that checkout. * `stable`/`beta` installs from npm using the matching dist-tag. Tip: if you want stable + dev in parallel, keep two clones and point your gateway at the stable one. ## Plugins and channels When you switch channels with `openclaw update`, OpenClaw also syncs plugin sources: * `dev` prefers bundled plugins from the git checkout. * `stable` and `beta` restore npm-installed plugin packages. ## Tagging best practices * Tag releases you want git checkouts to land on (`vYYYY.M.D` for stable, `vYYYY.M.D-beta.N` for beta). * `vYYYY.M.D.beta.N` is also recognized for compatibility, but prefer `-beta.N`. * Legacy `vYYYY.M.D-` tags are still recognized as stable (non-beta). * Keep tags immutable: never move or reuse a tag. * npm dist-tags remain the source of truth for npm installs: * `latest` → stable * `beta` → candidate build * `dev` → main snapshot (optional) ## macOS app availability Beta and dev builds may **not** include a macOS app release. That’s OK: * The git tag and npm dist-tag can still be published. * Call out “no macOS build for this beta” in release notes or changelog. # Docker Source: https://cryptoclawdocs.termix.ai/install/docker # Docker (optional) Docker is **optional**. Use it only if you want a containerized gateway or to validate the Docker flow. ## Is Docker right for me? * **Yes**: you want an isolated, throwaway gateway environment or to run OpenClaw on a host without local installs. * **No**: you’re running on your own machine and just want the fastest dev loop. Use the normal install flow instead. * **Sandboxing note**: agent sandboxing uses Docker too, but it does **not** require the full gateway to run in Docker. See [Sandboxing](/gateway/sandboxing). This guide covers: * Containerized Gateway (full OpenClaw in Docker) * Per-session Agent Sandbox (host gateway + Docker-isolated agent tools) Sandboxing details: [Sandboxing](/gateway/sandboxing) ## Requirements * Docker Desktop (or Docker Engine) + Docker Compose v2 * At least 2 GB RAM for image build (`pnpm install` may be OOM-killed on 1 GB hosts with exit 137) * Enough disk for images + logs * If running on a VPS/public host, review [Security hardening for network exposure](/gateway/security#04-network-exposure-bind--port--firewall), especially Docker `DOCKER-USER` firewall policy. ## Containerized Gateway (Docker Compose) ### Quick start (recommended) Docker defaults here assume bind modes (`lan`/`loopback`), not host aliases. Use bind mode values in `gateway.bind` (for example `lan` or `loopback`), not host aliases like `0.0.0.0` or `localhost`. From repo root: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ./docker-setup.sh ``` This script: * builds the gateway image locally (or pulls a remote image if `OPENCLAW_IMAGE` is set) * runs the onboarding wizard * prints optional provider setup hints * starts the gateway via Docker Compose * generates a gateway token and writes it to `.env` Optional env vars: * `OPENCLAW_IMAGE` — use a remote image instead of building locally (e.g. `ghcr.io/openclaw/openclaw:latest`) * `OPENCLAW_DOCKER_APT_PACKAGES` — install extra apt packages during build * `OPENCLAW_EXTRA_MOUNTS` — add extra host bind mounts * `OPENCLAW_HOME_VOLUME` — persist `/home/node` in a named volume * `OPENCLAW_SANDBOX` — opt in to Docker gateway sandbox bootstrap. Only explicit truthy values enable it: `1`, `true`, `yes`, `on` * `OPENCLAW_INSTALL_DOCKER_CLI` — build arg passthrough for local image builds (`1` installs Docker CLI in the image). `docker-setup.sh` sets this automatically when `OPENCLAW_SANDBOX=1` for local builds. * `OPENCLAW_DOCKER_SOCKET` — override Docker socket path (default: `DOCKER_HOST=unix://...` path, else `/var/run/docker.sock`) * `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` — break-glass: allow trusted private-network `ws://` targets for CLI/onboarding client paths (default is loopback-only) * `OPENCLAW_BROWSER_DISABLE_GRAPHICS_FLAGS=0` — disable container browser hardening flags `--disable-3d-apis`, `--disable-software-rasterizer`, `--disable-gpu` when you need WebGL/3D compatibility. * `OPENCLAW_BROWSER_DISABLE_EXTENSIONS=0` — keep extensions enabled when browser flows require them (default keeps extensions disabled in sandbox browser). * `OPENCLAW_BROWSER_RENDERER_PROCESS_LIMIT=` — set Chromium renderer process limit; set to `0` to skip the flag and use Chromium default behavior. After it finishes: * Open `http://127.0.0.1:18789/` in your browser. * Paste the token into the Control UI (Settings → token). * Need the URL again? Run `docker compose run --rm openclaw-cli dashboard --no-open`. ### Enable agent sandbox for Docker gateway (opt-in) `docker-setup.sh` can also bootstrap `agents.defaults.sandbox.*` for Docker deployments. Enable with: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export OPENCLAW_SANDBOX=1 ./docker-setup.sh ``` Custom socket path (for example rootless Docker): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export OPENCLAW_SANDBOX=1 export OPENCLAW_DOCKER_SOCKET=/run/user/1000/docker.sock ./docker-setup.sh ``` Notes: * The script mounts `docker.sock` only after sandbox prerequisites pass. * If sandbox setup cannot be completed, the script resets `agents.defaults.sandbox.mode` to `off` to avoid stale/broken sandbox config on reruns. * If `Dockerfile.sandbox` is missing, the script prints a warning and continues; build `openclaw-sandbox:bookworm-slim` with `scripts/sandbox-setup.sh` if needed. * For non-local `OPENCLAW_IMAGE` values, the image must already contain Docker CLI support for sandbox execution. ### Automation/CI (non-interactive, no TTY noise) For scripts and CI, disable Compose pseudo-TTY allocation with `-T`: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose run -T --rm openclaw-cli gateway probe docker compose run -T --rm openclaw-cli devices list --json ``` If your automation exports no Claude session vars, leaving them unset now resolves to empty values by default in `docker-compose.yml` to avoid repeated "variable is not set" warnings. ### Shared-network security note (CLI + gateway) `openclaw-cli` uses `network_mode: "service:openclaw-gateway"` so CLI commands can reliably reach the gateway over `127.0.0.1` in Docker. Treat this as a shared trust boundary: loopback binding is not isolation between these two containers. If you need stronger separation, run commands from a separate container/host network path instead of the bundled `openclaw-cli` service. To reduce impact if the CLI process is compromised, the compose config drops `NET_RAW`/`NET_ADMIN` and enables `no-new-privileges` on `openclaw-cli`. It writes config/workspace on the host: * `~/.openclaw/` * `~/.openclaw/workspace` Running on a VPS? See [Hetzner (Docker VPS)](/install/hetzner). ### Use a remote image (skip local build) Official pre-built images are published at: * [GitHub Container Registry package](https://github.com/openclaw/openclaw/pkgs/container/openclaw) Use image name `ghcr.io/openclaw/openclaw` (not similarly named Docker Hub images). Common tags: * `main` — latest build from `main` * `` — release tag builds (for example `2026.2.26`) * `latest` — latest stable release tag ### Base image metadata The main Docker image currently uses: * `node:22-bookworm` The docker image now publishes OCI base-image annotations (sha256 is an example): * `org.opencontainers.image.base.name=docker.io/library/node:22-bookworm` * `org.opencontainers.image.base.digest=sha256:cd7bcd2e7a1e6f72052feb023c7f6b722205d3fcab7bbcbd2d1bfdab10b1e935` * `org.opencontainers.image.source=https://github.com/openclaw/openclaw` * `org.opencontainers.image.url=https://openclaw.ai` * `org.opencontainers.image.documentation=https://docs.openclaw.ai/install/docker` * `org.opencontainers.image.licenses=MIT` * `org.opencontainers.image.title=OpenClaw` * `org.opencontainers.image.description=OpenClaw gateway and CLI runtime container image` * `org.opencontainers.image.revision=` * `org.opencontainers.image.version=` * `org.opencontainers.image.created=` Reference: [OCI image annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md) Release context: this repository's tagged history already uses Bookworm in `v2026.2.22` and earlier 2026 tags (for example `v2026.2.21`, `v2026.2.9`). By default the setup script builds the image from source. To pull a pre-built image instead, set `OPENCLAW_IMAGE` before running the script: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export OPENCLAW_IMAGE="ghcr.io/openclaw/openclaw:latest" ./docker-setup.sh ``` The script detects that `OPENCLAW_IMAGE` is not the default `openclaw:local` and runs `docker pull` instead of `docker build`. Everything else (onboarding, gateway start, token generation) works the same way. `docker-setup.sh` still runs from the repository root because it uses the local `docker-compose.yml` and helper files. `OPENCLAW_IMAGE` skips local image build time; it does not replace the compose/setup workflow. ### Shell Helpers (optional) For easier day-to-day Docker management, install `ClawDock`: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} mkdir -p ~/.clawdock && curl -sL https://raw.githubusercontent.com/openclaw/openclaw/main/scripts/shell-helpers/clawdock-helpers.sh -o ~/.clawdock/clawdock-helpers.sh ``` **Add to your shell config (zsh):** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} echo 'source ~/.clawdock/clawdock-helpers.sh' >> ~/.zshrc && source ~/.zshrc ``` Then use `clawdock-start`, `clawdock-stop`, `clawdock-dashboard`, etc. Run `clawdock-help` for all commands. See [`ClawDock` Helper README](https://github.com/openclaw/openclaw/blob/main/scripts/shell-helpers/README.md) for details. ### Manual flow (compose) ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker build -t openclaw:local -f Dockerfile . docker compose run --rm openclaw-cli onboard docker compose up -d openclaw-gateway ``` Note: run `docker compose ...` from the repo root. If you enabled `OPENCLAW_EXTRA_MOUNTS` or `OPENCLAW_HOME_VOLUME`, the setup script writes `docker-compose.extra.yml`; include it when running Compose elsewhere: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose -f docker-compose.yml -f docker-compose.extra.yml ``` ### Control UI token + pairing (Docker) If you see “unauthorized” or “disconnected (1008): pairing required”, fetch a fresh dashboard link and approve the browser device: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose run --rm openclaw-cli dashboard --no-open docker compose run --rm openclaw-cli devices list docker compose run --rm openclaw-cli devices approve ``` More detail: [Dashboard](/web/dashboard), [Devices](/cli/devices). ### Extra mounts (optional) If you want to mount additional host directories into the containers, set `OPENCLAW_EXTRA_MOUNTS` before running `docker-setup.sh`. This accepts a comma-separated list of Docker bind mounts and applies them to both `openclaw-gateway` and `openclaw-cli` by generating `docker-compose.extra.yml`. Example: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export OPENCLAW_EXTRA_MOUNTS="$HOME/.codex:/home/node/.codex:ro,$HOME/github:/home/node/github:rw" ./docker-setup.sh ``` Notes: * Paths must be shared with Docker Desktop on macOS/Windows. * Each entry must be `source:target[:options]` with no spaces, tabs, or newlines. * If you edit `OPENCLAW_EXTRA_MOUNTS`, rerun `docker-setup.sh` to regenerate the extra compose file. * `docker-compose.extra.yml` is generated. Don’t hand-edit it. ### Persist the entire container home (optional) If you want `/home/node` to persist across container recreation, set a named volume via `OPENCLAW_HOME_VOLUME`. This creates a Docker volume and mounts it at `/home/node`, while keeping the standard config/workspace bind mounts. Use a named volume here (not a bind path); for bind mounts, use `OPENCLAW_EXTRA_MOUNTS`. Example: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export OPENCLAW_HOME_VOLUME="openclaw_home" ./docker-setup.sh ``` You can combine this with extra mounts: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export OPENCLAW_HOME_VOLUME="openclaw_home" export OPENCLAW_EXTRA_MOUNTS="$HOME/.codex:/home/node/.codex:ro,$HOME/github:/home/node/github:rw" ./docker-setup.sh ``` Notes: * Named volumes must match `^[A-Za-z0-9][A-Za-z0-9_.-]*$`. * If you change `OPENCLAW_HOME_VOLUME`, rerun `docker-setup.sh` to regenerate the extra compose file. * The named volume persists until removed with `docker volume rm `. ### Install extra apt packages (optional) If you need system packages inside the image (for example, build tools or media libraries), set `OPENCLAW_DOCKER_APT_PACKAGES` before running `docker-setup.sh`. This installs the packages during the image build, so they persist even if the container is deleted. Example: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export OPENCLAW_DOCKER_APT_PACKAGES="ffmpeg build-essential" ./docker-setup.sh ``` Notes: * This accepts a space-separated list of apt package names. * If you change `OPENCLAW_DOCKER_APT_PACKAGES`, rerun `docker-setup.sh` to rebuild the image. ### Power-user / full-featured container (opt-in) The default Docker image is **security-first** and runs as the non-root `node` user. This keeps the attack surface small, but it means: * no system package installs at runtime * no Homebrew by default * no bundled Chromium/Playwright browsers If you want a more full-featured container, use these opt-in knobs: 1. **Persist `/home/node`** so browser downloads and tool caches survive: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export OPENCLAW_HOME_VOLUME="openclaw_home" ./docker-setup.sh ``` 2. **Bake system deps into the image** (repeatable + persistent): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export OPENCLAW_DOCKER_APT_PACKAGES="git curl jq" ./docker-setup.sh ``` 3. **Install Playwright browsers without `npx`** (avoids npm override conflicts): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose run --rm openclaw-cli \ node /app/node_modules/playwright-core/cli.js install chromium ``` If you need Playwright to install system deps, rebuild the image with `OPENCLAW_DOCKER_APT_PACKAGES` instead of using `--with-deps` at runtime. 4. **Persist Playwright browser downloads**: * Set `PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright` in `docker-compose.yml`. * Ensure `/home/node` persists via `OPENCLAW_HOME_VOLUME`, or mount `/home/node/.cache/ms-playwright` via `OPENCLAW_EXTRA_MOUNTS`. ### Permissions + EACCES The image runs as `node` (uid 1000). If you see permission errors on `/home/node/.openclaw`, make sure your host bind mounts are owned by uid 1000. Example (Linux host): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} sudo chown -R 1000:1000 /path/to/openclaw-config /path/to/openclaw-workspace ``` If you choose to run as root for convenience, you accept the security tradeoff. ### Faster rebuilds (recommended) To speed up rebuilds, order your Dockerfile so dependency layers are cached. This avoids re-running `pnpm install` unless lockfiles change: ```dockerfile theme={"theme":{"light":"min-light","dark":"min-dark"}} FROM node:22-bookworm # Install Bun (required for build scripts) RUN curl -fsSL https://bun.sh/install | bash ENV PATH="/root/.bun/bin:${PATH}" RUN corepack enable WORKDIR /app # Cache dependencies unless package metadata changes COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ COPY ui/package.json ./ui/package.json COPY scripts ./scripts RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build RUN pnpm ui:install RUN pnpm ui:build ENV NODE_ENV=production CMD ["node","dist/index.js"] ``` ### Channel setup (optional) Use the CLI container to configure channels, then restart the gateway if needed. WhatsApp (QR): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose run --rm openclaw-cli channels login ``` Telegram (bot token): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose run --rm openclaw-cli channels add --channel telegram --token "" ``` Discord (bot token): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose run --rm openclaw-cli channels add --channel discord --token "" ``` Docs: [WhatsApp](/channels/whatsapp), [Telegram](/channels/telegram), [Discord](/channels/discord) ### OpenAI Codex OAuth (headless Docker) If you pick OpenAI Codex OAuth in the wizard, it opens a browser URL and tries to capture a callback on `http://127.0.0.1:1455/auth/callback`. In Docker or headless setups that callback can show a browser error. Copy the full redirect URL you land on and paste it back into the wizard to finish auth. ### Health checks Container probe endpoints (no auth required): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsS http://127.0.0.1:18789/healthz curl -fsS http://127.0.0.1:18789/readyz ``` Aliases: `/health` and `/ready`. The Docker image includes a built-in `HEALTHCHECK` that pings `/healthz` in the background. In plain terms: Docker keeps checking if OpenClaw is still responsive. If checks keep failing, Docker marks the container as `unhealthy`, and orchestration systems (Docker Compose restart policy, Swarm, Kubernetes, etc.) can automatically restart or replace it. Authenticated deep health snapshot (gateway + channels): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose exec openclaw-gateway node dist/index.js health --token "$OPENCLAW_GATEWAY_TOKEN" ``` ### E2E smoke test (Docker) ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} scripts/e2e/onboard-docker.sh ``` ### QR import smoke test (Docker) ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} pnpm test:docker:qr ``` ### LAN vs loopback (Docker Compose) `docker-setup.sh` defaults `OPENCLAW_GATEWAY_BIND=lan` so host access to `http://127.0.0.1:18789` works with Docker port publishing. * `lan` (default): host browser + host CLI can reach the published gateway port. * `loopback`: only processes inside the container network namespace can reach the gateway directly; host-published port access may fail. The setup script also pins `gateway.mode=local` after onboarding so Docker CLI commands default to local loopback targeting. Legacy config note: use bind mode values in `gateway.bind` (`lan` / `loopback` / `custom` / `tailnet` / `auto`), not host aliases (`0.0.0.0`, `127.0.0.1`, `localhost`, `::`, `::1`). If you see `Gateway target: ws://172.x.x.x:18789` or repeated `pairing required` errors from Docker CLI commands, run: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose run --rm openclaw-cli config set gateway.mode local docker compose run --rm openclaw-cli config set gateway.bind lan docker compose run --rm openclaw-cli devices list --url ws://127.0.0.1:18789 ``` ### Notes * Gateway bind defaults to `lan` for container use (`OPENCLAW_GATEWAY_BIND`). * Dockerfile CMD uses `--allow-unconfigured`; mounted config with `gateway.mode` not `local` will still start. Override CMD to enforce the guard. * The gateway container is the source of truth for sessions (`~/.openclaw/agents//sessions/`). ## Agent Sandbox (host gateway + Docker tools) Deep dive: [Sandboxing](/gateway/sandboxing) ### What it does When `agents.defaults.sandbox` is enabled, **non-main sessions** run tools inside a Docker container. The gateway stays on your host, but the tool execution is isolated: * scope: `"agent"` by default (one container + workspace per agent) * scope: `"session"` for per-session isolation * per-scope workspace folder mounted at `/workspace` * optional agent workspace access (`agents.defaults.sandbox.workspaceAccess`) * allow/deny tool policy (deny wins) * inbound media is copied into the active sandbox workspace (`media/inbound/*`) so tools can read it (with `workspaceAccess: "rw"`, this lands in the agent workspace) Warning: `scope: "shared"` disables cross-session isolation. All sessions share one container and one workspace. ### Per-agent sandbox profiles (multi-agent) If you use multi-agent routing, each agent can override sandbox + tool settings: `agents.list[].sandbox` and `agents.list[].tools` (plus `agents.list[].tools.sandbox.tools`). This lets you run mixed access levels in one gateway: * Full access (personal agent) * Read-only tools + read-only workspace (family/work agent) * No filesystem/shell tools (public agent) See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for examples, precedence, and troubleshooting. ### Default behavior * Image: `openclaw-sandbox:bookworm-slim` * One container per agent * Agent workspace access: `workspaceAccess: "none"` (default) uses `~/.openclaw/sandboxes` * `"ro"` keeps the sandbox workspace at `/workspace` and mounts the agent workspace read-only at `/agent` (disables `write`/`edit`/`apply_patch`) * `"rw"` mounts the agent workspace read/write at `/workspace` * Auto-prune: idle > 24h OR age > 7d * Network: `none` by default (explicitly opt-in if you need egress) * `host` is blocked. * `container:` is blocked by default (namespace-join risk). * Default allow: `exec`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` * Default deny: `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway` ### Enable sandboxing If you plan to install packages in `setupCommand`, note: * Default `docker.network` is `"none"` (no egress). * `docker.network: "host"` is blocked. * `docker.network: "container:"` is blocked by default. * Break-glass override: `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin: true`. * `readOnlyRoot: true` blocks package installs. * `user` must be root for `apt-get` (omit `user` or set `user: "0:0"`). OpenClaw auto-recreates containers when `setupCommand` (or docker config) changes unless the container was **recently used** (within \~5 minutes). Hot containers log a warning with the exact `openclaw sandbox recreate ...` command. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { defaults: { sandbox: { mode: "non-main", // off | non-main | all scope: "agent", // session | agent | shared (agent is default) workspaceAccess: "none", // none | ro | rw workspaceRoot: "~/.openclaw/sandboxes", docker: { image: "openclaw-sandbox:bookworm-slim", workdir: "/workspace", readOnlyRoot: true, tmpfs: ["/tmp", "/var/tmp", "/run"], network: "none", user: "1000:1000", capDrop: ["ALL"], env: { LANG: "C.UTF-8" }, setupCommand: "apt-get update && apt-get install -y git curl jq", pidsLimit: 256, memory: "1g", memorySwap: "2g", cpus: 1, ulimits: { nofile: { soft: 1024, hard: 2048 }, nproc: 256, }, seccompProfile: "/path/to/seccomp.json", apparmorProfile: "openclaw-sandbox", dns: ["1.1.1.1", "8.8.8.8"], extraHosts: ["internal.service:10.0.0.5"], }, prune: { idleHours: 24, // 0 disables idle pruning maxAgeDays: 7, // 0 disables max-age pruning }, }, }, }, tools: { sandbox: { tools: { allow: [ "exec", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status", ], deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"], }, }, }, } ``` Hardening knobs live under `agents.defaults.sandbox.docker`: `network`, `user`, `pidsLimit`, `memory`, `memorySwap`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`, `dns`, `extraHosts`, `dangerouslyAllowContainerNamespaceJoin` (break-glass only). Multi-agent: override `agents.defaults.sandbox.{docker,browser,prune}.*` per agent via `agents.list[].sandbox.{docker,browser,prune}.*` (ignored when `agents.defaults.sandbox.scope` / `agents.list[].sandbox.scope` is `"shared"`). ### Build the default sandbox image ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} scripts/sandbox-setup.sh ``` This builds `openclaw-sandbox:bookworm-slim` using `Dockerfile.sandbox`. ### Sandbox common image (optional) If you want a sandbox image with common build tooling (Node, Go, Rust, etc.), build the common image: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} scripts/sandbox-common-setup.sh ``` This builds `openclaw-sandbox-common:bookworm-slim`. To use it: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { defaults: { sandbox: { docker: { image: "openclaw-sandbox-common:bookworm-slim" } }, }, }, } ``` ### Sandbox browser image To run the browser tool inside the sandbox, build the browser image: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} scripts/sandbox-browser-setup.sh ``` This builds `openclaw-sandbox-browser:bookworm-slim` using `Dockerfile.sandbox-browser`. The container runs Chromium with CDP enabled and an optional noVNC observer (headful via Xvfb). Notes: * Headful (Xvfb) reduces bot blocking vs headless. * Headless can still be used by setting `agents.defaults.sandbox.browser.headless=true`. * No full desktop environment (GNOME) is needed; Xvfb provides the display. * Browser containers default to a dedicated Docker network (`openclaw-sandbox-browser`) instead of global `bridge`. * Optional `agents.defaults.sandbox.browser.cdpSourceRange` restricts container-edge CDP ingress by CIDR (for example `172.21.0.1/32`). * noVNC observer access is password-protected by default; OpenClaw provides a short-lived observer token URL that serves a local bootstrap page and keeps the password in URL fragment (instead of URL query). * Browser container startup defaults are conservative for shared/container workloads, including: * `--remote-debugging-address=127.0.0.1` * `--remote-debugging-port=` * `--user-data-dir=${HOME}/.chrome` * `--no-first-run` * `--no-default-browser-check` * `--disable-3d-apis` * `--disable-software-rasterizer` * `--disable-gpu` * `--disable-dev-shm-usage` * `--disable-background-networking` * `--disable-features=TranslateUI` * `--disable-breakpad` * `--disable-crash-reporter` * `--metrics-recording-only` * `--renderer-process-limit=2` * `--no-zygote` * `--disable-extensions` * If `agents.defaults.sandbox.browser.noSandbox` is set, `--no-sandbox` and `--disable-setuid-sandbox` are also appended. * The three graphics hardening flags above are optional. If your workload needs WebGL/3D, set `OPENCLAW_BROWSER_DISABLE_GRAPHICS_FLAGS=0` to run without `--disable-3d-apis`, `--disable-software-rasterizer`, and `--disable-gpu`. * Extension behavior is controlled by `--disable-extensions` and can be disabled (enables extensions) via `OPENCLAW_BROWSER_DISABLE_EXTENSIONS=0` for extension-dependent pages or extensions-heavy workflows. * `--renderer-process-limit=2` is also configurable with `OPENCLAW_BROWSER_RENDERER_PROCESS_LIMIT`; set `0` to let Chromium choose its default process limit when browser concurrency needs tuning. Defaults are applied by default in the bundled image. If you need different Chromium flags, use a custom browser image and provide your own entrypoint. Use config: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { defaults: { sandbox: { browser: { enabled: true }, }, }, }, } ``` Custom browser image: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { defaults: { sandbox: { browser: { image: "my-openclaw-browser" } }, }, }, } ``` When enabled, the agent receives: * a sandbox browser control URL (for the `browser` tool) * a noVNC URL (if enabled and headless=false) Remember: if you use an allowlist for tools, add `browser` (and remove it from deny) or the tool remains blocked. Prune rules (`agents.defaults.sandbox.prune`) apply to browser containers too. ### Custom sandbox image Build your own image and point config to it: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker build -t my-openclaw-sbx -f Dockerfile.sandbox . ``` ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agents: { defaults: { sandbox: { docker: { image: "my-openclaw-sbx" } }, }, }, } ``` ### Tool policy (allow/deny) * `deny` wins over `allow`. * If `allow` is empty: all tools (except deny) are available. * If `allow` is non-empty: only tools in `allow` are available (minus deny). ### Pruning strategy Two knobs: * `prune.idleHours`: remove containers not used in X hours (0 = disable) * `prune.maxAgeDays`: remove containers older than X days (0 = disable) Example: * Keep busy sessions but cap lifetime: `idleHours: 24`, `maxAgeDays: 7` * Never prune: `idleHours: 0`, `maxAgeDays: 0` ### Security notes * Hard wall only applies to **tools** (exec/read/write/edit/apply\_patch). * Host-only tools like browser/camera/canvas are blocked by default. * Allowing `browser` in sandbox **breaks isolation** (browser runs on host). ## Troubleshooting * Image missing: build with [`scripts/sandbox-setup.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/sandbox-setup.sh) or set `agents.defaults.sandbox.docker.image`. * Container not running: it will auto-create per session on demand. * Permission errors in sandbox: set `docker.user` to a UID:GID that matches your mounted workspace ownership (or chown the workspace folder). * Custom tools not found: OpenClaw runs commands with `sh -lc` (login shell), which sources `/etc/profile` and may reset PATH. Set `docker.env.PATH` to prepend your custom tool paths (e.g., `/custom/bin:/usr/local/share/npm-global/bin`), or add a script under `/etc/profile.d/` in your Dockerfile. # exe.dev Source: https://cryptoclawdocs.termix.ai/install/exe-dev # exe.dev Goal: OpenClaw Gateway running on an exe.dev VM, reachable from your laptop via: `https://.exe.xyz` This page assumes exe.dev's default **exeuntu** image. If you picked a different distro, map packages accordingly. ## Beginner quick path 1. [https://exe.new/openclaw](https://exe.new/openclaw) 2. Fill in your auth key/token as needed 3. Click on "Agent" next to your VM, and wait... 4. ??? 5. Profit ## What you need * exe.dev account * `ssh exe.dev` access to [exe.dev](https://exe.dev) virtual machines (optional) ## Automated Install with Shelley Shelley, [exe.dev](https://exe.dev)'s agent, can install OpenClaw instantly with our prompt. The prompt used is as below: ``` Set up OpenClaw (https://docs.openclaw.ai/install) on this VM. Use the non-interactive and accept-risk flags for openclaw onboarding. Add the supplied auth or token as needed. Configure nginx to forward from the default port 18789 to the root location on the default enabled site config, making sure to enable Websocket support. Pairing is done by "openclaw devices list" and "openclaw devices approve ". Make sure the dashboard shows that OpenClaw's health is OK. exe.dev handles forwarding from port 8000 to port 80/443 and HTTPS for us, so the final "reachable" should be .exe.xyz, without port specification. ``` ## Manual installation ## 1) Create the VM From your device: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ssh exe.dev new ``` Then connect: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ssh .exe.xyz ``` Tip: keep this VM **stateful**. OpenClaw stores state under `~/.openclaw/` and `~/.openclaw/workspace/`. ## 2) Install prerequisites (on the VM) ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} sudo apt-get update sudo apt-get install -y git curl jq ca-certificates openssl ``` ## 3) Install OpenClaw Run the OpenClaw install script: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL https://openclaw.ai/install.sh | bash ``` ## 4) Setup nginx to proxy OpenClaw to port 8000 Edit `/etc/nginx/sites-enabled/default` with ``` server { listen 80 default_server; listen [::]:80 default_server; listen 8000; listen [::]:8000; server_name _; location / { proxy_pass http://127.0.0.1:18789; proxy_http_version 1.1; # WebSocket support proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # Standard proxy headers proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # Timeout settings for long-lived connections proxy_read_timeout 86400s; proxy_send_timeout 86400s; } } ``` ## 5) Access OpenClaw and grant privileges Access `https://.exe.xyz/` (see the Control UI output from onboarding). If it prompts for auth, paste the token from `gateway.auth.token` on the VM (retrieve with `openclaw config get gateway.auth.token`, or generate one with `openclaw doctor --generate-gateway-token`). Approve devices with `openclaw devices list` and `openclaw devices approve `. When in doubt, use Shelley from your browser! ## Remote Access Remote access is handled by [exe.dev](https://exe.dev)'s authentication. By default, HTTP traffic from port 8000 is forwarded to `https://.exe.xyz` with email auth. ## Updating ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} npm i -g openclaw@latest openclaw doctor openclaw gateway restart openclaw health ``` Guide: [Updating](/install/updating) # Fly.io Source: https://cryptoclawdocs.termix.ai/install/fly Deploy OpenClaw on Fly.io # Fly.io Deployment **Goal:** OpenClaw Gateway running on a [Fly.io](https://fly.io) machine with persistent storage, automatic HTTPS, and Discord/channel access. ## What you need * [flyctl CLI](https://fly.io/docs/hands-on/install-flyctl/) installed * Fly.io account (free tier works) * Model auth: API key for your chosen model provider * Channel credentials: Discord bot token, Telegram token, etc. ## Beginner quick path 1. Clone repo → customize `fly.toml` 2. Create app + volume → set secrets 3. Deploy with `fly deploy` 4. SSH in to create config or use Control UI ## 1) Create the Fly app ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Clone the repo git clone https://github.com/openclaw/openclaw.git cd openclaw # Create a new Fly app (pick your own name) fly apps create my-openclaw # Create a persistent volume (1GB is usually enough) fly volumes create openclaw_data --size 1 --region iad ``` **Tip:** Choose a region close to you. Common options: `lhr` (London), `iad` (Virginia), `sjc` (San Jose). ## 2) Configure fly.toml Edit `fly.toml` to match your app name and requirements. **Security note:** The default config exposes a public URL. For a hardened deployment with no public IP, see [Private Deployment](#private-deployment-hardened) or use `fly.private.toml`. ```toml theme={"theme":{"light":"min-light","dark":"min-dark"}} app = "my-openclaw" # Your app name primary_region = "iad" [build] dockerfile = "Dockerfile" [env] NODE_ENV = "production" OPENCLAW_PREFER_PNPM = "1" OPENCLAW_STATE_DIR = "/data" NODE_OPTIONS = "--max-old-space-size=1536" [processes] app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan" [http_service] internal_port = 3000 force_https = true auto_stop_machines = false auto_start_machines = true min_machines_running = 1 processes = ["app"] [[vm]] size = "shared-cpu-2x" memory = "2048mb" [mounts] source = "openclaw_data" destination = "/data" ``` **Key settings:** | Setting | Why | | ------------------------------ | --------------------------------------------------------------------------- | | `--bind lan` | Binds to `0.0.0.0` so Fly's proxy can reach the gateway | | `--allow-unconfigured` | Starts without a config file (you'll create one after) | | `internal_port = 3000` | Must match `--port 3000` (or `OPENCLAW_GATEWAY_PORT`) for Fly health checks | | `memory = "2048mb"` | 512MB is too small; 2GB recommended | | `OPENCLAW_STATE_DIR = "/data"` | Persists state on the volume | ## 3) Set secrets ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Required: Gateway token (for non-loopback binding) fly secrets set OPENCLAW_GATEWAY_TOKEN=$(openssl rand -hex 32) # Model provider API keys fly secrets set ANTHROPIC_API_KEY=sk-ant-... # Optional: Other providers fly secrets set OPENAI_API_KEY=sk-... fly secrets set GOOGLE_API_KEY=... # Channel tokens fly secrets set DISCORD_BOT_TOKEN=MTQ... ``` **Notes:** * Non-loopback binds (`--bind lan`) require `OPENCLAW_GATEWAY_TOKEN` for security. * Treat these tokens like passwords. * **Prefer env vars over config file** for all API keys and tokens. This keeps secrets out of `openclaw.json` where they could be accidentally exposed or logged. ## 4) Deploy ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} fly deploy ``` First deploy builds the Docker image (\~2-3 minutes). Subsequent deploys are faster. After deployment, verify: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} fly status fly logs ``` You should see: ``` [gateway] listening on ws://0.0.0.0:3000 (PID xxx) [discord] logged in to discord as xxx ``` ## 5) Create config file SSH into the machine to create a proper config: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} fly ssh console ``` Create the config directory and file: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} mkdir -p /data cat > /data/openclaw.json << 'EOF' { "agents": { "defaults": { "model": { "primary": "anthropic/claude-opus-4-6", "fallbacks": ["anthropic/claude-sonnet-4-5", "openai/gpt-4o"] }, "maxConcurrent": 4 }, "list": [ { "id": "main", "default": true } ] }, "auth": { "profiles": { "anthropic:default": { "mode": "token", "provider": "anthropic" }, "openai:default": { "mode": "token", "provider": "openai" } } }, "bindings": [ { "agentId": "main", "match": { "channel": "discord" } } ], "channels": { "discord": { "enabled": true, "groupPolicy": "allowlist", "guilds": { "YOUR_GUILD_ID": { "channels": { "general": { "allow": true } }, "requireMention": false } } } }, "gateway": { "mode": "local", "bind": "auto" }, "meta": { "lastTouchedVersion": "2026.1.29" } } EOF ``` **Note:** With `OPENCLAW_STATE_DIR=/data`, the config path is `/data/openclaw.json`. **Note:** The Discord token can come from either: * Environment variable: `DISCORD_BOT_TOKEN` (recommended for secrets) * Config file: `channels.discord.token` If using env var, no need to add token to config. The gateway reads `DISCORD_BOT_TOKEN` automatically. Restart to apply: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} exit fly machine restart ``` ## 6) Access the Gateway ### Control UI Open in browser: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} fly open ``` Or visit `https://my-openclaw.fly.dev/` Paste your gateway token (the one from `OPENCLAW_GATEWAY_TOKEN`) to authenticate. ### Logs ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} fly logs # Live logs fly logs --no-tail # Recent logs ``` ### SSH Console ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} fly ssh console ``` ## Troubleshooting ### "App is not listening on expected address" The gateway is binding to `127.0.0.1` instead of `0.0.0.0`. **Fix:** Add `--bind lan` to your process command in `fly.toml`. ### Health checks failing / connection refused Fly can't reach the gateway on the configured port. **Fix:** Ensure `internal_port` matches the gateway port (set `--port 3000` or `OPENCLAW_GATEWAY_PORT=3000`). ### OOM / Memory Issues Container keeps restarting or getting killed. Signs: `SIGABRT`, `v8::internal::Runtime_AllocateInYoungGeneration`, or silent restarts. **Fix:** Increase memory in `fly.toml`: ```toml theme={"theme":{"light":"min-light","dark":"min-dark"}} [[vm]] memory = "2048mb" ``` Or update an existing machine: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} fly machine update --vm-memory 2048 -y ``` **Note:** 512MB is too small. 1GB may work but can OOM under load or with verbose logging. **2GB is recommended.** ### Gateway Lock Issues Gateway refuses to start with "already running" errors. This happens when the container restarts but the PID lock file persists on the volume. **Fix:** Delete the lock file: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} fly ssh console --command "rm -f /data/gateway.*.lock" fly machine restart ``` The lock file is at `/data/gateway.*.lock` (not in a subdirectory). ### Config Not Being Read If using `--allow-unconfigured`, the gateway creates a minimal config. Your custom config at `/data/openclaw.json` should be read on restart. Verify the config exists: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} fly ssh console --command "cat /data/openclaw.json" ``` ### Writing Config via SSH The `fly ssh console -C` command doesn't support shell redirection. To write a config file: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Use echo + tee (pipe from local to remote) echo '{"your":"config"}' | fly ssh console -C "tee /data/openclaw.json" # Or use sftp fly sftp shell > put /local/path/config.json /data/openclaw.json ``` **Note:** `fly sftp` may fail if the file already exists. Delete first: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} fly ssh console --command "rm /data/openclaw.json" ``` ### State Not Persisting If you lose credentials or sessions after a restart, the state dir is writing to the container filesystem. **Fix:** Ensure `OPENCLAW_STATE_DIR=/data` is set in `fly.toml` and redeploy. ## Updates ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Pull latest changes git pull # Redeploy fly deploy # Check health fly status fly logs ``` ### Updating Machine Command If you need to change the startup command without a full redeploy: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Get machine ID fly machines list # Update command fly machine update --command "node dist/index.js gateway --port 3000 --bind lan" -y # Or with memory increase fly machine update --vm-memory 2048 --command "node dist/index.js gateway --port 3000 --bind lan" -y ``` **Note:** After `fly deploy`, the machine command may reset to what's in `fly.toml`. If you made manual changes, re-apply them after deploy. ## Private Deployment (Hardened) By default, Fly allocates public IPs, making your gateway accessible at `https://your-app.fly.dev`. This is convenient but means your deployment is discoverable by internet scanners (Shodan, Censys, etc.). For a hardened deployment with **no public exposure**, use the private template. ### When to use private deployment * You only make **outbound** calls/messages (no inbound webhooks) * You use **ngrok or Tailscale** tunnels for any webhook callbacks * You access the gateway via **SSH, proxy, or WireGuard** instead of browser * You want the deployment **hidden from internet scanners** ### Setup Use `fly.private.toml` instead of the standard config: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Deploy with private config fly deploy -c fly.private.toml ``` Or convert an existing deployment: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # List current IPs fly ips list -a my-openclaw # Release public IPs fly ips release -a my-openclaw fly ips release -a my-openclaw # Switch to private config so future deploys don't re-allocate public IPs # (remove [http_service] or deploy with the private template) fly deploy -c fly.private.toml # Allocate private-only IPv6 fly ips allocate-v6 --private -a my-openclaw ``` After this, `fly ips list` should show only a `private` type IP: ``` VERSION IP TYPE REGION v6 fdaa:x:x:x:x::x private global ``` ### Accessing a private deployment Since there's no public URL, use one of these methods: **Option 1: Local proxy (simplest)** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Forward local port 3000 to the app fly proxy 3000:3000 -a my-openclaw # Then open http://localhost:3000 in browser ``` **Option 2: WireGuard VPN** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Create WireGuard config (one-time) fly wireguard create # Import to WireGuard client, then access via internal IPv6 # Example: http://[fdaa:x:x:x:x::x]:3000 ``` **Option 3: SSH only** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} fly ssh console -a my-openclaw ``` ### Webhooks with private deployment If you need webhook callbacks (Twilio, Telnyx, etc.) without public exposure: 1. **ngrok tunnel** - Run ngrok inside the container or as a sidecar 2. **Tailscale Funnel** - Expose specific paths via Tailscale 3. **Outbound-only** - Some providers (Twilio) work fine for outbound calls without webhooks Example voice-call config with ngrok: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "plugins": { "entries": { "voice-call": { "enabled": true, "config": { "provider": "twilio", "tunnel": { "provider": "ngrok" }, "webhookSecurity": { "allowedHosts": ["example.ngrok.app"] } } } } } } ``` The ngrok tunnel runs inside the container and provides a public webhook URL without exposing the Fly app itself. Set `webhookSecurity.allowedHosts` to the public tunnel hostname so forwarded host headers are accepted. ### Security benefits | Aspect | Public | Private | | ----------------- | ------------ | ---------- | | Internet scanners | Discoverable | Hidden | | Direct attacks | Possible | Blocked | | Control UI access | Browser | Proxy/VPN | | Webhook delivery | Direct | Via tunnel | ## Notes * Fly.io uses **x86 architecture** (not ARM) * The Dockerfile is compatible with both architectures * For WhatsApp/Telegram onboarding, use `fly ssh console` * Persistent data lives on the volume at `/data` * Signal requires Java + signal-cli; use a custom image and keep memory at 2GB+. ## Cost With the recommended config (`shared-cpu-2x`, 2GB RAM): * \~\$10-15/month depending on usage * Free tier includes some allowance See [Fly.io pricing](https://fly.io/docs/about/pricing/) for details. # GCP Source: https://cryptoclawdocs.termix.ai/install/gcp # OpenClaw on GCP Compute Engine (Docker, Production VPS Guide) ## Goal Run a persistent OpenClaw Gateway on a GCP Compute Engine VM using Docker, with durable state, baked-in binaries, and safe restart behavior. If you want "OpenClaw 24/7 for \~\$5-12/mo", this is a reliable setup on Google Cloud. Pricing varies by machine type and region; pick the smallest VM that fits your workload and scale up if you hit OOMs. ## What are we doing (simple terms)? * Create a GCP project and enable billing * Create a Compute Engine VM * Install Docker (isolated app runtime) * Start the OpenClaw Gateway in Docker * Persist `~/.openclaw` + `~/.openclaw/workspace` on the host (survives restarts/rebuilds) * Access the Control UI from your laptop via an SSH tunnel The Gateway can be accessed via: * SSH port forwarding from your laptop * Direct port exposure if you manage firewalling and tokens yourself This guide uses Debian on GCP Compute Engine. Ubuntu also works; map packages accordingly. For the generic Docker flow, see [Docker](/install/docker). *** ## Quick path (experienced operators) 1. Create GCP project + enable Compute Engine API 2. Create Compute Engine VM (e2-small, Debian 12, 20GB) 3. SSH into the VM 4. Install Docker 5. Clone OpenClaw repository 6. Create persistent host directories 7. Configure `.env` and `docker-compose.yml` 8. Bake required binaries, build, and launch *** ## What you need * GCP account (free tier eligible for e2-micro) * gcloud CLI installed (or use Cloud Console) * SSH access from your laptop * Basic comfort with SSH + copy/paste * \~20-30 minutes * Docker and Docker Compose * Model auth credentials * Optional provider credentials * WhatsApp QR * Telegram bot token * Gmail OAuth *** ## 1) Install gcloud CLI (or use Console) **Option A: gcloud CLI** (recommended for automation) Install from [https://cloud.google.com/sdk/docs/install](https://cloud.google.com/sdk/docs/install) Initialize and authenticate: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} gcloud init gcloud auth login ``` **Option B: Cloud Console** All steps can be done via the web UI at [https://console.cloud.google.com](https://console.cloud.google.com) *** ## 2) Create a GCP project **CLI:** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} gcloud projects create my-openclaw-project --name="OpenClaw Gateway" gcloud config set project my-openclaw-project ``` Enable billing at [https://console.cloud.google.com/billing](https://console.cloud.google.com/billing) (required for Compute Engine). Enable the Compute Engine API: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} gcloud services enable compute.googleapis.com ``` **Console:** 1. Go to IAM & Admin > Create Project 2. Name it and create 3. Enable billing for the project 4. Navigate to APIs & Services > Enable APIs > search "Compute Engine API" > Enable *** ## 3) Create the VM **Machine types:** | Type | Specs | Cost | Notes | | --------- | ------------------------ | ------------------ | -------------------------------------------- | | e2-medium | 2 vCPU, 4GB RAM | \~\$25/mo | Most reliable for local Docker builds | | e2-small | 2 vCPU, 2GB RAM | \~\$12/mo | Minimum recommended for Docker build | | e2-micro | 2 vCPU (shared), 1GB RAM | Free tier eligible | Often fails with Docker build OOM (exit 137) | **CLI:** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} gcloud compute instances create openclaw-gateway \ --zone=us-central1-a \ --machine-type=e2-small \ --boot-disk-size=20GB \ --image-family=debian-12 \ --image-project=debian-cloud ``` **Console:** 1. Go to Compute Engine > VM instances > Create instance 2. Name: `openclaw-gateway` 3. Region: `us-central1`, Zone: `us-central1-a` 4. Machine type: `e2-small` 5. Boot disk: Debian 12, 20GB 6. Create *** ## 4) SSH into the VM **CLI:** ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} gcloud compute ssh openclaw-gateway --zone=us-central1-a ``` **Console:** Click the "SSH" button next to your VM in the Compute Engine dashboard. Note: SSH key propagation can take 1-2 minutes after VM creation. If connection is refused, wait and retry. *** ## 5) Install Docker (on the VM) ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} sudo apt-get update sudo apt-get install -y git curl ca-certificates curl -fsSL https://get.docker.com | sudo sh sudo usermod -aG docker $USER ``` Log out and back in for the group change to take effect: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} exit ``` Then SSH back in: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} gcloud compute ssh openclaw-gateway --zone=us-central1-a ``` Verify: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker --version docker compose version ``` *** ## 6) Clone the OpenClaw repository ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} git clone https://github.com/openclaw/openclaw.git cd openclaw ``` This guide assumes you will build a custom image to guarantee binary persistence. *** ## 7) Create persistent host directories Docker containers are ephemeral. All long-lived state must live on the host. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} mkdir -p ~/.openclaw mkdir -p ~/.openclaw/workspace ``` *** ## 8) Configure environment variables Create `.env` in the repository root. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} OPENCLAW_IMAGE=openclaw:latest OPENCLAW_GATEWAY_TOKEN=change-me-now OPENCLAW_GATEWAY_BIND=lan OPENCLAW_GATEWAY_PORT=18789 OPENCLAW_CONFIG_DIR=/home/$USER/.openclaw OPENCLAW_WORKSPACE_DIR=/home/$USER/.openclaw/workspace GOG_KEYRING_PASSWORD=change-me-now XDG_CONFIG_HOME=/home/node/.openclaw ``` Generate strong secrets: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openssl rand -hex 32 ``` **Do not commit this file.** *** ## 9) Docker Compose configuration Create or update `docker-compose.yml`. ```yaml theme={"theme":{"light":"min-light","dark":"min-dark"}} services: openclaw-gateway: image: ${OPENCLAW_IMAGE} build: . restart: unless-stopped env_file: - .env environment: - HOME=/home/node - NODE_ENV=production - TERM=xterm-256color - OPENCLAW_GATEWAY_BIND=${OPENCLAW_GATEWAY_BIND} - OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT} - OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN} - GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD} - XDG_CONFIG_HOME=${XDG_CONFIG_HOME} - PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin volumes: - ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw - ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace ports: # Recommended: keep the Gateway loopback-only on the VM; access via SSH tunnel. # To expose it publicly, remove the `127.0.0.1:` prefix and firewall accordingly. - "127.0.0.1:${OPENCLAW_GATEWAY_PORT}:18789" command: [ "node", "dist/index.js", "gateway", "--bind", "${OPENCLAW_GATEWAY_BIND}", "--port", "${OPENCLAW_GATEWAY_PORT}", ] ``` *** ## 10) Bake required binaries into the image (critical) Installing binaries inside a running container is a trap. Anything installed at runtime will be lost on restart. All external binaries required by skills must be installed at image build time. The examples below show three common binaries only: * `gog` for Gmail access * `goplaces` for Google Places * `wacli` for WhatsApp These are examples, not a complete list. You may install as many binaries as needed using the same pattern. If you add new skills later that depend on additional binaries, you must: 1. Update the Dockerfile 2. Rebuild the image 3. Restart the containers **Example Dockerfile** ```dockerfile theme={"theme":{"light":"min-light","dark":"min-dark"}} FROM node:22-bookworm RUN apt-get update && apt-get install -y socat && rm -rf /var/lib/apt/lists/* # Example binary 1: Gmail CLI RUN curl -L https://github.com/steipete/gog/releases/latest/download/gog_Linux_x86_64.tar.gz \ | tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/gog # Example binary 2: Google Places CLI RUN curl -L https://github.com/steipete/goplaces/releases/latest/download/goplaces_Linux_x86_64.tar.gz \ | tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/goplaces # Example binary 3: WhatsApp CLI RUN curl -L https://github.com/steipete/wacli/releases/latest/download/wacli_Linux_x86_64.tar.gz \ | tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/wacli # Add more binaries below using the same pattern WORKDIR /app COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ COPY ui/package.json ./ui/package.json COPY scripts ./scripts RUN corepack enable RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build RUN pnpm ui:install RUN pnpm ui:build ENV NODE_ENV=production CMD ["node","dist/index.js"] ``` *** ## 11) Build and launch ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose build docker compose up -d openclaw-gateway ``` If build fails with `Killed` / `exit code 137` during `pnpm install --frozen-lockfile`, the VM is out of memory. Use `e2-small` minimum, or `e2-medium` for more reliable first builds. When binding to LAN (`OPENCLAW_GATEWAY_BIND=lan`), configure a trusted browser origin before continuing: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose run --rm openclaw-cli config set gateway.controlUi.allowedOrigins '["http://127.0.0.1:18789"]' --strict-json ``` If you changed the gateway port, replace `18789` with your configured port. Verify binaries: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose exec openclaw-gateway which gog docker compose exec openclaw-gateway which goplaces docker compose exec openclaw-gateway which wacli ``` Expected output: ``` /usr/local/bin/gog /usr/local/bin/goplaces /usr/local/bin/wacli ``` *** ## 12) Verify Gateway ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose logs -f openclaw-gateway ``` Success: ``` [gateway] listening on ws://0.0.0.0:18789 ``` *** ## 13) Access from your laptop Create an SSH tunnel to forward the Gateway port: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} gcloud compute ssh openclaw-gateway --zone=us-central1-a -- -L 18789:127.0.0.1:18789 ``` Open in your browser: `http://127.0.0.1:18789/` Fetch a fresh tokenized dashboard link: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose run --rm openclaw-cli dashboard --no-open ``` Paste the token from that URL. If Control UI shows `unauthorized` or `disconnected (1008): pairing required`, approve the browser device: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose run --rm openclaw-cli devices list docker compose run --rm openclaw-cli devices approve ``` *** ## What persists where (source of truth) OpenClaw runs in Docker, but Docker is not the source of truth. All long-lived state must survive restarts, rebuilds, and reboots. | Component | Location | Persistence mechanism | Notes | | ------------------- | --------------------------------- | ---------------------- | -------------------------------- | | Gateway config | `/home/node/.openclaw/` | Host volume mount | Includes `openclaw.json`, tokens | | Model auth profiles | `/home/node/.openclaw/` | Host volume mount | OAuth tokens, API keys | | Skill configs | `/home/node/.openclaw/skills/` | Host volume mount | Skill-level state | | Agent workspace | `/home/node/.openclaw/workspace/` | Host volume mount | Code and agent artifacts | | WhatsApp session | `/home/node/.openclaw/` | Host volume mount | Preserves QR login | | Gmail keyring | `/home/node/.openclaw/` | Host volume + password | Requires `GOG_KEYRING_PASSWORD` | | External binaries | `/usr/local/bin/` | Docker image | Must be baked at build time | | Node runtime | Container filesystem | Docker image | Rebuilt every image build | | OS packages | Container filesystem | Docker image | Do not install at runtime | | Docker container | Ephemeral | Restartable | Safe to destroy | *** ## Updates To update OpenClaw on the VM: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} cd ~/openclaw git pull docker compose build docker compose up -d ``` *** ## Troubleshooting **SSH connection refused** SSH key propagation can take 1-2 minutes after VM creation. Wait and retry. **OS Login issues** Check your OS Login profile: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} gcloud compute os-login describe-profile ``` Ensure your account has the required IAM permissions (Compute OS Login or Compute OS Admin Login). **Out of memory (OOM)** If Docker build fails with `Killed` and `exit code 137`, the VM was OOM-killed. Upgrade to e2-small (minimum) or e2-medium (recommended for reliable local builds): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Stop the VM first gcloud compute instances stop openclaw-gateway --zone=us-central1-a # Change machine type gcloud compute instances set-machine-type openclaw-gateway \ --zone=us-central1-a \ --machine-type=e2-small # Start the VM gcloud compute instances start openclaw-gateway --zone=us-central1-a ``` *** ## Service accounts (security best practice) For personal use, your default user account works fine. For automation or CI/CD pipelines, create a dedicated service account with minimal permissions: 1. Create a service account: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} gcloud iam service-accounts create openclaw-deploy \ --display-name="OpenClaw Deployment" ``` 2. Grant Compute Instance Admin role (or narrower custom role): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} gcloud projects add-iam-policy-binding my-openclaw-project \ --member="serviceAccount:openclaw-deploy@my-openclaw-project.iam.gserviceaccount.com" \ --role="roles/compute.instanceAdmin.v1" ``` Avoid using the Owner role for automation. Use the principle of least privilege. See [https://cloud.google.com/iam/docs/understanding-roles](https://cloud.google.com/iam/docs/understanding-roles) for IAM role details. *** ## Next steps * Set up messaging channels: [Channels](/channels) * Pair local devices as nodes: [Nodes](/nodes) * Configure the Gateway: [Gateway configuration](/gateway/configuration) # Hetzner Source: https://cryptoclawdocs.termix.ai/install/hetzner # OpenClaw on Hetzner (Docker, Production VPS Guide) ## Goal Run a persistent OpenClaw Gateway on a Hetzner VPS using Docker, with durable state, baked-in binaries, and safe restart behavior. If you want “OpenClaw 24/7 for \~\$5”, this is the simplest reliable setup. Hetzner pricing changes; pick the smallest Debian/Ubuntu VPS and scale up if you hit OOMs. Security model reminder: * Company-shared agents are fine when everyone is in the same trust boundary and the runtime is business-only. * Keep strict separation: dedicated VPS/runtime + dedicated accounts; no personal Apple/Google/browser/password-manager profiles on that host. * If users are adversarial to each other, split by gateway/host/OS user. See [Security](/gateway/security) and [VPS hosting](/vps). ## What are we doing (simple terms)? * Rent a small Linux server (Hetzner VPS) * Install Docker (isolated app runtime) * Start the OpenClaw Gateway in Docker * Persist `~/.openclaw` + `~/.openclaw/workspace` on the host (survives restarts/rebuilds) * Access the Control UI from your laptop via an SSH tunnel The Gateway can be accessed via: * SSH port forwarding from your laptop * Direct port exposure if you manage firewalling and tokens yourself This guide assumes Ubuntu or Debian on Hetzner.\ If you are on another Linux VPS, map packages accordingly. For the generic Docker flow, see [Docker](/install/docker). *** ## Quick path (experienced operators) 1. Provision Hetzner VPS 2. Install Docker 3. Clone OpenClaw repository 4. Create persistent host directories 5. Configure `.env` and `docker-compose.yml` 6. Bake required binaries into the image 7. `docker compose up -d` 8. Verify persistence and Gateway access *** ## What you need * Hetzner VPS with root access * SSH access from your laptop * Basic comfort with SSH + copy/paste * \~20 minutes * Docker and Docker Compose * Model auth credentials * Optional provider credentials * WhatsApp QR * Telegram bot token * Gmail OAuth *** ## 1) Provision the VPS Create an Ubuntu or Debian VPS in Hetzner. Connect as root: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ssh root@YOUR_VPS_IP ``` This guide assumes the VPS is stateful. Do not treat it as disposable infrastructure. *** ## 2) Install Docker (on the VPS) ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} apt-get update apt-get install -y git curl ca-certificates curl -fsSL https://get.docker.com | sh ``` Verify: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker --version docker compose version ``` *** ## 3) Clone the OpenClaw repository ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} git clone https://github.com/openclaw/openclaw.git cd openclaw ``` This guide assumes you will build a custom image to guarantee binary persistence. *** ## 4) Create persistent host directories Docker containers are ephemeral. All long-lived state must live on the host. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} mkdir -p /root/.openclaw/workspace # Set ownership to the container user (uid 1000): chown -R 1000:1000 /root/.openclaw ``` *** ## 5) Configure environment variables Create `.env` in the repository root. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} OPENCLAW_IMAGE=openclaw:latest OPENCLAW_GATEWAY_TOKEN=change-me-now OPENCLAW_GATEWAY_BIND=lan OPENCLAW_GATEWAY_PORT=18789 OPENCLAW_CONFIG_DIR=/root/.openclaw OPENCLAW_WORKSPACE_DIR=/root/.openclaw/workspace GOG_KEYRING_PASSWORD=change-me-now XDG_CONFIG_HOME=/home/node/.openclaw ``` Generate strong secrets: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openssl rand -hex 32 ``` **Do not commit this file.** *** ## 6) Docker Compose configuration Create or update `docker-compose.yml`. ```yaml theme={"theme":{"light":"min-light","dark":"min-dark"}} services: openclaw-gateway: image: ${OPENCLAW_IMAGE} build: . restart: unless-stopped env_file: - .env environment: - HOME=/home/node - NODE_ENV=production - TERM=xterm-256color - OPENCLAW_GATEWAY_BIND=${OPENCLAW_GATEWAY_BIND} - OPENCLAW_GATEWAY_PORT=${OPENCLAW_GATEWAY_PORT} - OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN} - GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD} - XDG_CONFIG_HOME=${XDG_CONFIG_HOME} - PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin volumes: - ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw - ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace ports: # Recommended: keep the Gateway loopback-only on the VPS; access via SSH tunnel. # To expose it publicly, remove the `127.0.0.1:` prefix and firewall accordingly. - "127.0.0.1:${OPENCLAW_GATEWAY_PORT}:18789" command: [ "node", "dist/index.js", "gateway", "--bind", "${OPENCLAW_GATEWAY_BIND}", "--port", "${OPENCLAW_GATEWAY_PORT}", "--allow-unconfigured", ] ``` `--allow-unconfigured` is only for bootstrap convenience, it is not a replacement for a proper gateway configuration. Still set auth (`gateway.auth.token` or password) and use safe bind settings for your deployment. *** ## 7) Bake required binaries into the image (critical) Installing binaries inside a running container is a trap. Anything installed at runtime will be lost on restart. All external binaries required by skills must be installed at image build time. The examples below show three common binaries only: * `gog` for Gmail access * `goplaces` for Google Places * `wacli` for WhatsApp These are examples, not a complete list. You may install as many binaries as needed using the same pattern. If you add new skills later that depend on additional binaries, you must: 1. Update the Dockerfile 2. Rebuild the image 3. Restart the containers **Example Dockerfile** ```dockerfile theme={"theme":{"light":"min-light","dark":"min-dark"}} FROM node:22-bookworm RUN apt-get update && apt-get install -y socat && rm -rf /var/lib/apt/lists/* # Example binary 1: Gmail CLI RUN curl -L https://github.com/steipete/gog/releases/latest/download/gog_Linux_x86_64.tar.gz \ | tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/gog # Example binary 2: Google Places CLI RUN curl -L https://github.com/steipete/goplaces/releases/latest/download/goplaces_Linux_x86_64.tar.gz \ | tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/goplaces # Example binary 3: WhatsApp CLI RUN curl -L https://github.com/steipete/wacli/releases/latest/download/wacli_Linux_x86_64.tar.gz \ | tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/wacli # Add more binaries below using the same pattern WORKDIR /app COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ COPY ui/package.json ./ui/package.json COPY scripts ./scripts RUN corepack enable RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build RUN pnpm ui:install RUN pnpm ui:build ENV NODE_ENV=production CMD ["node","dist/index.js"] ``` *** ## 8) Build and launch ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose build docker compose up -d openclaw-gateway ``` Verify binaries: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose exec openclaw-gateway which gog docker compose exec openclaw-gateway which goplaces docker compose exec openclaw-gateway which wacli ``` Expected output: ``` /usr/local/bin/gog /usr/local/bin/goplaces /usr/local/bin/wacli ``` *** ## 9) Verify Gateway ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} docker compose logs -f openclaw-gateway ``` Success: ``` [gateway] listening on ws://0.0.0.0:18789 ``` From your laptop: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ssh -N -L 18789:127.0.0.1:18789 root@YOUR_VPS_IP ``` Open: `http://127.0.0.1:18789/` Paste your gateway token. *** ## What persists where (source of truth) OpenClaw runs in Docker, but Docker is not the source of truth. All long-lived state must survive restarts, rebuilds, and reboots. | Component | Location | Persistence mechanism | Notes | | ------------------- | --------------------------------- | ---------------------- | -------------------------------- | | Gateway config | `/home/node/.openclaw/` | Host volume mount | Includes `openclaw.json`, tokens | | Model auth profiles | `/home/node/.openclaw/` | Host volume mount | OAuth tokens, API keys | | Skill configs | `/home/node/.openclaw/skills/` | Host volume mount | Skill-level state | | Agent workspace | `/home/node/.openclaw/workspace/` | Host volume mount | Code and agent artifacts | | WhatsApp session | `/home/node/.openclaw/` | Host volume mount | Preserves QR login | | Gmail keyring | `/home/node/.openclaw/` | Host volume + password | Requires `GOG_KEYRING_PASSWORD` | | External binaries | `/usr/local/bin/` | Docker image | Must be baked at build time | | Node runtime | Container filesystem | Docker image | Rebuilt every image build | | OS packages | Container filesystem | Docker image | Do not install at runtime | | Docker container | Ephemeral | Restartable | Safe to destroy | *** ## Infrastructure as Code (Terraform) For teams preferring infrastructure-as-code workflows, a community-maintained Terraform setup provides: * Modular Terraform configuration with remote state management * Automated provisioning via cloud-init * Deployment scripts (bootstrap, deploy, backup/restore) * Security hardening (firewall, UFW, SSH-only access) * SSH tunnel configuration for gateway access **Repositories:** * Infrastructure: [openclaw-terraform-hetzner](https://github.com/andreesg/openclaw-terraform-hetzner) * Docker config: [openclaw-docker-config](https://github.com/andreesg/openclaw-docker-config) This approach complements the Docker setup above with reproducible deployments, version-controlled infrastructure, and automated disaster recovery. > **Note:** Community-maintained. For issues or contributions, see the repository links above. # Install Source: https://cryptoclawdocs.termix.ai/install/index # Install Already followed [Getting Started](/start/getting-started)? You're all set — this page is for alternative install methods, platform-specific instructions, and maintenance. ## System requirements * **[Node 22+](/install/node)** (the [installer script](#install-methods) will install it if missing) * macOS, Linux, or Windows * `pnpm` only if you build from source On Windows, we strongly recommend running OpenClaw under [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install). ## Install methods The **installer script** is the recommended way to install OpenClaw. It handles Node detection, installation, and onboarding in one step. For VPS/cloud hosts, avoid third-party "1-click" marketplace images when possible. Prefer a clean base OS image (for example Ubuntu LTS), then install OpenClaw yourself with the installer script. Downloads the CLI, installs it globally via npm, and launches the onboarding wizard. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL https://openclaw.ai/install.sh | bash ``` ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} iwr -useb https://openclaw.ai/install.ps1 | iex ``` That's it — the script handles Node detection, installation, and onboarding. To skip onboarding and just install the binary: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard ``` ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} & ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -NoOnboard ``` For all flags, env vars, and CI/automation options, see [Installer internals](/install/installer). If you already have Node 22+ and prefer to manage the install yourself: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} npm install -g openclaw@latest openclaw onboard --install-daemon ``` If you have libvips installed globally (common on macOS via Homebrew) and `sharp` fails, force prebuilt binaries: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install -g openclaw@latest ``` If you see `sharp: Please add node-gyp to your dependencies`, either install build tooling (macOS: Xcode CLT + `npm install -g node-gyp`) or use the env var above. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} pnpm add -g openclaw@latest pnpm approve-builds -g # approve openclaw, node-llama-cpp, sharp, etc. openclaw onboard --install-daemon ``` pnpm requires explicit approval for packages with build scripts. After the first install shows the "Ignored build scripts" warning, run `pnpm approve-builds -g` and select the listed packages. For contributors or anyone who wants to run from a local checkout. Clone the [OpenClaw repo](https://github.com/openclaw/openclaw) and build: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} git clone https://github.com/openclaw/openclaw.git cd openclaw pnpm install pnpm ui:build pnpm build ``` Make the `openclaw` command available globally: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} pnpm link --global ``` Alternatively, skip the link and run commands via `pnpm openclaw ...` from inside the repo. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw onboard --install-daemon ``` For deeper development workflows, see [Setup](/start/setup). ## Other install methods Containerized or headless deployments. Rootless container: run `setup-podman.sh` once, then the launch script. Declarative install via Nix. Automated fleet provisioning. CLI-only usage via the Bun runtime. ## After install Verify everything is working: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw doctor # check for config issues openclaw status # gateway status openclaw dashboard # open the browser UI ``` If you need custom runtime paths, use: * `OPENCLAW_HOME` for home-directory based internal paths * `OPENCLAW_STATE_DIR` for mutable state location * `OPENCLAW_CONFIG_PATH` for config file location See [Environment vars](/help/environment) for precedence and full details. ## Troubleshooting: `openclaw` not found Quick diagnosis: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} node -v npm -v npm prefix -g echo "$PATH" ``` If `$(npm prefix -g)/bin` (macOS/Linux) or `$(npm prefix -g)` (Windows) is **not** in your `$PATH`, your shell can't find global npm binaries (including `openclaw`). Fix — add it to your shell startup file (`~/.zshrc` or `~/.bashrc`): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} export PATH="$(npm prefix -g)/bin:$PATH" ``` On Windows, add the output of `npm prefix -g` to your PATH. Then open a new terminal (or `rehash` in zsh / `hash -r` in bash). ## Update / uninstall Keep OpenClaw up to date. Move to a new machine. Remove OpenClaw completely. # Installer Internals Source: https://cryptoclawdocs.termix.ai/install/installer # Installer internals OpenClaw ships three installer scripts, served from `openclaw.ai`. | Script | Platform | What it does | | ---------------------------------- | -------------------- | -------------------------------------------------------------------------------------------- | | [`install.sh`](#installsh) | macOS / Linux / WSL | Installs Node if needed, installs OpenClaw via npm (default) or git, and can run onboarding. | | [`install-cli.sh`](#install-clish) | macOS / Linux / WSL | Installs Node + OpenClaw into a local prefix (`~/.openclaw`). No root required. | | [`install.ps1`](#installps1) | Windows (PowerShell) | Installs Node if needed, installs OpenClaw via npm (default) or git, and can run onboarding. | ## Quick commands ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --help ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install-cli.sh | bash ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install-cli.sh | bash -s -- --help ``` ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} iwr -useb https://openclaw.ai/install.ps1 | iex ``` ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} & ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -Tag beta -NoOnboard -DryRun ``` If install succeeds but `openclaw` is not found in a new terminal, see [Node.js troubleshooting](/install/node#troubleshooting). *** ## install.sh Recommended for most interactive installs on macOS/Linux/WSL. ### Flow (install.sh) Supports macOS and Linux (including WSL). If macOS is detected, installs Homebrew if missing. Checks Node version and installs Node 22 if needed (Homebrew on macOS, NodeSource setup scripts on Linux apt/dnf/yum). Installs Git if missing. * `npm` method (default): global npm install * `git` method: clone/update repo, install deps with pnpm, build, then install wrapper at `~/.local/bin/openclaw` * Runs `openclaw doctor --non-interactive` on upgrades and git installs (best effort) * Attempts onboarding when appropriate (TTY available, onboarding not disabled, and bootstrap/config checks pass) * Defaults `SHARP_IGNORE_GLOBAL_LIBVIPS=1` ### Source checkout detection If run inside an OpenClaw checkout (`package.json` + `pnpm-workspace.yaml`), the script offers: * use checkout (`git`), or * use global install (`npm`) If no TTY is available and no install method is set, it defaults to `npm` and warns. The script exits with code `2` for invalid method selection or invalid `--install-method` values. ### Examples (install.sh) ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --no-onboard ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --dry-run ``` | Flag | Description | | ------------------------------- | ---------------------------------------------------------- | | `--install-method npm\|git` | Choose install method (default: `npm`). Alias: `--method` | | `--npm` | Shortcut for npm method | | `--git` | Shortcut for git method. Alias: `--github` | | `--version ` | npm version or dist-tag (default: `latest`) | | `--beta` | Use beta dist-tag if available, else fallback to `latest` | | `--git-dir ` | Checkout directory (default: `~/openclaw`). Alias: `--dir` | | `--no-git-update` | Skip `git pull` for existing checkout | | `--no-prompt` | Disable prompts | | `--no-onboard` | Skip onboarding | | `--onboard` | Enable onboarding | | `--dry-run` | Print actions without applying changes | | `--verbose` | Enable debug output (`set -x`, npm notice-level logs) | | `--help` | Show usage (`-h`) | | Variable | Description | | ------------------------------------------- | --------------------------------------------- | | `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method | | `OPENCLAW_VERSION=latest\|next\|` | npm version or dist-tag | | `OPENCLAW_BETA=0\|1` | Use beta if available | | `OPENCLAW_GIT_DIR=` | Checkout directory | | `OPENCLAW_GIT_UPDATE=0\|1` | Toggle git updates | | `OPENCLAW_NO_PROMPT=1` | Disable prompts | | `OPENCLAW_NO_ONBOARD=1` | Skip onboarding | | `OPENCLAW_DRY_RUN=1` | Dry run mode | | `OPENCLAW_VERBOSE=1` | Debug mode | | `OPENCLAW_NPM_LOGLEVEL=error\|warn\|notice` | npm log level | | `SHARP_IGNORE_GLOBAL_LIBVIPS=0\|1` | Control sharp/libvips behavior (default: `1`) | *** ## install-cli.sh Designed for environments where you want everything under a local prefix (default `~/.openclaw`) and no system Node dependency. ### Flow (install-cli.sh) Downloads Node tarball (default `22.22.0`) to `/tools/node-v` and verifies SHA-256. If Git is missing, attempts install via apt/dnf/yum on Linux or Homebrew on macOS. Installs with npm using `--prefix `, then writes wrapper to `/bin/openclaw`. ### Examples (install-cli.sh) ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install-cli.sh | bash ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install-cli.sh | bash -s -- --prefix /opt/openclaw --version latest ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install-cli.sh | bash -s -- --json --prefix /opt/openclaw ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install-cli.sh | bash -s -- --onboard ``` | Flag | Description | | ---------------------- | ------------------------------------------------------------------------------- | | `--prefix ` | Install prefix (default: `~/.openclaw`) | | `--version ` | OpenClaw version or dist-tag (default: `latest`) | | `--node-version ` | Node version (default: `22.22.0`) | | `--json` | Emit NDJSON events | | `--onboard` | Run `openclaw onboard` after install | | `--no-onboard` | Skip onboarding (default) | | `--set-npm-prefix` | On Linux, force npm prefix to `~/.npm-global` if current prefix is not writable | | `--help` | Show usage (`-h`) | | Variable | Description | | ------------------------------------------- | --------------------------------------------------------------------------------- | | `OPENCLAW_PREFIX=` | Install prefix | | `OPENCLAW_VERSION=` | OpenClaw version or dist-tag | | `OPENCLAW_NODE_VERSION=` | Node version | | `OPENCLAW_NO_ONBOARD=1` | Skip onboarding | | `OPENCLAW_NPM_LOGLEVEL=error\|warn\|notice` | npm log level | | `OPENCLAW_GIT_DIR=` | Legacy cleanup lookup path (used when removing old `Peekaboo` submodule checkout) | | `SHARP_IGNORE_GLOBAL_LIBVIPS=0\|1` | Control sharp/libvips behavior (default: `1`) | *** ## install.ps1 ### Flow (install.ps1) Requires PowerShell 5+. If missing, attempts install via winget, then Chocolatey, then Scoop. * `npm` method (default): global npm install using selected `-Tag` * `git` method: clone/update repo, install/build with pnpm, and install wrapper at `%USERPROFILE%\.local\bin\openclaw.cmd` Adds needed bin directory to user PATH when possible, then runs `openclaw doctor --non-interactive` on upgrades and git installs (best effort). ### Examples (install.ps1) ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} iwr -useb https://openclaw.ai/install.ps1 | iex ``` ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} & ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -InstallMethod git ``` ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} & ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -InstallMethod git -GitDir "C:\openclaw" ``` ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} & ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -DryRun ``` ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} # install.ps1 has no dedicated -Verbose flag yet. Set-PSDebug -Trace 1 & ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -NoOnboard Set-PSDebug -Trace 0 ``` | Flag | Description | | ------------------------- | ------------------------------------------------------ | | `-InstallMethod npm\|git` | Install method (default: `npm`) | | `-Tag ` | npm dist-tag (default: `latest`) | | `-GitDir ` | Checkout directory (default: `%USERPROFILE%\openclaw`) | | `-NoOnboard` | Skip onboarding | | `-NoGitUpdate` | Skip `git pull` | | `-DryRun` | Print actions only | | Variable | Description | | ---------------------------------- | ------------------ | | `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method | | `OPENCLAW_GIT_DIR=` | Checkout directory | | `OPENCLAW_NO_ONBOARD=1` | Skip onboarding | | `OPENCLAW_GIT_UPDATE=0` | Disable git pull | | `OPENCLAW_DRY_RUN=1` | Dry run mode | If `-InstallMethod git` is used and Git is missing, the script exits and prints the Git for Windows link. *** ## CI and automation Use non-interactive flags/env vars for predictable runs. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --no-prompt --no-onboard ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} OPENCLAW_INSTALL_METHOD=git OPENCLAW_NO_PROMPT=1 \ curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install-cli.sh | bash -s -- --json --prefix /opt/openclaw ``` ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} & ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -NoOnboard ``` *** ## Troubleshooting Git is required for `git` install method. For `npm` installs, Git is still checked/installed to avoid `spawn git ENOENT` failures when dependencies use git URLs. Some Linux setups point npm global prefix to root-owned paths. `install.sh` can switch prefix to `~/.npm-global` and append PATH exports to shell rc files (when those files exist). The scripts default `SHARP_IGNORE_GLOBAL_LIBVIPS=1` to avoid sharp building against system libvips. To override: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} SHARP_IGNORE_GLOBAL_LIBVIPS=0 curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash ``` Install Git for Windows, reopen PowerShell, rerun installer. Run `npm config get prefix` and add that directory to your user PATH (no `\bin` suffix needed on Windows), then reopen PowerShell. `install.ps1` does not currently expose a `-Verbose` switch. Use PowerShell tracing for script-level diagnostics: ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} Set-PSDebug -Trace 1 & ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -NoOnboard Set-PSDebug -Trace 0 ``` Usually a PATH issue. See [Node.js troubleshooting](/install/node#troubleshooting). # macOS VMs Source: https://cryptoclawdocs.termix.ai/install/macos-vm # OpenClaw on macOS VMs (Sandboxing) ## Recommended default (most users) * **Small Linux VPS** for an always-on Gateway and low cost. See [VPS hosting](/vps). * **Dedicated hardware** (Mac mini or Linux box) if you want full control and a **residential IP** for browser automation. Many sites block data center IPs, so local browsing often works better. * **Hybrid:** keep the Gateway on a cheap VPS, and connect your Mac as a **node** when you need browser/UI automation. See [Nodes](/nodes) and [Gateway remote](/gateway/remote). Use a macOS VM when you specifically need macOS-only capabilities (iMessage/BlueBubbles) or want strict isolation from your daily Mac. ## macOS VM options ### Local VM on your Apple Silicon Mac (Lume) Run OpenClaw in a sandboxed macOS VM on your existing Apple Silicon Mac using [Lume](https://cua.ai/docs/lume). This gives you: * Full macOS environment in isolation (your host stays clean) * iMessage support via BlueBubbles (impossible on Linux/Windows) * Instant reset by cloning VMs * No extra hardware or cloud costs ### Hosted Mac providers (cloud) If you want macOS in the cloud, hosted Mac providers work too: * [MacStadium](https://www.macstadium.com/) (hosted Macs) * Other hosted Mac vendors also work; follow their VM + SSH docs Once you have SSH access to a macOS VM, continue at step 6 below. *** ## Quick path (Lume, experienced users) 1. Install Lume 2. `lume create openclaw --os macos --ipsw latest` 3. Complete Setup Assistant, enable Remote Login (SSH) 4. `lume run openclaw --no-display` 5. SSH in, install OpenClaw, configure channels 6. Done *** ## What you need (Lume) * Apple Silicon Mac (M1/M2/M3/M4) * macOS Sequoia or later on the host * \~60 GB free disk space per VM * \~20 minutes *** ## 1) Install Lume ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)" ``` If `~/.local/bin` isn't in your PATH: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} echo 'export PATH="$PATH:$HOME/.local/bin"' >> ~/.zshrc && source ~/.zshrc ``` Verify: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} lume --version ``` Docs: [Lume Installation](https://cua.ai/docs/lume/guide/getting-started/installation) *** ## 2) Create the macOS VM ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} lume create openclaw --os macos --ipsw latest ``` This downloads macOS and creates the VM. A VNC window opens automatically. Note: The download can take a while depending on your connection. *** ## 3) Complete Setup Assistant In the VNC window: 1. Select language and region 2. Skip Apple ID (or sign in if you want iMessage later) 3. Create a user account (remember the username and password) 4. Skip all optional features After setup completes, enable SSH: 1. Open System Settings → General → Sharing 2. Enable "Remote Login" *** ## 4) Get the VM's IP address ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} lume get openclaw ``` Look for the IP address (usually `192.168.64.x`). *** ## 5) SSH into the VM ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ssh youruser@192.168.64.X ``` Replace `youruser` with the account you created, and the IP with your VM's IP. *** ## 6) Install OpenClaw Inside the VM: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} npm install -g openclaw@latest openclaw onboard --install-daemon ``` Follow the onboarding prompts to set up your model provider (Anthropic, OpenAI, etc.). *** ## 7) Configure channels Edit the config file: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} nano ~/.openclaw/openclaw.json ``` Add your channels: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "channels": { "whatsapp": { "dmPolicy": "allowlist", "allowFrom": ["+15551234567"] }, "telegram": { "botToken": "YOUR_BOT_TOKEN" } } } ``` Then login to WhatsApp (scan QR): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw channels login ``` *** ## 8) Run the VM headlessly Stop the VM and restart without display: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} lume stop openclaw lume run openclaw --no-display ``` The VM runs in the background. OpenClaw's daemon keeps the gateway running. To check status: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ssh youruser@192.168.64.X "openclaw status" ``` *** ## Bonus: iMessage integration This is the killer feature of running on macOS. Use [BlueBubbles](https://bluebubbles.app) to add iMessage to OpenClaw. Inside the VM: 1. Download BlueBubbles from bluebubbles.app 2. Sign in with your Apple ID 3. Enable the Web API and set a password 4. Point BlueBubbles webhooks at your gateway (example: `https://your-gateway-host:3000/bluebubbles-webhook?password=`) Add to your OpenClaw config: ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "channels": { "bluebubbles": { "serverUrl": "http://localhost:1234", "password": "your-api-password", "webhookPath": "/bluebubbles-webhook" } } } ``` Restart the gateway. Now your agent can send and receive iMessages. Full setup details: [BlueBubbles channel](/channels/bluebubbles) *** ## Save a golden image Before customizing further, snapshot your clean state: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} lume stop openclaw lume clone openclaw openclaw-golden ``` Reset anytime: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} lume stop openclaw && lume delete openclaw lume clone openclaw-golden openclaw lume run openclaw --no-display ``` *** ## Running 24/7 Keep the VM running by: * Keeping your Mac plugged in * Disabling sleep in System Settings → Energy Saver * Using `caffeinate` if needed For true always-on, consider a dedicated Mac mini or a small VPS. See [VPS hosting](/vps). *** ## Troubleshooting | Problem | Solution | | ------------------------ | ---------------------------------------------------------------------------------- | | Can't SSH into VM | Check "Remote Login" is enabled in VM's System Settings | | VM IP not showing | Wait for VM to fully boot, run `lume get openclaw` again | | Lume command not found | Add `~/.local/bin` to your PATH | | WhatsApp QR not scanning | Ensure you're logged into the VM (not host) when running `openclaw channels login` | *** ## Related docs * [VPS hosting](/vps) * [Nodes](/nodes) * [Gateway remote](/gateway/remote) * [BlueBubbles channel](/channels/bluebubbles) * [Lume Quickstart](https://cua.ai/docs/lume/guide/getting-started/quickstart) * [Lume CLI Reference](https://cua.ai/docs/lume/reference/cli-reference) * [Unattended VM Setup](https://cua.ai/docs/lume/guide/fundamentals/unattended-setup) (advanced) * [Docker Sandboxing](/install/docker) (alternative isolation approach) # Migration Guide Source: https://cryptoclawdocs.termix.ai/install/migrating # Migrating OpenClaw to a new machine This guide migrates a OpenClaw Gateway from one machine to another **without redoing onboarding**. The migration is simple conceptually: * Copy the **state directory** (`$OPENCLAW_STATE_DIR`, default: `~/.openclaw/`) — this includes config, auth, sessions, and channel state. * Copy your **workspace** (`~/.openclaw/workspace/` by default) — this includes your agent files (memory, prompts, etc.). But there are common footguns around **profiles**, **permissions**, and **partial copies**. ## Before you start (what you are migrating) ### 1) Identify your state directory Most installs use the default: * **State dir:** `~/.openclaw/` But it may be different if you use: * `--profile ` (often becomes `~/.openclaw-/`) * `OPENCLAW_STATE_DIR=/some/path` If you’re not sure, run on the **old** machine: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw status ``` Look for mentions of `OPENCLAW_STATE_DIR` / profile in the output. If you run multiple gateways, repeat for each profile. ### 2) Identify your workspace Common defaults: * `~/.openclaw/workspace/` (recommended workspace) * a custom folder you created Your workspace is where files like `MEMORY.md`, `USER.md`, and `memory/*.md` live. ### 3) Understand what you will preserve If you copy **both** the state dir and workspace, you keep: * Gateway configuration (`openclaw.json`) * Auth profiles / API keys / OAuth tokens * Session history + agent state * Channel state (e.g. WhatsApp login/session) * Your workspace files (memory, skills notes, etc.) If you copy **only** the workspace (e.g., via Git), you do **not** preserve: * sessions * credentials * channel logins Those live under `$OPENCLAW_STATE_DIR`. ## Migration steps (recommended) ### Step 0 — Make a backup (old machine) On the **old** machine, stop the gateway first so files aren’t changing mid-copy: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway stop ``` (Optional but recommended) archive the state dir and workspace: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} # Adjust paths if you use a profile or custom locations cd ~ tar -czf openclaw-state.tgz .openclaw tar -czf openclaw-workspace.tgz .openclaw/workspace ``` If you have multiple profiles/state dirs (e.g. `~/.openclaw-main`, `~/.openclaw-work`), archive each. ### Step 1 — Install OpenClaw on the new machine On the **new** machine, install the CLI (and Node if needed): * See: [Install](/install) At this stage, it’s OK if onboarding creates a fresh `~/.openclaw/` — you will overwrite it in the next step. ### Step 2 — Copy the state dir + workspace to the new machine Copy **both**: * `$OPENCLAW_STATE_DIR` (default `~/.openclaw/`) * your workspace (default `~/.openclaw/workspace/`) Common approaches: * `scp` the tarballs and extract * `rsync -a` over SSH * external drive After copying, ensure: * Hidden directories were included (e.g. `.openclaw/`) * File ownership is correct for the user running the gateway ### Step 3 — Run Doctor (migrations + service repair) On the **new** machine: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw doctor ``` Doctor is the “safe boring” command. It repairs services, applies config migrations, and warns about mismatches. Then: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway restart openclaw status ``` ## Common footguns (and how to avoid them) ### Footgun: profile / state-dir mismatch If you ran the old gateway with a profile (or `OPENCLAW_STATE_DIR`), and the new gateway uses a different one, you’ll see symptoms like: * config changes not taking effect * channels missing / logged out * empty session history Fix: run the gateway/service using the **same** profile/state dir you migrated, then rerun: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw doctor ``` ### Footgun: copying only `openclaw.json` `openclaw.json` is not enough. Many providers store state under: * `$OPENCLAW_STATE_DIR/credentials/` * `$OPENCLAW_STATE_DIR/agents//...` Always migrate the entire `$OPENCLAW_STATE_DIR` folder. ### Footgun: permissions / ownership If you copied as root or changed users, the gateway may fail to read credentials/sessions. Fix: ensure the state dir + workspace are owned by the user running the gateway. ### Footgun: migrating between remote/local modes * If your UI (WebUI/TUI) points at a **remote** gateway, the remote host owns the session store + workspace. * Migrating your laptop won’t move the remote gateway’s state. If you’re in remote mode, migrate the **gateway host**. ### Footgun: secrets in backups `$OPENCLAW_STATE_DIR` contains secrets (API keys, OAuth tokens, WhatsApp creds). Treat backups like production secrets: * store encrypted * avoid sharing over insecure channels * rotate keys if you suspect exposure ## Verification checklist On the new machine, confirm: * `openclaw status` shows the gateway running * Your channels are still connected (e.g. WhatsApp doesn’t require re-pair) * The dashboard opens and shows existing sessions * Your workspace files (memory, configs) are present ## Related * [Doctor](/gateway/doctor) * [Gateway troubleshooting](/gateway/troubleshooting) * [Where does OpenClaw store its data?](/help/faq#where-does-openclaw-store-its-data) # Nix Source: https://cryptoclawdocs.termix.ai/install/nix # Nix Installation The recommended way to run OpenClaw with Nix is via **[nix-openclaw](https://github.com/openclaw/nix-openclaw)** — a batteries-included Home Manager module. ## Quick Start Paste this to your AI agent (Claude, Cursor, etc.): ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} I want to set up nix-openclaw on my Mac. Repository: github:openclaw/nix-openclaw What I need you to do: 1. Check if Determinate Nix is installed (if not, install it) 2. Create a local flake at ~/code/openclaw-local using templates/agent-first/flake.nix 3. Help me create a Telegram bot (@BotFather) and get my chat ID (@userinfobot) 4. Set up secrets (bot token, model provider API key) - plain files at ~/.secrets/ is fine 5. Fill in the template placeholders and run home-manager switch 6. Verify: launchd running, bot responds to messages Reference the nix-openclaw README for module options. ``` > **📦 Full guide: [github.com/openclaw/nix-openclaw](https://github.com/openclaw/nix-openclaw)** > > The nix-openclaw repo is the source of truth for Nix installation. This page is just a quick overview. ## What you get * Gateway + macOS app + tools (whisper, spotify, cameras) — all pinned * Launchd service that survives reboots * Plugin system with declarative config * Instant rollback: `home-manager switch --rollback` *** ## Nix Mode Runtime Behavior When `OPENCLAW_NIX_MODE=1` is set (automatic with nix-openclaw): OpenClaw supports a **Nix mode** that makes configuration deterministic and disables auto-install flows. Enable it by exporting: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} OPENCLAW_NIX_MODE=1 ``` On macOS, the GUI app does not automatically inherit shell env vars. You can also enable Nix mode via defaults: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} defaults write ai.openclaw.mac openclaw.nixMode -bool true ``` ### Config + state paths OpenClaw reads JSON5 config from `OPENCLAW_CONFIG_PATH` and stores mutable data in `OPENCLAW_STATE_DIR`. When needed, you can also set `OPENCLAW_HOME` to control the base home directory used for internal path resolution. * `OPENCLAW_HOME` (default precedence: `HOME` / `USERPROFILE` / `os.homedir()`) * `OPENCLAW_STATE_DIR` (default: `~/.openclaw`) * `OPENCLAW_CONFIG_PATH` (default: `$OPENCLAW_STATE_DIR/openclaw.json`) When running under Nix, set these explicitly to Nix-managed locations so runtime state and config stay out of the immutable store. ### Runtime behavior in Nix mode * Auto-install and self-mutation flows are disabled * Missing dependencies surface Nix-specific remediation messages * UI surfaces a read-only Nix mode banner when present ## Packaging note (macOS) The macOS packaging flow expects a stable Info.plist template at: ``` apps/macos/Sources/OpenClaw/Resources/Info.plist ``` [`scripts/package-mac-app.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/package-mac-app.sh) copies this template into the app bundle and patches dynamic fields (bundle ID, version/build, Git SHA, Sparkle keys). This keeps the plist deterministic for SwiftPM packaging and Nix builds (which do not rely on a full Xcode toolchain). ## Related * [nix-openclaw](https://github.com/openclaw/nix-openclaw) — full setup guide * [Wizard](/start/wizard) — non-Nix CLI setup * [Docker](/install/docker) — containerized setup # Deploy on Northflank Source: https://cryptoclawdocs.termix.ai/install/northflank Deploy OpenClaw on Northflank with a one-click template and finish setup in your browser. This is the easiest “no terminal on the server” path: Northflank runs the Gateway for you, and you configure everything via the `/setup` web wizard. ## How to get started 1. Click [Deploy OpenClaw](https://northflank.com/stacks/deploy-openclaw) to open the template. 2. Create an [account on Northflank](https://app.northflank.com/signup) if you don’t already have one. 3. Click **Deploy OpenClaw now**. 4. Set the required environment variable: `SETUP_PASSWORD`. 5. Click **Deploy stack** to build and run the OpenClaw template. 6. Wait for the deployment to complete, then click **View resources**. 7. Open the OpenClaw service. 8. Open the public OpenClaw URL and complete setup at `/setup`. 9. Open the Control UI at `/openclaw`. ## What you get * Hosted OpenClaw Gateway + Control UI * Web setup wizard at `/setup` (no terminal commands) * Persistent storage via Northflank Volume (`/data`) so config/credentials/workspace survive redeploys ## Setup flow 1. Visit `https:///setup` and enter your `SETUP_PASSWORD`. 2. Choose a model/auth provider and paste your key. 3. (Optional) Add Telegram/Discord/Slack tokens. 4. Click **Run setup**. 5. Open the Control UI at `https:///openclaw` If Telegram DMs are set to pairing, the setup wizard can approve the pairing code. ## Getting chat tokens ### Telegram bot token 1. Message `@BotFather` in Telegram 2. Run `/newbot` 3. Copy the token (looks like `123456789:AA...`) 4. Paste it into `/setup` ### Discord bot token 1. Go to [https://discord.com/developers/applications](https://discord.com/developers/applications) 2. **New Application** → choose a name 3. **Bot** → **Add Bot** 4. **Enable MESSAGE CONTENT INTENT** under Bot → Privileged Gateway Intents (required or the bot will crash on startup) 5. Copy the **Bot Token** and paste into `/setup` 6. Invite the bot to your server (OAuth2 URL Generator; scopes: `bot`, `applications.commands`) # Podman Source: https://cryptoclawdocs.termix.ai/install/podman # Podman Run the OpenClaw gateway in a **rootless** Podman container. Uses the same image as Docker (build from the repo [Dockerfile](https://github.com/openclaw/openclaw/blob/main/Dockerfile)). ## Requirements * Podman (rootless) * Sudo for one-time setup (create user, build image) ## Quick start **1. One-time setup** (from repo root; creates user, builds image, installs launch script): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ./setup-podman.sh ``` This also creates a minimal `~openclaw/.openclaw/openclaw.json` (sets `gateway.mode="local"`) so the gateway can start without running the wizard. By default the container is **not** installed as a systemd service, you start it manually (see below). For a production-style setup with auto-start and restarts, install it as a systemd Quadlet user service instead: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ./setup-podman.sh --quadlet ``` (Or set `OPENCLAW_PODMAN_QUADLET=1`; use `--container` to install only the container and launch script.) **2. Start gateway** (manual, for quick smoke testing): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ./scripts/run-openclaw-podman.sh launch ``` **3. Onboarding wizard** (e.g. to add channels or providers): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} ./scripts/run-openclaw-podman.sh launch setup ``` Then open `http://127.0.0.1:18789/` and use the token from `~openclaw/.openclaw/.env` (or the value printed by setup). ## Systemd (Quadlet, optional) If you ran `./setup-podman.sh --quadlet` (or `OPENCLAW_PODMAN_QUADLET=1`), a [Podman Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) unit is installed so the gateway runs as a systemd user service for the openclaw user. The service is enabled and started at the end of setup. * **Start:** `sudo systemctl --machine openclaw@ --user start openclaw.service` * **Stop:** `sudo systemctl --machine openclaw@ --user stop openclaw.service` * **Status:** `sudo systemctl --machine openclaw@ --user status openclaw.service` * **Logs:** `sudo journalctl --machine openclaw@ --user -u openclaw.service -f` The quadlet file lives at `~openclaw/.config/containers/systemd/openclaw.container`. To change ports or env, edit that file (or the `.env` it sources), then `sudo systemctl --machine openclaw@ --user daemon-reload` and restart the service. On boot, the service starts automatically if lingering is enabled for openclaw (setup does this when loginctl is available). To add quadlet **after** an initial setup that did not use it, re-run: `./setup-podman.sh --quadlet`. ## The openclaw user (non-login) `setup-podman.sh` creates a dedicated system user `openclaw`: * **Shell:** `nologin` — no interactive login; reduces attack surface. * **Home:** e.g. `/home/openclaw` — holds `~/.openclaw` (config, workspace) and the launch script `run-openclaw-podman.sh`. * **Rootless Podman:** The user must have a **subuid** and **subgid** range. Many distros assign these automatically when the user is created. If setup prints a warning, add lines to `/etc/subuid` and `/etc/subgid`: ```text theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw:100000:65536 ``` Then start the gateway as that user (e.g. from cron or systemd): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} sudo -u openclaw /home/openclaw/run-openclaw-podman.sh sudo -u openclaw /home/openclaw/run-openclaw-podman.sh setup ``` * **Config:** Only `openclaw` and root can access `/home/openclaw/.openclaw`. To edit config: use the Control UI once the gateway is running, or `sudo -u openclaw $EDITOR /home/openclaw/.openclaw/openclaw.json`. ## Environment and config * **Token:** Stored in `~openclaw/.openclaw/.env` as `OPENCLAW_GATEWAY_TOKEN`. `setup-podman.sh` and `run-openclaw-podman.sh` generate it if missing (uses `openssl`, `python3`, or `od`). * **Optional:** In that `.env` you can set provider keys (e.g. `GROQ_API_KEY`, `OLLAMA_API_KEY`) and other OpenClaw env vars. * **Host ports:** By default the script maps `18789` (gateway) and `18790` (bridge). Override the **host** port mapping with `OPENCLAW_PODMAN_GATEWAY_HOST_PORT` and `OPENCLAW_PODMAN_BRIDGE_HOST_PORT` when launching. * **Gateway bind:** By default, `run-openclaw-podman.sh` starts the gateway with `--bind loopback` for safe local access. To expose on LAN, set `OPENCLAW_GATEWAY_BIND=lan` and configure `gateway.controlUi.allowedOrigins` (or explicitly enable host-header fallback) in `openclaw.json`. * **Paths:** Host config and workspace default to `~openclaw/.openclaw` and `~openclaw/.openclaw/workspace`. Override the host paths used by the launch script with `OPENCLAW_CONFIG_DIR` and `OPENCLAW_WORKSPACE_DIR`. ## Useful commands * **Logs:** With quadlet: `sudo journalctl --machine openclaw@ --user -u openclaw.service -f`. With script: `sudo -u openclaw podman logs -f openclaw` * **Stop:** With quadlet: `sudo systemctl --machine openclaw@ --user stop openclaw.service`. With script: `sudo -u openclaw podman stop openclaw` * **Start again:** With quadlet: `sudo systemctl --machine openclaw@ --user start openclaw.service`. With script: re-run the launch script or `podman start openclaw` * **Remove container:** `sudo -u openclaw podman rm -f openclaw` — config and workspace on the host are kept ## Troubleshooting * **Permission denied (EACCES) on config or auth-profiles:** The container defaults to `--userns=keep-id` and runs as the same uid/gid as the host user running the script. Ensure your host `OPENCLAW_CONFIG_DIR` and `OPENCLAW_WORKSPACE_DIR` are owned by that user. * **Gateway start blocked (missing `gateway.mode=local`):** Ensure `~openclaw/.openclaw/openclaw.json` exists and sets `gateway.mode="local"`. `setup-podman.sh` creates this file if missing. * **Rootless Podman fails for user openclaw:** Check `/etc/subuid` and `/etc/subgid` contain a line for `openclaw` (e.g. `openclaw:100000:65536`). Add it if missing and restart. * **Container name in use:** The launch script uses `podman run --replace`, so the existing container is replaced when you start again. To clean up manually: `podman rm -f openclaw`. * **Script not found when running as openclaw:** Ensure `setup-podman.sh` was run so that `run-openclaw-podman.sh` is copied to openclaw’s home (e.g. `/home/openclaw/run-openclaw-podman.sh`). * **Quadlet service not found or fails to start:** Run `sudo systemctl --machine openclaw@ --user daemon-reload` after editing the `.container` file. Quadlet requires cgroups v2: `podman info --format '{{.Host.CgroupsVersion}}'` should show `2`. ## Optional: run as your own user To run the gateway as your normal user (no dedicated openclaw user): build the image, create `~/.openclaw/.env` with `OPENCLAW_GATEWAY_TOKEN`, and run the container with `--userns=keep-id` and mounts to your `~/.openclaw`. The launch script is designed for the openclaw-user flow; for a single-user setup you can instead run the `podman run` command from the script manually, pointing config and workspace to your home. Recommended for most users: use `setup-podman.sh` and run as the openclaw user so config and process are isolated. # Deploy on Railway Source: https://cryptoclawdocs.termix.ai/install/railway Deploy OpenClaw on Railway with a one-click template and finish setup in your browser. This is the easiest “no terminal on the server” path: Railway runs the Gateway for you, and you configure everything via the `/setup` web wizard. ## Quick checklist (new users) 1. Click **Deploy on Railway** (below). 2. Add a **Volume** mounted at `/data`. 3. Set the required **Variables** (at least `SETUP_PASSWORD`). 4. Enable **HTTP Proxy** on port `8080`. 5. Open `https:///setup` and finish the wizard. ## One-click deploy Deploy on Railway After deploy, find your public URL in **Railway → your service → Settings → Domains**. Railway will either: * give you a generated domain (often `https://.up.railway.app`), or * use your custom domain if you attached one. Then open: * `https:///setup` — setup wizard (password protected) * `https:///openclaw` — Control UI ## What you get * Hosted OpenClaw Gateway + Control UI * Web setup wizard at `/setup` (no terminal commands) * Persistent storage via Railway Volume (`/data`) so config/credentials/workspace survive redeploys * Backup export at `/setup/export` to migrate off Railway later ## Required Railway settings ### Public Networking Enable **HTTP Proxy** for the service. * Port: `8080` ### Volume (required) Attach a volume mounted at: * `/data` ### Variables Set these variables on the service: * `SETUP_PASSWORD` (required) * `PORT=8080` (required — must match the port in Public Networking) * `OPENCLAW_STATE_DIR=/data/.openclaw` (recommended) * `OPENCLAW_WORKSPACE_DIR=/data/workspace` (recommended) * `OPENCLAW_GATEWAY_TOKEN` (recommended; treat as an admin secret) ## Setup flow 1. Visit `https:///setup` and enter your `SETUP_PASSWORD`. 2. Choose a model/auth provider and paste your key. 3. (Optional) Add Telegram/Discord/Slack tokens. 4. Click **Run setup**. If Telegram DMs are set to pairing, the setup wizard can approve the pairing code. ## Getting chat tokens ### Telegram bot token 1. Message `@BotFather` in Telegram 2. Run `/newbot` 3. Copy the token (looks like `123456789:AA...`) 4. Paste it into `/setup` ### Discord bot token 1. Go to [https://discord.com/developers/applications](https://discord.com/developers/applications) 2. **New Application** → choose a name 3. **Bot** → **Add Bot** 4. **Enable MESSAGE CONTENT INTENT** under Bot → Privileged Gateway Intents (required or the bot will crash on startup) 5. Copy the **Bot Token** and paste into `/setup` 6. Invite the bot to your server (OAuth2 URL Generator; scopes: `bot`, `applications.commands`) ## Backups & migration Download a backup at: * `https:///setup/export` This exports your OpenClaw state + workspace so you can migrate to another host without losing config or memory. # Deploy on Render Source: https://cryptoclawdocs.termix.ai/install/render Deploy OpenClaw on Render using Infrastructure as Code. The included `render.yaml` Blueprint defines your entire stack declaratively, service, disk, environment variables, so you can deploy with a single click and version your infrastructure alongside your code. ## Prerequisites * A [Render account](https://render.com) (free tier available) * An API key from your preferred [model provider](/providers) ## Deploy with a Render Blueprint [Deploy to Render](https://render.com/deploy?repo=https://github.com/openclaw/openclaw) Clicking this link will: 1. Create a new Render service from the `render.yaml` Blueprint at the root of this repo. 2. Prompt you to set `SETUP_PASSWORD` 3. Build the Docker image and deploy Once deployed, your service URL follows the pattern `https://.onrender.com`. ## Understanding the Blueprint Render Blueprints are YAML files that define your infrastructure. The `render.yaml` in this repository configures everything needed to run OpenClaw: ```yaml theme={"theme":{"light":"min-light","dark":"min-dark"}} services: - type: web name: openclaw runtime: docker plan: starter healthCheckPath: /health envVars: - key: PORT value: "8080" - key: SETUP_PASSWORD sync: false # prompts during deploy - key: OPENCLAW_STATE_DIR value: /data/.openclaw - key: OPENCLAW_WORKSPACE_DIR value: /data/workspace - key: OPENCLAW_GATEWAY_TOKEN generateValue: true # auto-generates a secure token disk: name: openclaw-data mountPath: /data sizeGB: 1 ``` Key Blueprint features used: | Feature | Purpose | | --------------------- | ---------------------------------------------------------- | | `runtime: docker` | Builds from the repo's Dockerfile | | `healthCheckPath` | Render monitors `/health` and restarts unhealthy instances | | `sync: false` | Prompts for value during deploy (secrets) | | `generateValue: true` | Auto-generates a cryptographically secure value | | `disk` | Persistent storage that survives redeploys | ## Choosing a plan | Plan | Spin-down | Disk | Best for | | --------- | ----------------- | ------------- | ----------------------------- | | Free | After 15 min idle | Not available | Testing, demos | | Starter | Never | 1GB+ | Personal use, small teams | | Standard+ | Never | 1GB+ | Production, multiple channels | The Blueprint defaults to `starter`. To use free tier, change `plan: free` in your fork's `render.yaml` (but note: no persistent disk means config resets on each deploy). ## After deployment ### Complete the setup wizard 1. Navigate to `https://.onrender.com/setup` 2. Enter your `SETUP_PASSWORD` 3. Select a model provider and paste your API key 4. Optionally configure messaging channels (Telegram, Discord, Slack) 5. Click **Run setup** ### Access the Control UI The web dashboard is available at `https://.onrender.com/openclaw`. ## Render Dashboard features ### Logs View real-time logs in **Dashboard → your service → Logs**. Filter by: * Build logs (Docker image creation) * Deploy logs (service startup) * Runtime logs (application output) ### Shell access For debugging, open a shell session via **Dashboard → your service → Shell**. The persistent disk is mounted at `/data`. ### Environment variables Modify variables in **Dashboard → your service → Environment**. Changes trigger an automatic redeploy. ### Auto-deploy If you use the original OpenClaw repository, Render will not auto-deploy your OpenClaw. To update it, run a manual Blueprint sync from the dashboard. ## Custom domain 1. Go to **Dashboard → your service → Settings → Custom Domains** 2. Add your domain 3. Configure DNS as instructed (CNAME to `*.onrender.com`) 4. Render provisions a TLS certificate automatically ## Scaling Render supports horizontal and vertical scaling: * **Vertical**: Change the plan to get more CPU/RAM * **Horizontal**: Increase instance count (Standard plan and above) For OpenClaw, vertical scaling is usually sufficient. Horizontal scaling requires sticky sessions or external state management. ## Backups and migration Export your configuration and workspace at any time: ``` https://.onrender.com/setup/export ``` This downloads a portable backup you can restore on any OpenClaw host. ## Troubleshooting ### Service won't start Check the deploy logs in the Render Dashboard. Common issues: * Missing `SETUP_PASSWORD` — the Blueprint prompts for this, but verify it's set * Port mismatch — ensure `PORT=8080` matches the Dockerfile's exposed port ### Slow cold starts (free tier) Free tier services spin down after 15 minutes of inactivity. The first request after spin-down takes a few seconds while the container starts. Upgrade to Starter plan for always-on. ### Data loss after redeploy This happens on free tier (no persistent disk). Upgrade to a paid plan, or regularly export your config via `/setup/export`. ### Health check failures Render expects a 200 response from `/health` within 30 seconds. If builds succeed but deploys fail, the service may be taking too long to start. Check: * Build logs for errors * Whether the container runs locally with `docker build && docker run` # Uninstall Source: https://cryptoclawdocs.termix.ai/install/uninstall # Uninstall Two paths: * **Easy path** if `openclaw` is still installed. * **Manual service removal** if the CLI is gone but the service is still running. ## Easy path (CLI still installed) Recommended: use the built-in uninstaller: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw uninstall ``` Non-interactive (automation / npx): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw uninstall --all --yes --non-interactive npx -y openclaw uninstall --all --yes --non-interactive ``` Manual steps (same result): 1. Stop the gateway service: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway stop ``` 2. Uninstall the gateway service (launchd/systemd/schtasks): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway uninstall ``` 3. Delete state + config: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} rm -rf "${OPENCLAW_STATE_DIR:-$HOME/.openclaw}" ``` If you set `OPENCLAW_CONFIG_PATH` to a custom location outside the state dir, delete that file too. 4. Delete your workspace (optional, removes agent files): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} rm -rf ~/.openclaw/workspace ``` 5. Remove the CLI install (pick the one you used): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} npm rm -g openclaw pnpm remove -g openclaw bun remove -g openclaw ``` 6. If you installed the macOS app: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} rm -rf /Applications/OpenClaw.app ``` Notes: * If you used profiles (`--profile` / `OPENCLAW_PROFILE`), repeat step 3 for each state dir (defaults are `~/.openclaw-`). * In remote mode, the state dir lives on the **gateway host**, so run steps 1-4 there too. ## Manual service removal (CLI not installed) Use this if the gateway service keeps running but `openclaw` is missing. ### macOS (launchd) Default label is `ai.openclaw.gateway` (or `ai.openclaw.`; legacy `com.openclaw.*` may still exist): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} launchctl bootout gui/$UID/ai.openclaw.gateway rm -f ~/Library/LaunchAgents/ai.openclaw.gateway.plist ``` If you used a profile, replace the label and plist name with `ai.openclaw.`. Remove any legacy `com.openclaw.*` plists if present. ### Linux (systemd user unit) Default unit name is `openclaw-gateway.service` (or `openclaw-gateway-.service`): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} systemctl --user disable --now openclaw-gateway.service rm -f ~/.config/systemd/user/openclaw-gateway.service systemctl --user daemon-reload ``` ### Windows (Scheduled Task) Default task name is `OpenClaw Gateway` (or `OpenClaw Gateway ()`). The task script lives under your state dir. ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} schtasks /Delete /F /TN "OpenClaw Gateway" Remove-Item -Force "$env:USERPROFILE\.openclaw\gateway.cmd" ``` If you used a profile, delete the matching task name and `~\.openclaw-\gateway.cmd`. ## Normal install vs source checkout ### Normal install (install.sh / npm / pnpm / bun) If you used `https://openclaw.ai/install.sh` or `install.ps1`, the CLI was installed with `npm install -g openclaw@latest`. Remove it with `npm rm -g openclaw` (or `pnpm remove -g` / `bun remove -g` if you installed that way). ### Source checkout (git clone) If you run from a repo checkout (`git clone` + `openclaw ...` / `bun run openclaw ...`): 1. Uninstall the gateway service **before** deleting the repo (use the easy path above or manual service removal). 2. Delete the repo directory. 3. Remove state + workspace as shown above. # Updating Source: https://cryptoclawdocs.termix.ai/install/updating # Updating OpenClaw is moving fast (pre “1.0”). Treat updates like shipping infra: update → run checks → restart (or use `openclaw update`, which restarts) → verify. ## Recommended: re-run the website installer (upgrade in place) The **preferred** update path is to re-run the installer from the website. It detects existing installs, upgrades in place, and runs `openclaw doctor` when needed. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL https://openclaw.ai/install.sh | bash ``` Notes: * Add `--no-onboard` if you don’t want the onboarding wizard to run again. * For **source installs**, use: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL https://openclaw.ai/install.sh | bash -s -- --install-method git --no-onboard ``` The installer will `git pull --rebase` **only** if the repo is clean. * For **global installs**, the script uses `npm install -g openclaw@latest` under the hood. * Legacy note: `clawdbot` remains available as a compatibility shim. ## Before you update * Know how you installed: **global** (npm/pnpm) vs **from source** (git clone). * Know how your Gateway is running: **foreground terminal** vs **supervised service** (launchd/systemd). * Snapshot your tailoring: * Config: `~/.openclaw/openclaw.json` * Credentials: `~/.openclaw/credentials/` * Workspace: `~/.openclaw/workspace` ## Update (global install) Global install (pick one): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} npm i -g openclaw@latest ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} pnpm add -g openclaw@latest ``` We do **not** recommend Bun for the Gateway runtime (WhatsApp/Telegram bugs). To switch update channels (git + npm installs): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw update --channel beta openclaw update --channel dev openclaw update --channel stable ``` Use `--tag ` for a one-off install tag/version. See [Development channels](/install/development-channels) for channel semantics and release notes. Note: on npm installs, the gateway logs an update hint on startup (checks the current channel tag). Disable via `update.checkOnStart: false`. ### Core auto-updater (optional) Auto-updater is **off by default** and is a core Gateway feature (not a plugin). ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "update": { "channel": "stable", "auto": { "enabled": true, "stableDelayHours": 6, "stableJitterHours": 12, "betaCheckIntervalHours": 1 } } } ``` Behavior: * `stable`: when a new version is seen, OpenClaw waits `stableDelayHours` and then applies a deterministic per-install jitter in `stableJitterHours` (spread rollout). * `beta`: checks on `betaCheckIntervalHours` cadence (default: hourly) and applies when an update is available. * `dev`: no automatic apply; use manual `openclaw update`. Use `openclaw update --dry-run` to preview update actions before enabling automation. Then: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw doctor openclaw gateway restart openclaw health ``` Notes: * If your Gateway runs as a service, `openclaw gateway restart` is preferred over killing PIDs. * If you’re pinned to a specific version, see “Rollback / pinning” below. ## Update (`openclaw update`) For **source installs** (git checkout), prefer: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw update ``` It runs a safe-ish update flow: * Requires a clean worktree. * Switches to the selected channel (tag or branch). * Fetches + rebases against the configured upstream (dev channel). * Installs deps, builds, builds the Control UI, and runs `openclaw doctor`. * Restarts the gateway by default (use `--no-restart` to skip). If you installed via **npm/pnpm** (no git metadata), `openclaw update` will try to update via your package manager. If it can’t detect the install, use “Update (global install)” instead. ## Update (Control UI / RPC) The Control UI has **Update & Restart** (RPC: `update.run`). It: 1. Runs the same source-update flow as `openclaw update` (git checkout only). 2. Writes a restart sentinel with a structured report (stdout/stderr tail). 3. Restarts the gateway and pings the last active session with the report. If the rebase fails, the gateway aborts and restarts without applying the update. ## Update (from source) From the repo checkout: Preferred: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw update ``` Manual (equivalent-ish): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} git pull pnpm install pnpm build pnpm ui:build # auto-installs UI deps on first run openclaw doctor openclaw health ``` Notes: * `pnpm build` matters when you run the packaged `openclaw` binary ([`openclaw.mjs`](https://github.com/openclaw/openclaw/blob/main/openclaw.mjs)) or use Node to run `dist/`. * If you run from a repo checkout without a global install, use `pnpm openclaw ...` for CLI commands. * If you run directly from TypeScript (`pnpm openclaw ...`), a rebuild is usually unnecessary, but **config migrations still apply** → run doctor. * Switching between global and git installs is easy: install the other flavor, then run `openclaw doctor` so the gateway service entrypoint is rewritten to the current install. ## Always Run: `openclaw doctor` Doctor is the “safe update” command. It’s intentionally boring: repair + migrate + warn. Note: if you’re on a **source install** (git checkout), `openclaw doctor` will offer to run `openclaw update` first. Typical things it does: * Migrate deprecated config keys / legacy config file locations. * Audit DM policies and warn on risky “open” settings. * Check Gateway health and can offer to restart. * Detect and migrate older gateway services (launchd/systemd; legacy schtasks) to current OpenClaw services. * On Linux, ensure systemd user lingering (so the Gateway survives logout). Details: [Doctor](/gateway/doctor) ## Start / stop / restart the Gateway CLI (works regardless of OS): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway status openclaw gateway stop openclaw gateway restart openclaw gateway --port 18789 openclaw logs --follow ``` If you’re supervised: * macOS launchd (app-bundled LaunchAgent): `launchctl kickstart -k gui/$UID/ai.openclaw.gateway` (use `ai.openclaw.`; legacy `com.openclaw.*` still works) * Linux systemd user service: `systemctl --user restart openclaw-gateway[-].service` * Windows (WSL2): `systemctl --user restart openclaw-gateway[-].service` * `launchctl`/`systemctl` only work if the service is installed; otherwise run `openclaw gateway install`. Runbook + exact service labels: [Gateway runbook](/gateway) ## Rollback / pinning (when something breaks) ### Pin (global install) Install a known-good version (replace `` with the last working one): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} npm i -g openclaw@ ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} pnpm add -g openclaw@ ``` Tip: to see the current published version, run `npm view openclaw version`. Then restart + re-run doctor: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw doctor openclaw gateway restart ``` ### Pin (source) by date Pick a commit from a date (example: “state of main as of 2026-01-01”): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} git fetch origin git checkout "$(git rev-list -n 1 --before=\"2026-01-01\" origin/main)" ``` Then reinstall deps + restart: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} pnpm install pnpm build openclaw gateway restart ``` If you want to go back to latest later: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} git checkout main git pull ``` ## If you’re stuck * Run `openclaw doctor` again and read the output carefully (it often tells you the fix). * Check: [Troubleshooting](/gateway/troubleshooting) * Ask in Discord: [https://discord.gg/clawd](https://discord.gg/clawd) # Perplexity Sonar Source: https://cryptoclawdocs.termix.ai/perplexity # Perplexity Sonar OpenClaw can use Perplexity Sonar for the `web_search` tool. You can connect through Perplexity’s direct API or via OpenRouter. ## API options ### Perplexity (direct) * Base URL: [https://api.perplexity.ai](https://api.perplexity.ai) * Environment variable: `PERPLEXITY_API_KEY` ### OpenRouter (alternative) * Base URL: [https://openrouter.ai/api/v1](https://openrouter.ai/api/v1) * Environment variable: `OPENROUTER_API_KEY` * Supports prepaid/crypto credits. ## Config example ```json5 theme={null} { tools: { web: { search: { provider: "perplexity", perplexity: { apiKey: "pplx-...", baseUrl: "https://api.perplexity.ai", model: "perplexity/sonar-pro", }, }, }, }, } ``` ## Switching from Brave ```json5 theme={null} { tools: { web: { search: { provider: "perplexity", perplexity: { apiKey: "pplx-...", baseUrl: "https://api.perplexity.ai", }, }, }, }, } ``` If both `PERPLEXITY_API_KEY` and `OPENROUTER_API_KEY` are set, set `tools.web.search.perplexity.baseUrl` (or `tools.web.search.perplexity.apiKey`) to disambiguate. If no base URL is set, OpenClaw chooses a default based on the API key source: * `PERPLEXITY_API_KEY` or `pplx-...` → direct Perplexity (`https://api.perplexity.ai`) * `OPENROUTER_API_KEY` or `sk-or-...` → OpenRouter (`https://openrouter.ai/api/v1`) * Unknown key formats → OpenRouter (safe fallback) ## Models * `perplexity/sonar` — fast Q\&A with web search * `perplexity/sonar-pro` (default) — multi-step reasoning + web search * `perplexity/sonar-reasoning-pro` — deep research See [Web tools](/tools/web) for the full web\_search configuration. # Pi Integration Architecture Source: https://cryptoclawdocs.termix.ai/pi # Pi Integration Architecture This document describes how OpenClaw integrates with [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) and its sibling packages (`pi-ai`, `pi-agent-core`, `pi-tui`) to power its AI agent capabilities. ## Overview OpenClaw uses the pi SDK to embed an AI coding agent into its messaging gateway architecture. Instead of spawning pi as a subprocess or using RPC mode, OpenClaw directly imports and instantiates pi's `AgentSession` via `createAgentSession()`. This embedded approach provides: * Full control over session lifecycle and event handling * Custom tool injection (messaging, sandbox, channel-specific actions) * System prompt customization per channel/context * Session persistence with branching/compaction support * Multi-account auth profile rotation with failover * Provider-agnostic model switching ## Package Dependencies ```json theme={"theme":{"light":"min-light","dark":"min-dark"}} { "@mariozechner/pi-agent-core": "0.49.3", "@mariozechner/pi-ai": "0.49.3", "@mariozechner/pi-coding-agent": "0.49.3", "@mariozechner/pi-tui": "0.49.3" } ``` | Package | Purpose | | ----------------- | ------------------------------------------------------------------------------------------------------ | | `pi-ai` | Core LLM abstractions: `Model`, `streamSimple`, message types, provider APIs | | `pi-agent-core` | Agent loop, tool execution, `AgentMessage` types | | `pi-coding-agent` | High-level SDK: `createAgentSession`, `SessionManager`, `AuthStorage`, `ModelRegistry`, built-in tools | | `pi-tui` | Terminal UI components (used in OpenClaw's local TUI mode) | ## File Structure ``` src/agents/ ├── pi-embedded-runner.ts # Re-exports from pi-embedded-runner/ ├── pi-embedded-runner/ │ ├── run.ts # Main entry: runEmbeddedPiAgent() │ ├── run/ │ │ ├── attempt.ts # Single attempt logic with session setup │ │ ├── params.ts # RunEmbeddedPiAgentParams type │ │ ├── payloads.ts # Build response payloads from run results │ │ ├── images.ts # Vision model image injection │ │ └── types.ts # EmbeddedRunAttemptResult │ ├── abort.ts # Abort error detection │ ├── cache-ttl.ts # Cache TTL tracking for context pruning │ ├── compact.ts # Manual/auto compaction logic │ ├── extensions.ts # Load pi extensions for embedded runs │ ├── extra-params.ts # Provider-specific stream params │ ├── google.ts # Google/Gemini turn ordering fixes │ ├── history.ts # History limiting (DM vs group) │ ├── lanes.ts # Session/global command lanes │ ├── logger.ts # Subsystem logger │ ├── model.ts # Model resolution via ModelRegistry │ ├── runs.ts # Active run tracking, abort, queue │ ├── sandbox-info.ts # Sandbox info for system prompt │ ├── session-manager-cache.ts # SessionManager instance caching │ ├── session-manager-init.ts # Session file initialization │ ├── system-prompt.ts # System prompt builder │ ├── tool-split.ts # Split tools into builtIn vs custom │ ├── types.ts # EmbeddedPiAgentMeta, EmbeddedPiRunResult │ └── utils.ts # ThinkLevel mapping, error description ├── pi-embedded-subscribe.ts # Session event subscription/dispatch ├── pi-embedded-subscribe.types.ts # SubscribeEmbeddedPiSessionParams ├── pi-embedded-subscribe.handlers.ts # Event handler factory ├── pi-embedded-subscribe.handlers.lifecycle.ts ├── pi-embedded-subscribe.handlers.types.ts ├── pi-embedded-block-chunker.ts # Streaming block reply chunking ├── pi-embedded-messaging.ts # Messaging tool sent tracking ├── pi-embedded-helpers.ts # Error classification, turn validation ├── pi-embedded-helpers/ # Helper modules ├── pi-embedded-utils.ts # Formatting utilities ├── pi-tools.ts # createOpenClawCodingTools() ├── pi-tools.abort.ts # AbortSignal wrapping for tools ├── pi-tools.policy.ts # Tool allowlist/denylist policy ├── pi-tools.read.ts # Read tool customizations ├── pi-tools.schema.ts # Tool schema normalization ├── pi-tools.types.ts # AnyAgentTool type alias ├── pi-tool-definition-adapter.ts # AgentTool -> ToolDefinition adapter ├── pi-settings.ts # Settings overrides ├── pi-extensions/ # Custom pi extensions │ ├── compaction-safeguard.ts # Safeguard extension │ ├── compaction-safeguard-runtime.ts │ ├── context-pruning.ts # Cache-TTL context pruning extension │ └── context-pruning/ ├── model-auth.ts # Auth profile resolution ├── auth-profiles.ts # Profile store, cooldown, failover ├── model-selection.ts # Default model resolution ├── models-config.ts # models.json generation ├── model-catalog.ts # Model catalog cache ├── context-window-guard.ts # Context window validation ├── failover-error.ts # FailoverError class ├── defaults.ts # DEFAULT_PROVIDER, DEFAULT_MODEL ├── system-prompt.ts # buildAgentSystemPrompt() ├── system-prompt-params.ts # System prompt parameter resolution ├── system-prompt-report.ts # Debug report generation ├── tool-summaries.ts # Tool description summaries ├── tool-policy.ts # Tool policy resolution ├── transcript-policy.ts # Transcript validation policy ├── skills.ts # Skill snapshot/prompt building ├── skills/ # Skill subsystem ├── sandbox.ts # Sandbox context resolution ├── sandbox/ # Sandbox subsystem ├── channel-tools.ts # Channel-specific tool injection ├── openclaw-tools.ts # OpenClaw-specific tools ├── bash-tools.ts # exec/process tools ├── apply-patch.ts # apply_patch tool (OpenAI) ├── tools/ # Individual tool implementations │ ├── browser-tool.ts │ ├── canvas-tool.ts │ ├── cron-tool.ts │ ├── discord-actions*.ts │ ├── gateway-tool.ts │ ├── image-tool.ts │ ├── message-tool.ts │ ├── nodes-tool.ts │ ├── session*.ts │ ├── slack-actions.ts │ ├── telegram-actions.ts │ ├── web-*.ts │ └── whatsapp-actions.ts └── ... ``` ## Core Integration Flow ### 1. Running an Embedded Agent The main entry point is `runEmbeddedPiAgent()` in `pi-embedded-runner/run.ts`: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} import { runEmbeddedPiAgent } from "./agents/pi-embedded-runner.js"; const result = await runEmbeddedPiAgent({ sessionId: "user-123", sessionKey: "main:whatsapp:+1234567890", sessionFile: "/path/to/session.jsonl", workspaceDir: "/path/to/workspace", config: openclawConfig, prompt: "Hello, how are you?", provider: "anthropic", model: "claude-sonnet-4-20250514", timeoutMs: 120_000, runId: "run-abc", onBlockReply: async (payload) => { await sendToChannel(payload.text, payload.mediaUrls); }, }); ``` ### 2. Session Creation Inside `runEmbeddedAttempt()` (called by `runEmbeddedPiAgent()`), the pi SDK is used: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} import { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager, } from "@mariozechner/pi-coding-agent"; const resourceLoader = new DefaultResourceLoader({ cwd: resolvedWorkspace, agentDir, settingsManager, additionalExtensionPaths, }); await resourceLoader.reload(); const { session } = await createAgentSession({ cwd: resolvedWorkspace, agentDir, authStorage: params.authStorage, modelRegistry: params.modelRegistry, model: params.model, thinkingLevel: mapThinkingLevel(params.thinkLevel), tools: builtInTools, customTools: allCustomTools, sessionManager, settingsManager, resourceLoader, }); applySystemPromptOverrideToSession(session, systemPromptOverride); ``` ### 3. Event Subscription `subscribeEmbeddedPiSession()` subscribes to pi's `AgentSession` events: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} const subscription = subscribeEmbeddedPiSession({ session: activeSession, runId: params.runId, verboseLevel: params.verboseLevel, reasoningMode: params.reasoningLevel, toolResultFormat: params.toolResultFormat, onToolResult: params.onToolResult, onReasoningStream: params.onReasoningStream, onBlockReply: params.onBlockReply, onPartialReply: params.onPartialReply, onAgentEvent: params.onAgentEvent, }); ``` Events handled include: * `message_start` / `message_end` / `message_update` (streaming text/thinking) * `tool_execution_start` / `tool_execution_update` / `tool_execution_end` * `turn_start` / `turn_end` * `agent_start` / `agent_end` * `auto_compaction_start` / `auto_compaction_end` ### 4. Prompting After setup, the session is prompted: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} await session.prompt(effectivePrompt, { images: imageResult.images }); ``` The SDK handles the full agent loop: sending to LLM, executing tool calls, streaming responses. Image injection is prompt-local: OpenClaw loads image refs from the current prompt and passes them via `images` for that turn only. It does not re-scan older history turns to re-inject image payloads. ## Tool Architecture ### Tool Pipeline 1. **Base Tools**: pi's `codingTools` (read, bash, edit, write) 2. **Custom Replacements**: OpenClaw replaces bash with `exec`/`process`, customizes read/edit/write for sandbox 3. **OpenClaw Tools**: messaging, browser, canvas, sessions, cron, gateway, etc. 4. **Channel Tools**: Discord/Telegram/Slack/WhatsApp-specific action tools 5. **Policy Filtering**: Tools filtered by profile, provider, agent, group, sandbox policies 6. **Schema Normalization**: Schemas cleaned for Gemini/OpenAI quirks 7. **AbortSignal Wrapping**: Tools wrapped to respect abort signals ### Tool Definition Adapter pi-agent-core's `AgentTool` has a different `execute` signature than pi-coding-agent's `ToolDefinition`. The adapter in `pi-tool-definition-adapter.ts` bridges this: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] { return tools.map((tool) => ({ name: tool.name, label: tool.label ?? name, description: tool.description ?? "", parameters: tool.parameters, execute: async (toolCallId, params, onUpdate, _ctx, signal) => { // pi-coding-agent signature differs from pi-agent-core return await tool.execute(toolCallId, params, signal, onUpdate); }, })); } ``` ### Tool Split Strategy `splitSdkTools()` passes all tools via `customTools`: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} export function splitSdkTools(options: { tools: AnyAgentTool[]; sandboxEnabled: boolean }) { return { builtInTools: [], // Empty. We override everything customTools: toToolDefinitions(options.tools), }; } ``` This ensures OpenClaw's policy filtering, sandbox integration, and extended toolset remain consistent across providers. ## System Prompt Construction The system prompt is built in `buildAgentSystemPrompt()` (`system-prompt.ts`). It assembles a full prompt with sections including Tooling, Tool Call Style, Safety guardrails, OpenClaw CLI reference, Skills, Docs, Workspace, Sandbox, Messaging, Reply Tags, Voice, Silent Replies, Heartbeats, Runtime metadata, plus Memory and Reactions when enabled, and optional context files and extra system prompt content. Sections are trimmed for minimal prompt mode used by subagents. The prompt is applied after session creation via `applySystemPromptOverrideToSession()`: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} const systemPromptOverride = createSystemPromptOverride(appendPrompt); applySystemPromptOverrideToSession(session, systemPromptOverride); ``` ## Session Management ### Session Files Sessions are JSONL files with tree structure (id/parentId linking). Pi's `SessionManager` handles persistence: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} const sessionManager = SessionManager.open(params.sessionFile); ``` OpenClaw wraps this with `guardSessionManager()` for tool result safety. ### Session Caching `session-manager-cache.ts` caches SessionManager instances to avoid repeated file parsing: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} await prewarmSessionFile(params.sessionFile); sessionManager = SessionManager.open(params.sessionFile); trackSessionManagerAccess(params.sessionFile); ``` ### History Limiting `limitHistoryTurns()` trims conversation history based on channel type (DM vs group). ### Compaction Auto-compaction triggers on context overflow. `compactEmbeddedPiSessionDirect()` handles manual compaction: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} const compactResult = await compactEmbeddedPiSessionDirect({ sessionId, sessionFile, provider, model, ... }); ``` ## Authentication & Model Resolution ### Auth Profiles OpenClaw maintains an auth profile store with multiple API keys per provider: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} const authStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); const profileOrder = resolveAuthProfileOrder({ cfg, store: authStore, provider, preferredProfile }); ``` Profiles rotate on failures with cooldown tracking: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} await markAuthProfileFailure({ store, profileId, reason, cfg, agentDir }); const rotated = await advanceAuthProfile(); ``` ### Model Resolution ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} import { resolveModel } from "./pi-embedded-runner/model.js"; const { model, error, authStorage, modelRegistry } = resolveModel( provider, modelId, agentDir, config, ); // Uses pi's ModelRegistry and AuthStorage authStorage.setRuntimeApiKey(model.provider, apiKeyInfo.apiKey); ``` ### Failover `FailoverError` triggers model fallback when configured: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} if (fallbackConfigured && isFailoverErrorMessage(errorText)) { throw new FailoverError(errorText, { reason: promptFailoverReason ?? "unknown", provider, model: modelId, profileId, status: resolveFailoverStatus(promptFailoverReason), }); } ``` ## Pi Extensions OpenClaw loads custom pi extensions for specialized behavior: ### Compaction Safeguard `src/agents/pi-extensions/compaction-safeguard.ts` adds guardrails to compaction, including adaptive token budgeting plus tool failure and file operation summaries: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} if (resolveCompactionMode(params.cfg) === "safeguard") { setCompactionSafeguardRuntime(params.sessionManager, { maxHistoryShare }); paths.push(resolvePiExtensionPath("compaction-safeguard")); } ``` ### Context Pruning `src/agents/pi-extensions/context-pruning.ts` implements cache-TTL based context pruning: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} if (cfg?.agents?.defaults?.contextPruning?.mode === "cache-ttl") { setContextPruningRuntime(params.sessionManager, { settings, contextWindowTokens, isToolPrunable, lastCacheTouchAt, }); paths.push(resolvePiExtensionPath("context-pruning")); } ``` ## Streaming & Block Replies ### Block Chunking `EmbeddedBlockChunker` manages streaming text into discrete reply blocks: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} const blockChunker = blockChunking ? new EmbeddedBlockChunker(blockChunking) : null; ``` ### Thinking/Final Tag Stripping Streaming output is processed to strip ``/`` blocks and extract `` content: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} const stripBlockTags = (text: string, state: { thinking: boolean; final: boolean }) => { // Strip ... content // If enforceFinalTag, only return ... content }; ``` ### Reply Directives Reply directives like `[[media:url]]`, `[[voice]]`, `[[reply:id]]` are parsed and extracted: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} const { text: cleanedText, mediaUrls, audioAsVoice, replyToId } = consumeReplyDirectives(chunk); ``` ## Error Handling ### Error Classification `pi-embedded-helpers.ts` classifies errors for appropriate handling: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} isContextOverflowError(errorText) // Context too large isCompactionFailureError(errorText) // Compaction failed isAuthAssistantError(lastAssistant) // Auth failure isRateLimitAssistantError(...) // Rate limited isFailoverAssistantError(...) // Should failover classifyFailoverReason(errorText) // "auth" | "rate_limit" | "quota" | "timeout" | ... ``` ### Thinking Level Fallback If a thinking level is unsupported, it falls back: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} const fallbackThinking = pickFallbackThinkingLevel({ message: errorText, attempted: attemptedThinking, }); if (fallbackThinking) { thinkLevel = fallbackThinking; continue; } ``` ## Sandbox Integration When sandbox mode is enabled, tools and paths are constrained: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} const sandbox = await resolveSandboxContext({ config: params.config, sessionKey: sandboxSessionKey, workspaceDir: resolvedWorkspace, }); if (sandboxRoot) { // Use sandboxed read/edit/write tools // Exec runs in container // Browser uses bridge URL } ``` ## Provider-Specific Handling ### Anthropic * Refusal magic string scrubbing * Turn validation for consecutive roles * Claude Code parameter compatibility ### Google/Gemini * Turn ordering fixes (`applyGoogleTurnOrderingFix`) * Tool schema sanitization (`sanitizeToolsForGoogle`) * Session history sanitization (`sanitizeSessionHistory`) ### OpenAI * `apply_patch` tool for Codex models * Thinking level downgrade handling ## TUI Integration OpenClaw also has a local TUI mode that uses pi-tui components directly: ```typescript theme={"theme":{"light":"min-light","dark":"min-dark"}} // src/tui/tui.ts import { ... } from "@mariozechner/pi-tui"; ``` This provides the interactive terminal experience similar to pi's native mode. ## Key Differences from Pi CLI | Aspect | Pi CLI | OpenClaw Embedded | | --------------- | ----------------------- | ---------------------------------------------------------------------------------------------- | | Invocation | `pi` command / RPC | SDK via `createAgentSession()` | | Tools | Default coding tools | Custom OpenClaw tool suite | | System prompt | AGENTS.md + prompts | Dynamic per-channel/context | | Session storage | `~/.pi/agent/sessions/` | `~/.openclaw/agents//sessions/` (or `$OPENCLAW_STATE_DIR/agents//sessions/`) | | Auth | Single credential | Multi-profile with rotation | | Extensions | Loaded from disk | Programmatic + disk paths | | Event handling | TUI rendering | Callback-based (onBlockReply, etc.) | ## Future Considerations Areas for potential rework: 1. **Tool signature alignment**: Currently adapting between pi-agent-core and pi-coding-agent signatures 2. **Session manager wrapping**: `guardSessionManager` adds safety but increases complexity 3. **Extension loading**: Could use pi's `ResourceLoader` more directly 4. **Streaming handler complexity**: `subscribeEmbeddedPiSession` has grown large 5. **Provider quirks**: Many provider-specific codepaths that pi could potentially handle ## Tests Pi integration coverage spans these suites: * `src/agents/pi-*.test.ts` * `src/agents/pi-auth-json.test.ts` * `src/agents/pi-embedded-*.test.ts` * `src/agents/pi-embedded-helpers*.test.ts` * `src/agents/pi-embedded-runner*.test.ts` * `src/agents/pi-embedded-runner/**/*.test.ts` * `src/agents/pi-embedded-subscribe*.test.ts` * `src/agents/pi-tools*.test.ts` * `src/agents/pi-tool-definition-adapter*.test.ts` * `src/agents/pi-settings.test.ts` * `src/agents/pi-extensions/**/*.test.ts` Live/opt-in: * `src/agents/pi-embedded-runner-extraparams.live.test.ts` (enable `OPENCLAW_LIVE_TEST=1`) For current run commands, see [Pi Development Workflow](/pi-dev). # Agent Bootstrapping Source: https://cryptoclawdocs.termix.ai/start/bootstrapping # Agent Bootstrapping Bootstrapping is the **first‑run** ritual that prepares an agent workspace and collects identity details. It happens after onboarding, when the agent starts for the first time. ## What bootstrapping does On the first agent run, OpenClaw bootstraps the workspace (default `~/.openclaw/workspace`): * Seeds `AGENTS.md`, `BOOTSTRAP.md`, `IDENTITY.md`, `USER.md`. * Runs a short Q\&A ritual (one question at a time). * Writes identity + preferences to `IDENTITY.md`, `USER.md`, `SOUL.md`. * Removes `BOOTSTRAP.md` when finished so it only runs once. ## Where it runs Bootstrapping always runs on the **gateway host**. If the macOS app connects to a remote Gateway, the workspace and bootstrapping files live on that remote machine. When the Gateway runs on another machine, edit workspace files on the gateway host (for example, `user@gateway-host:~/.openclaw/workspace`). ## Related docs * macOS app onboarding: [Onboarding](/start/onboarding) * Workspace layout: [Agent workspace](/concepts/agent-workspace) # Getting Started Source: https://cryptoclawdocs.termix.ai/start/getting-started # Getting Started Goal: go from zero to a first working chat with minimal setup. Fastest chat: open the Control UI (no channel setup needed). Run `openclaw dashboard` and chat in the browser, or open `http://127.0.0.1:18789/` on the gateway host. Docs: [Dashboard](/web/dashboard) and [Control UI](/web/control-ui). ## Prereqs * Node 22 or newer Check your Node version with `node --version` if you are unsure. ## Quick setup (CLI) ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} curl -fsSL https://openclaw.ai/install.sh | bash ``` Install Script Process ```powershell theme={"theme":{"light":"min-light","dark":"min-dark"}} iwr -useb https://openclaw.ai/install.ps1 | iex ``` Other install methods and requirements: [Install](/install). ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw onboard --install-daemon ``` The wizard configures auth, gateway settings, and optional channels. See [Onboarding Wizard](/start/wizard) for details. If you installed the service, it should already be running: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway status ``` ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw dashboard ``` If the Control UI loads, your Gateway is ready for use. ## Optional checks and extras Useful for quick tests or troubleshooting. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway --port 18789 ``` Requires a configured channel. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw message send --target +15555550123 --message "Hello from OpenClaw" ``` ## Useful environment variables If you run OpenClaw as a service account or want custom config/state locations: * `OPENCLAW_HOME` sets the home directory used for internal path resolution. * `OPENCLAW_STATE_DIR` overrides the state directory. * `OPENCLAW_CONFIG_PATH` overrides the config file path. Full environment variable reference: [Environment vars](/help/environment). ## Go deeper Full CLI wizard reference and advanced options. First run flow for the macOS app. ## What you will have * A running Gateway * Auth configured * Control UI access or a connected channel ## Next steps * DM safety and approvals: [Pairing](/channels/pairing) * Connect more channels: [Channels](/channels) * Advanced workflows and from source: [Setup](/start/setup) # Onboarding (macOS App) Source: https://cryptoclawdocs.termix.ai/start/onboarding # Onboarding (macOS App) This doc describes the **current** first‑run onboarding flow. The goal is a smooth “day 0” experience: pick where the Gateway runs, connect auth, run the wizard, and let the agent bootstrap itself. For a general overview of onboarding paths, see [Onboarding Overview](/start/onboarding-overview). Security trust model: * By default, OpenClaw is a personal agent: one trusted operator boundary. * Shared/multi-user setups require lock-down (split trust boundaries, keep tool access minimal, and follow [Security](/gateway/security)). * Local onboarding now defaults new configs to `tools.profile: "messaging"` so broad runtime/filesystem tools are opt-in. * If hooks/webhooks or other untrusted content feeds are enabled, use a strong modern model tier and keep strict tool policy/sandboxing. Where does the **Gateway** run? * **This Mac (Local only):** onboarding can configure auth and write credentials locally. * **Remote (over SSH/Tailnet):** onboarding does **not** configure local auth; credentials must exist on the gateway host. * **Configure later:** skip setup and leave the app unconfigured. **Gateway auth tip:** * The wizard now generates a **token** even for loopback, so local WS clients must authenticate. * If you disable auth, any local process can connect; use that only on fully trusted machines. * Use a **token** for multi‑machine access or non‑loopback binds. Onboarding requests TCC permissions needed for: * Automation (AppleScript) * Notifications * Accessibility * Screen Recording * Microphone * Speech Recognition * Camera * Location This step is optional The app can install the global `openclaw` CLI via npm/pnpm so terminal workflows and launchd tasks work out of the box. After setup, the app opens a dedicated onboarding chat session so the agent can introduce itself and guide next steps. This keeps first‑run guidance separate from your normal conversation. See [Bootstrapping](/start/bootstrapping) for what happens on the gateway host during the first agent run. # Onboarding Overview Source: https://cryptoclawdocs.termix.ai/start/onboarding-overview # Onboarding Overview OpenClaw supports multiple onboarding paths depending on where the Gateway runs and how you prefer to configure providers. ## Choose your onboarding path * **CLI wizard** for macOS, Linux, and Windows (via WSL2). * **macOS app** for a guided first run on Apple silicon or Intel Macs. ## CLI onboarding wizard Run the wizard in a terminal: ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw onboard ``` Use the CLI wizard when you want full control of the Gateway, workspace, channels, and skills. Docs: * [Onboarding Wizard (CLI)](/start/wizard) * [`openclaw onboard` command](/cli/onboard) ## macOS app onboarding Use the OpenClaw app when you want a fully guided setup on macOS. Docs: * [Onboarding (macOS App)](/start/onboarding) ## Custom Provider If you need an endpoint that is not listed, including hosted providers that expose standard OpenAI or Anthropic APIs, choose **Custom Provider** in the CLI wizard. You will be asked to: * Pick OpenAI-compatible, Anthropic-compatible, or **Unknown** (auto-detect). * Enter a base URL and API key (if required by the provider). * Provide a model ID and optional alias. * Choose an Endpoint ID so multiple custom endpoints can coexist. For detailed steps, follow the CLI onboarding docs above. # Personal Assistant Setup Source: https://cryptoclawdocs.termix.ai/start/openclaw # Building a personal assistant with OpenClaw OpenClaw is a WhatsApp + Telegram + Discord + iMessage gateway for **Pi** agents. Plugins add Mattermost. This guide is the "personal assistant" setup: one dedicated WhatsApp number that behaves like your always-on agent. ## ⚠️ Safety first You’re putting an agent in a position to: * run commands on your machine (depending on your Pi tool setup) * read/write files in your workspace * send messages back out via WhatsApp/Telegram/Discord/Mattermost (plugin) Start conservative: * Always set `channels.whatsapp.allowFrom` (never run open-to-the-world on your personal Mac). * Use a dedicated WhatsApp number for the assistant. * Heartbeats now default to every 30 minutes. Disable until you trust the setup by setting `agents.defaults.heartbeat.every: "0m"`. ## Prerequisites * OpenClaw installed and onboarded — see [Getting Started](/start/getting-started) if you haven't done this yet * A second phone number (SIM/eSIM/prepaid) for the assistant ## The two-phone setup (recommended) You want this: ```mermaid theme={"theme":{"light":"min-light","dark":"min-dark"}} flowchart TB A["Your Phone (personal)

Your WhatsApp
+1-555-YOU"] -- message --> B["Second Phone (assistant)

Assistant WA
+1-555-ASSIST"] B -- linked via QR --> C["Your Mac (openclaw)

Pi agent"] ``` If you link your personal WhatsApp to OpenClaw, every message to you becomes “agent input”. That’s rarely what you want. ## 5-minute quick start 1. Pair WhatsApp Web (shows QR; scan with the assistant phone): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw channels login ``` 2. Start the Gateway (leave it running): ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw gateway --port 18789 ``` 3. Put a minimal config in `~/.openclaw/openclaw.json`: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { channels: { whatsapp: { allowFrom: ["+15555550123"] } }, } ``` Now message the assistant number from your allowlisted phone. When onboarding finishes, we auto-open the dashboard and print a clean (non-tokenized) link. If it prompts for auth, paste the token from `gateway.auth.token` into Control UI settings. To reopen later: `openclaw dashboard`. ## Give the agent a workspace (AGENTS) OpenClaw reads operating instructions and “memory” from its workspace directory. By default, OpenClaw uses `~/.openclaw/workspace` as the agent workspace, and will create it (plus starter `AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`) automatically on setup/first agent run. `BOOTSTRAP.md` is only created when the workspace is brand new (it should not come back after you delete it). `MEMORY.md` is optional (not auto-created); when present, it is loaded for normal sessions. Subagent sessions only inject `AGENTS.md` and `TOOLS.md`. Tip: treat this folder like OpenClaw’s “memory” and make it a git repo (ideally private) so your `AGENTS.md` + memory files are backed up. If git is installed, brand-new workspaces are auto-initialized. ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw setup ``` Full workspace layout + backup guide: [Agent workspace](/concepts/agent-workspace) Memory workflow: [Memory](/concepts/memory) Optional: choose a different workspace with `agents.defaults.workspace` (supports `~`). ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agent: { workspace: "~/.openclaw/workspace", }, } ``` If you already ship your own workspace files from a repo, you can disable bootstrap file creation entirely: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agent: { skipBootstrap: true, }, } ``` ## The config that turns it into “an assistant” OpenClaw defaults to a good assistant setup, but you’ll usually want to tune: * persona/instructions in `SOUL.md` * thinking defaults (if desired) * heartbeats (once you trust it) Example: ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { logging: { level: "info" }, agent: { model: "anthropic/claude-opus-4-6", workspace: "~/.openclaw/workspace", thinkingDefault: "high", timeoutSeconds: 1800, // Start with 0; enable later. heartbeat: { every: "0m" }, }, channels: { whatsapp: { allowFrom: ["+15555550123"], groups: { "*": { requireMention: true }, }, }, }, routing: { groupChat: { mentionPatterns: ["@openclaw", "openclaw"], }, }, session: { scope: "per-sender", resetTriggers: ["/new", "/reset"], reset: { mode: "daily", atHour: 4, idleMinutes: 10080, }, }, } ``` ## Sessions and memory * Session files: `~/.openclaw/agents//sessions/{{SessionId}}.jsonl` * Session metadata (token usage, last route, etc): `~/.openclaw/agents//sessions/sessions.json` (legacy: `~/.openclaw/sessions/sessions.json`) * `/new` or `/reset` starts a fresh session for that chat (configurable via `resetTriggers`). If sent alone, the agent replies with a short hello to confirm the reset. * `/compact [instructions]` compacts the session context and reports the remaining context budget. ## Heartbeats (proactive mode) By default, OpenClaw runs a heartbeat every 30 minutes with the prompt: `Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.` Set `agents.defaults.heartbeat.every: "0m"` to disable. * If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), OpenClaw skips the heartbeat run to save API calls. * If the file is missing, the heartbeat still runs and the model decides what to do. * If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), OpenClaw suppresses outbound delivery for that heartbeat. * By default, heartbeat delivery to DM-style `user:` targets is allowed. Set `agents.defaults.heartbeat.directPolicy: "block"` to suppress direct-target delivery while keeping heartbeat runs active. * Heartbeats run full agent turns — shorter intervals burn more tokens. ```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}} { agent: { heartbeat: { every: "30m" }, }, } ``` ## Media in and out Inbound attachments (images/audio/docs) can be surfaced to your command via templates: * `{{MediaPath}}` (local temp file path) * `{{MediaUrl}}` (pseudo-URL) * `{{Transcript}}` (if audio transcription is enabled) Outbound attachments from the agent: include `MEDIA:` on its own line (no spaces). Example: ``` Here’s the screenshot. MEDIA:https://example.com/screenshot.png ``` OpenClaw extracts these and sends them as media alongside the text. ## Operations checklist ```bash theme={"theme":{"light":"min-light","dark":"min-dark"}} openclaw status # local status (creds, sessions, queued events) openclaw status --all # full diagnosis (read-only, pasteable) openclaw status --deep # adds gateway health probes (Telegram + Discord) openclaw health --json # gateway health snapshot (WS) ``` Logs live under `/tmp/openclaw/` (default: `openclaw-YYYY-MM-DD.log`). ## Next steps * WebChat: [WebChat](/web/webchat) * Gateway ops: [Gateway runbook](/gateway) * Cron + wakeups: [Cron jobs](/automation/cron-jobs) * macOS menu bar companion: [OpenClaw macOS app](/platforms/macos) * iOS node app: [iOS app](/platforms/ios) * Android node app: [Android app](/platforms/android) * Windows status: [Windows (WSL2)](/platforms/windows) * Linux status: [Linux app](/platforms/linux) * Security: [Security](/gateway/security) # Showcase Source: https://cryptoclawdocs.termix.ai/start/showcase Real-world OpenClaw projects from the community # Showcase Real projects from the community. See what people are building with OpenClaw. **Want to be featured?** Share your project in [#showcase on Discord](https://discord.gg/clawd) or [tag @openclaw on X](https://x.com/openclaw). ## 🎥 OpenClaw in Action Full setup walkthrough (28m) by VelvetShark.