Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
---
summary: "Reusable sender allowlists for message channels"
read_when:
- Configuring the same allowlist across multiple message channels
- Sharing DM and group sender access rules
- Reviewing message-channel access control
title: "Access groups"
---
Access groups are named sender lists you define once under `accessGroups` and reference from channel allowlists with `accessGroup:<name>`.
Use them when the same people should be allowed across several message channels, or when one trusted set should apply to both DMs and group sender authorization.
A group grants nothing by itself. It only matters where an allowlist field references it.
## Static message sender groups
Static sender groups use `type: "message.senders"`. `members` is keyed by message-channel id, plus `"*"` for entries shared by every channel:
```json5
{
accessGroups: {
operators: {
type: "message.senders",
members: {
"*": ["global-owner-id"],
discord: ["discord:123456789012345678"],
telegram: ["987654321"],
whatsapp: ["+15551234567"],
},
},
},
}
```
| Key | Meaning |
| -------------------------- | --------------------------------------------------------------------------- |
| `"*"` | Shared entries checked for every message channel that references the group. |
| `discord`, `telegram`, ... | Entries checked only for that channel's allowlist matching. |
Entries are matched with the destination channel's normal `allowFrom` rules. OpenClaw does not translate sender ids between channels: if Alice has a Telegram id and a Discord id, list both ids under the matching channel keys.
## Reference groups from allowlists
Reference a group with `accessGroup:<name>` anywhere the message channel path supports sender allowlists.
DM allowlist example:
```json5
{
accessGroups: {
operators: {
type: "message.senders",
members: {
discord: ["discord:123456789012345678"],
telegram: ["987654321"],
},
},
},
channels: {
discord: {
dmPolicy: "allowlist",
allowFrom: ["accessGroup:operators"],
},
telegram: {
dmPolicy: "allowlist",
allowFrom: ["accessGroup:operators"],
},
},
}
```
Group sender allowlist example:
```json5
{
accessGroups: {
oncall: {
type: "message.senders",
members: {
whatsapp: ["+15551234567"],
googlechat: ["users/1234567890"],
},
},
},
channels: {
whatsapp: {
groupPolicy: "allowlist",
groupAllowFrom: ["accessGroup:oncall"],
},
googlechat: {
groups: {
"spaces/AAA": {
users: ["accessGroup:oncall"],
},
},
},
},
}
```
You can mix groups and direct entries:
```json5
{
channels: {
discord: {
dmPolicy: "allowlist",
allowFrom: ["accessGroup:operators", "discord:123456789012345678"],
},
},
}
```
## Supported message-channel paths
Access groups work in the shared message-channel authorization paths:
- DM sender allowlists such as `channels.<channel>.allowFrom`
- group sender allowlists such as `channels.<channel>.groupAllowFrom`
- channel-specific per-room sender allowlists that use the same sender matching rules (for example Google Chat `groups.<space>.users`)
- command authorization paths that reuse message-channel sender allowlists
Channel support depends on whether that channel is wired through the shared OpenClaw sender-authorization helpers. Current bundled support includes ClickClack, Discord, Feishu, Google Chat, iMessage, IRC, LINE, Mattermost, Microsoft Teams, Nextcloud Talk, Nostr, QQ Bot, Signal, Slack, SMS, Telegram, WhatsApp, Zalo, and Zalo Personal. Static `message.senders` groups are channel-agnostic, so new message channels get them by using the shared plugin SDK ingress helpers instead of custom allowlist expansion.
## Discord channel audiences
Discord also supports a dynamic access group type:
```json5
{
accessGroups: {
maintainers: {
type: "discord.channelAudience",
guildId: "1456350064065904867",
channelId: "1456744319972282449",
membership: "canViewChannel",
},
},
channels: {
discord: {
dmPolicy: "allowlist",
allowFrom: ["accessGroup:maintainers"],
},
},
}
```
`discord.channelAudience` means "allow Discord DM senders who can currently view this guild channel." OpenClaw resolves the sender through Discord at authorization time and applies Discord `ViewChannel` permission rules. `membership` is optional and defaults to `canViewChannel`.
Use this when a Discord channel is already the source of truth for a team, such as `#maintainers` or `#on-call`.
Requirements and failure behavior:
- The bot needs access to the guild and channel.
- The bot needs the Discord Developer Portal **Server Members Intent**.
- The access group fails closed when Discord returns `Missing Access`, the sender cannot be resolved as a guild member, or the channel belongs to another guild.
More Discord-specific examples: [Discord access control](/channels/discord#access-control-and-routing)
## Plugin diagnostics
Plugin authors can inspect structured access-group state without expanding it back into a flat allowlist:
```typescript
import { resolveAccessGroupAllowFromState } from "openclaw/plugin-sdk/access-groups";
const state = await resolveAccessGroupAllowFromState({
accessGroups: cfg.accessGroups,
allowFrom: channelConfig.allowFrom,
channel: "my-channel",
accountId: "default",
senderId,
isSenderAllowed,
});
```
The result reports referenced, matched, missing, unsupported, and failed groups. Use it for diagnostics or conformance tests. Use `expandAllowFromWithAccessGroups(...)` only for compatibility paths that still expect a flat `allowFrom` array.
## Security notes
- Access groups are allowlist aliases, not roles. They do not create owners, approve pairing requests, or grant tool permissions by themselves.
- `dmPolicy: "open"` still requires `"*"` in the effective DM allowlist. Referencing an access group is not the same as public access.
- Missing group names fail closed. If `allowFrom` contains `accessGroup:operators` and `accessGroups.operators` is absent, that entry authorizes nobody.
- Keep channel ids stable. Prefer numeric/user ids over display names when the channel supports both.
## Troubleshooting
If a sender should match but is blocked:
1. Confirm the allowlist field contains the exact `accessGroup:<name>` reference.
2. Confirm `accessGroups.<name>.type` is correct.
3. Confirm the sender id is listed under the matching channel key, or under `"*"`.
4. Confirm the entry uses that channel's normal allowlist syntax.
5. For Discord channel audiences, confirm the bot can see the guild channel and has Server Members Intent enabled.
Run `openclaw doctor` after editing access-control config. It catches many invalid allowlist and policy combinations before runtime.

View File

@@ -0,0 +1,211 @@
---
summary: "Let supported group rooms provide quiet context unless the agent sends with the message tool"
read_when:
- Configuring always-on group or channel rooms
- You want the agent to watch room chatter without posting final text automatically
- Debugging typing and token usage with no visible room message
title: "Ambient room events"
sidebarTitle: "Ambient room events"
---
Ambient room events let OpenClaw process unmentioned group or channel chatter as quiet context. The agent can update memory and session state, but the room stays silent unless the agent explicitly calls the `message` tool.
For always-on group chats, combine `messages.groupChat.unmentionedInbound: "room_event"` with `messages.groupChat.visibleReplies: "message_tool"`. The agent listens, decides when a reply is useful, and never needs the old prompt pattern of answering `NO_REPLY`.
Supported today: Discord guild channels, Slack channels and private channels, Slack multi-person DMs, and Telegram groups or supergroups. Other group channels keep their existing group behavior unless their channel page says they support ambient room events.
## Recommended setup
Set the global group-chat behavior:
```json5
{
messages: {
groupChat: {
unmentionedInbound: "room_event",
visibleReplies: "message_tool",
historyLimit: 50,
},
},
}
```
Then make the room always-on by disabling mention gating for that room. The room must still pass its normal `groupPolicy`, room allowlist, and sender allowlist.
After saving the config, the Gateway hot-applies `messages` settings. Restart only when file watching or config reload is disabled (`gateway.reload.mode: "off"`).
## What changes
With `messages.groupChat.unmentionedInbound: "room_event"`:
- unmentioned allowed group or channel messages become quiet room events
- mentioned messages stay user requests
- text control commands and native commands stay user requests
- abort or stop requests stay user requests
- direct messages stay user requests
Room events use strict visible delivery. Final assistant text is private. The agent must call `message(action=send)` to post in the room.
## Discord example
```json5
{
messages: {
groupChat: {
unmentionedInbound: "room_event",
visibleReplies: "message_tool",
historyLimit: 50,
},
},
channels: {
discord: {
groupPolicy: "allowlist",
guilds: {
"<DISCORD_SERVER_ID>": {
requireMention: false,
users: ["<YOUR_DISCORD_USER_ID>"],
},
},
},
},
}
```
Use per-channel Discord config when only one channel should be ambient. Under `groupPolicy: "allowlist"`, listing the channel is what allows it (`enabled: false` disables an entry):
```json5
{
channels: {
discord: {
groupPolicy: "allowlist",
guilds: {
"<DISCORD_SERVER_ID>": {
channels: {
"<DISCORD_CHANNEL_ID_OR_NAME>": {
requireMention: false,
},
},
},
},
},
},
}
```
## Slack example
Slack channel allowlists are ID-first. Use channel IDs such as `C12345678`, not `#channel-name`. Listing the channel under `channels.slack.channels` is what allows it (`enabled: false` disables an entry):
```json5
{
messages: {
groupChat: {
unmentionedInbound: "room_event",
visibleReplies: "message_tool",
historyLimit: 50,
},
},
channels: {
slack: {
groupPolicy: "allowlist",
channels: {
"<SLACK_CHANNEL_ID>": {
requireMention: false,
},
},
},
},
}
```
## Telegram example
For Telegram groups, the bot must be able to see normal group messages. If `requireMention: false`, disable BotFather privacy mode or use another Telegram setup that delivers full group traffic to the bot.
```json5
{
messages: {
groupChat: {
unmentionedInbound: "room_event",
visibleReplies: "message_tool",
historyLimit: 50,
},
},
channels: {
telegram: {
groups: {
"<TELEGRAM_GROUP_CHAT_ID>": {
groupPolicy: "open",
requireMention: false,
},
},
},
},
}
```
Telegram group IDs are usually negative numbers such as `-1001234567890`. Read `chat.id` from `openclaw logs --follow`, forward a group message to an ID helper bot, or inspect Bot API `getUpdates`.
## Agent specific policy
Use an agent override when several agents share the same room but only one should treat unmentioned chatter as ambient context:
```json5
{
messages: {
groupChat: {
visibleReplies: "message_tool",
},
},
agents: {
list: [
{
id: "main",
groupChat: {
unmentionedInbound: "room_event",
mentionPatterns: ["@openclaw", "openclaw"],
},
},
],
},
}
```
The agent-specific `agents.list[].groupChat.unmentionedInbound` value overrides `messages.groupChat.unmentionedInbound` for that agent.
## Visible reply modes
`messages.groupChat.visibleReplies` defaults to `"automatic"` for normal group/channel user requests. Keep that default when final assistant text should post visibly without an explicit message-tool call.
For ambient always-on rooms, `messages.groupChat.visibleReplies: "message_tool"` is still recommended, especially with latest-generation, tool-reliable models such as GPT 5.5. It lets the agent decide when to speak by calling the message tool. If the model returns final text without calling the tool, OpenClaw keeps that final text private and logs suppressed-delivery metadata.
Room events stay strict even when other group requests use automatic replies. Unmentioned ambient room events always require `message(action=send)` for visible output.
## History
`messages.groupChat.historyLimit` sets the global group history default (50 when unset; must be a positive integer). Channels can override it with `channels.<channel>.historyLimit`, and some channels also support per-account history limits. Set the channel-level `historyLimit: 0` to disable group history context for that channel.
Supported room-event channels keep recent ambient room messages as context. Telegram keeps an always-on rolling per-group window bounded by `historyLimit`; user-request turns select entries after the bot's last recorded reply, while room-event turns receive the full recent window so the model can see its own recent posts. The retired Telegram `includeGroupHistoryContext` mode key is removed by `openclaw doctor --fix`.
## Troubleshooting
If the room shows typing or token usage but no visible message:
1. Confirm the room is allowed by the channel allowlist and sender allowlist.
2. Confirm `requireMention: false` is set at the room level you expect.
3. Check whether `messages.groupChat.unmentionedInbound` or the agent override is `"room_event"`.
4. Inspect logs for suppressed final payload metadata or `didSendViaMessagingTool: false`.
5. For normal group requests, keep or restore `messages.groupChat.visibleReplies: "automatic"` if you want final replies posted automatically. For ambient rooms using `message_tool`, use a model/runtime that reliably calls tools.
If Telegram ambient rooms do not trigger at all, check BotFather privacy mode and verify the Gateway is receiving normal group messages.
If Slack ambient rooms do not trigger, verify the channel key is the Slack channel ID and the app has the history scope for that room type: `channels:history` (public), `groups:history` (private), or `mpim:history` (multi-person DMs).
## Related
- [Groups](/channels/groups)
- [Discord](/channels/discord)
- [Slack](/channels/slack)
- [Telegram](/channels/telegram)
- [Channel troubleshooting](/channels/troubleshooting)
- [Channel configuration reference](/gateway/config-channels)

View File

@@ -0,0 +1,118 @@
---
summary: "Bot-to-bot loop protection defaults and channel overrides"
read_when:
- Configuring bot-authored channel messages
- Tuning bot-to-bot loop protection
title: "Bot loop protection"
sidebarTitle: "Bot loop protection"
---
OpenClaw can accept messages written by other bots on channels that support `allowBots`. When that path is enabled, pair loop protection prevents two bot identities from replying to each other indefinitely.
The guard is enforced by the core inbound reply runner. Each supporting channel maps its inbound event into generic facts: account or scope, conversation id, sender bot id, and receiver bot id. Core tracks the participant pair in both directions (A to B and B to A count as the same pair), applies a sliding-window budget, and suppresses the pair during a cooldown after the budget is exceeded.
## Defaults
Pair loop protection is active whenever a channel lets bot-authored messages reach dispatch. Built-in defaults:
| Key | Default | Meaning |
| -------------------- | ------- | --------------------------------------------------- |
| `enabled` | `true` | Guard active for channels that support it. |
| `maxEventsPerWindow` | `20` | Events a bot pair can exchange within the window. |
| `windowSeconds` | `60` | Sliding window length. |
| `cooldownSeconds` | `60` | Suppression time after the pair exceeds the budget. |
The guard does not affect human-authored messages, single-bot deployments, self-message filtering, or bot replies that stay under the budget.
## Configure shared defaults
Set `channels.defaults.botLoopProtection` once to give every supporting channel the same baseline. Channel, account, and room overrides can still tune individual surfaces.
```json5
{
channels: {
defaults: {
botLoopProtection: {
maxEventsPerWindow: 20,
windowSeconds: 60,
cooldownSeconds: 60,
},
},
},
}
```
Set `enabled: false` only when your channel policy intentionally allows bot-to-bot conversations without automatic suppression.
## Override per channel, account, or room
Supporting channels layer their own config over the shared default, key by key. Precedence, narrowest first:
1. `channels.<channel>.<room-or-space>.botLoopProtection`, when the channel supports per-conversation overrides
2. `channels.<channel>.accounts.<account>.botLoopProtection`, when the channel supports accounts
3. `channels.<channel>.botLoopProtection`, when the channel supports top-level defaults
4. `channels.defaults.botLoopProtection`
5. built-in defaults
```json5
{
channels: {
defaults: {
botLoopProtection: {
maxEventsPerWindow: 20,
},
},
discord: {
botLoopProtection: {
maxEventsPerWindow: 8,
},
accounts: {
secondary: {
allowBots: "mentions",
botLoopProtection: {
maxEventsPerWindow: 5,
cooldownSeconds: 90,
},
},
},
},
googlechat: {
allowBots: true,
groups: {
"spaces/AAAA": {
botLoopProtection: {
maxEventsPerWindow: 5,
},
},
},
},
matrix: {
allowBots: "mentions",
groups: {
"!roomid:example.org": {
botLoopProtection: {
maxEventsPerWindow: 5,
},
},
},
},
slack: {
allowBots: "mentions",
botLoopProtection: {
maxEventsPerWindow: 8,
},
},
},
}
```
## Channel support
- Discord: native `author.bot` facts, keyed by Discord account, channel, and bot pair.
- Google Chat: native `sender.type=BOT` facts for accepted bot-authored messages, keyed by account, space, and bot pair.
- Matrix: configured Matrix bot accounts, keyed by Matrix account, room, and configured bot pair.
- Slack: native `bot_id` facts for accepted bot-authored messages, keyed by Slack account, channel, and bot pair.
Channels that do not expose a reliable inbound bot identity keep using their normal self-message and access-policy filters. They should not opt into this guard until they can identify both participants in the bot pair.
See [SDK runtime](/plugins/sdk-runtime#reusable-runtime-utilities) for plugin implementation details.

View File

@@ -0,0 +1,369 @@
---
summary: "Broadcast a WhatsApp message to multiple agents"
read_when:
- Configuring broadcast groups
- Debugging multi-agent replies in WhatsApp
status: experimental
title: "Broadcast groups"
sidebarTitle: "Broadcast groups"
---
<Note>
**Status:** Experimental. Added in 2026.1.9. WhatsApp (web channel) only.
</Note>
## Overview
Broadcast groups run **multiple agents** on the same inbound message. Each agent processes the message in its own isolated session and posts its own reply, so one WhatsApp number can host a team of specialized agents in a single group chat or DM.
Broadcast groups are evaluated after channel allowlists and group activation rules. In WhatsApp groups, broadcasts happen when OpenClaw would normally reply (for example: on mention, depending on your group settings). They only change **which agents run**, never whether a message is eligible for processing.
The live WhatsApp QA lane includes `whatsapp-broadcast-group-fanout`, which verifies that one mentioned group message can produce distinct visible replies from two configured agents.
## Configuration
### Basic setup
Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer ids, values are arrays of agent ids:
- group chats: group JID (e.g. `120363403215116621@g.us`)
- DMs: sender E.164 phone number (e.g. `+15551234567`)
```json
{
"broadcast": {
"120363403215116621@g.us": ["alfred", "baerbel", "assistant3"]
}
}
```
**Result:** when OpenClaw would reply in this chat, it runs all three agents.
Every listed agent id must exist in `agents.list`: config validation reports unknown ids, and the runtime skips them with a `Broadcast agent <id> not found in agents.list; skipping` warning.
### Processing strategy
`broadcast.strategy` sets how agents process the message:
| Strategy | Behavior |
| -------------------- | --------------------------------------------------------------------- |
| `parallel` (default) | All agents process simultaneously; replies arrive in any order. |
| `sequential` | Agents process in array order; each waits for the previous to finish. |
```json
{
"broadcast": {
"strategy": "sequential",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
```
### Complete example
```json
{
"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
<Steps>
<Step title="Incoming message arrives">
A WhatsApp group or DM message arrives.
</Step>
<Step title="Route and admission">
OpenClaw applies channel allowlists, group activation rules, and configured ACP binding ownership.
</Step>
<Step title="Broadcast check">
If no configured ACP binding owns the route, OpenClaw checks whether the peer ID is in `broadcast`.
</Step>
<Step title="If broadcast applies">
- All listed agents process the message.
- Each agent has its own session key and isolated context.
- Agents process in parallel (default) or sequentially.
- Audio attachments are transcribed once before fan-out, so agents share one transcript instead of making separate STT calls.
</Step>
<Step title="If broadcast does not apply">
OpenClaw dispatches the ordinary route or the configured ACP session route selected during routing.
</Step>
</Steps>
<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.
</Note>
### 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** (an agent does not see other agents' replies)
- **Workspace** (separate sandboxes if configured)
- **Tool access** (different allow/deny lists)
- **Memory/context** (separate `IDENTITY.md`, `SOUL.md`, etc.)
One exception is shared on purpose: the **group context buffer** (recent group messages used for context) is shared per peer, so all broadcast agents see the same context when triggered. It is cleared once after the fan-out completes.
This allows each agent to have different personalities, models, skills, and tool access (for example read-only vs. read-write).
### Example: isolated sessions
In group `120363403215116621@g.us` with agents `["alfred", "baerbel"]`:
<Tabs>
<Tab title="Alfred's context">
```text
Session: agent:alfred:whatsapp:group:120363403215116621@g.us
History: [user message, alfred's previous responses]
Workspace: ~/openclaw-alfred/
Tools: read, write, exec
```
</Tab>
<Tab title="Baerbel's context">
```text
Session: agent:baerbel:whatsapp:group:120363403215116621@g.us
History: [user message, baerbel's previous responses]
Workspace: ~/openclaw-baerbel/
Tools: read only
```
</Tab>
</Tabs>
## Use cases
- **Specialized agent teams**: a dev group where `code-reviewer`, `security-auditor`, `test-generator`, and `docs-checker` each answer the same message from their own angle.
- **Multi-language support**: one support chat with `support-en`, `support-de`, `support-es` responding in their languages.
- **Quality assurance**: `support-agent` answers while `qa-agent` reviews and only responds when it finds issues.
- **Task automation**: `task-tracker`, `time-logger`, and `report-generator` all consume the same status update.
## Best practices
<AccordionGroup>
<Accordion title="1. Keep agents focused">
Give each agent a single, clear responsibility (`formatter`, `linter`, `tester`) instead of one generic "dev-helper" agent.
</Accordion>
<Accordion title="2. Use descriptive ids and names">
```json
{
"agents": {
"list": [
{ "id": "security-scanner", "name": "Security Scanner" },
{ "id": "code-formatter", "name": "Code Formatter" },
{ "id": "test-generator", "name": "Test Generator" }
]
}
}
```
</Accordion>
<Accordion title="3. Configure different tool access">
```json
{
"agents": {
"list": [
{ "id": "reviewer", "tools": { "allow": ["read", "exec"] } },
{ "id": "fixer", "tools": { "allow": ["read", "write", "edit", "exec"] } }
]
}
}
```
`reviewer` is read-only. `fixer` can read and write.
</Accordion>
<Accordion title="4. Monitor performance">
With many agents, prefer `"strategy": "parallel"` (default), keep broadcast groups to a handful of agents, and use faster models for simpler agents.
</Accordion>
<Accordion title="5. Failures stay isolated">
Agents fail independently. One agent's error is logged (`Broadcast agent <id> failed: ...`) and does not block the others.
</Accordion>
</AccordionGroup>
## Compatibility
### Providers
Broadcast groups are currently implemented for WhatsApp (web channel) only. Other channels ignore the `broadcast` config.
### Routing
Broadcast groups work alongside existing routing:
```json
{
"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).
<Note>
**Precedence:** `broadcast` takes priority over ordinary route bindings. Configured ACP bindings (`bindings[].type="acp"`) are exclusive: when one matches, OpenClaw dispatches to the configured ACP session instead of fan-out broadcast.
</Note>
## Troubleshooting
<AccordionGroup>
<Accordion title="Agents not responding">
**Check:**
1. Agent IDs exist in `agents.list` (config validation rejects unknown ids).
2. Peer ID format is correct (group JID like `120363403215116621@g.us`, or E.164 like `+15551234567` for DMs).
3. The message passed normal gating (mention/activation rules still apply).
**Debug:**
```bash
openclaw logs --follow | grep -i broadcast
```
A successful fan-out logs `Broadcasting message to <n> agents (<strategy>)`.
</Accordion>
<Accordion title="Only one agent responding">
**Cause:** the peer ID might be in ordinary route bindings but not `broadcast`, or it might match an exclusive configured ACP binding.
**Fix:** add ordinary route-bound peers to the broadcast config, or remove/change the configured ACP binding if fan-out broadcast is desired.
</Accordion>
<Accordion title="Performance issues">
If slow with many agents: reduce the number of agents per group, use lighter models, and check sandbox startup time.
</Accordion>
</AccordionGroup>
## Examples
<AccordionGroup>
<Accordion title="Example 1: Code review team">
```json
{
"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"] } }
]
}
}
```
One code snippet in the group produces four replies: formatting fixes, a security finding, a coverage gap, and a docs nit.
</Accordion>
<Accordion title="Example 2: Multi-language pipeline">
```json
{
"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" }
]
}
}
```
</Accordion>
</AccordionGroup>
## API reference
### Config schema
```typescript
interface OpenClawConfig {
broadcast?: {
strategy?: "parallel" | "sequential";
[peerId: string]: string[];
};
}
```
### Fields
<ParamField path="strategy" type='"parallel" | "sequential"' default='"parallel"'>
How to process agents. `parallel` runs all agents simultaneously; `sequential` runs them in array order.
</ParamField>
<ParamField path="[peerId]" type="string[]">
WhatsApp group JID or E.164 phone number. Value is the array of agent IDs that should all process messages from that peer.
</ParamField>
## Limitations
1. **Max agents:** no hard limit, but many agents (10+) can be slow.
2. **Shared context:** agents do not see each other's responses (by design).
3. **Message ordering:** parallel responses may arrive in any order.
4. **Rate limits:** all replies come from one WhatsApp account, so every agent's reply counts toward the same WhatsApp rate limits.
## Related
- [Channel routing](/channels/channel-routing)
- [Groups](/channels/groups)
- [Multi-agent sandbox tools](/tools/multi-agent-sandbox-tools)
- [Pairing](/channels/pairing)
- [Session management](/concepts/session)

View File

@@ -0,0 +1,168 @@
---
summary: "Routing rules per channel (WhatsApp, Telegram, Discord, Slack) and shared context"
read_when:
- Changing channel routing or inbox behavior
title: "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**: a bundled channel plugin such as `discord`, `googlechat`, `imessage`, `irc`, `line`, `signal`, `slack`, `telegram`, or `whatsapp`, plus installed plugin channels. `webchat` is the internal WebChat UI channel and is not a configurable outbound channel.
- **AccountId**: per-channel account instance (when supported).
- Optional channel default account: `channels.<channel>.defaultAccount` chooses
which account is used when an outbound path does not specify `accountId`.
- In multi-account setups, set an explicit default (`defaultAccount` or an account named `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.
## Outbound target prefixes
Explicit outbound targets may include a provider prefix, such as `telegram:123` or `tg:123`. Core treats that prefix as a channel-selection hint only when the selected channel is `last` or otherwise unresolved, and only when the loaded plugin advertises that prefix. If the caller already selected an explicit channel, the provider prefix must match that channel; cross-channel combinations such as WhatsApp delivery to `telegram:123` fail before plugin-specific target normalization.
Target-kind and service prefixes such as `channel:<id>`, `user:<id>`, `room:<id>`, `thread:<id>`, `imessage:<handle>`, and `sms:<number>` stay inside the selected channel's grammar. They do not select the provider by themselves.
## Session key shapes (examples)
Direct messages collapse to the agent's **main** session by default:
- `agent:<agentId>:<mainKey>` (default: `agent:main:main`)
`session.dmScope` controls DM collapsing: `main` (default) shares one main
session, while `per-peer`, `per-channel-peer`, and `per-account-channel-peer`
keep DMs in separate sessions. A route binding can override the scope for its
matched peers via `bindings[].session.dmScope`.
Even when direct-message conversation history is shared with main, sandbox and
tool policy use a derived per-account direct-chat runtime key for external DMs
so channel-originated messages are not treated like local main-session runs.
Groups and channels remain isolated per channel:
- Groups: `agent:<agentId>:<channel>:group:<id>`
- Channels/rooms: `agent:<agentId>:<channel>:channel:<id>`
Threads:
- Slack/Discord threads append `:thread:<threadId>` to the base key.
- Telegram forum topics embed `:topic:<topicId>` 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`.
## Guarded inbound recording
Channel plugins can mark an inbound session record as `createIfMissing: false`
when a guarded path must not create a new OpenClaw session. In that mode,
OpenClaw may update metadata and `lastRoute` for an existing session, but it
does not create a route-only session entry just because a message was observed.
## 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. **Peer wildcard match** (`peer.id: "*"` for a peer kind).
4. **Guild + roles match** (Discord) via `guildId` + `roles`.
5. **Guild match** (Discord) via `guildId`.
6. **Team match** (Slack) via `teamId`.
7. **Account match** (`accountId` on the channel).
8. **Channel match** (any account on that channel, `accountId: "*"`).
9. **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
{
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
{
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/<agentId>/sessions/sessions.json`
- JSONL transcripts live alongside the store
You can override the store path via `session.store` and `{agentId}` templating.
Gateway and ACP session discovery also scans disk-backed agent stores under the
default `agents/` root and under templated `session.store` roots. Discovered
stores must stay inside that resolved agent root and use a regular
`sessions.json` file. Symlinks and out-of-root paths are ignored.
## 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.
## Related
- [Groups](/channels/groups)
- [Broadcast groups](/channels/broadcast-groups)
- [Pairing](/channels/pairing)

164
docs/channels/clickclack.md Normal file
View File

@@ -0,0 +1,164 @@
---
summary: "ClickClack bot-token channel setup and target syntax"
read_when:
- Connecting OpenClaw to a ClickClack workspace
- Testing ClickClack bot identities
title: "ClickClack"
---
ClickClack connects OpenClaw to a self-hosted ClickClack workspace through first-class ClickClack bot tokens.
Use this when you want an OpenClaw agent to appear as a ClickClack bot user. ClickClack supports independent service bots and user-owned bots; user-owned bots keep an `owner_user_id` and receive only the token scopes you grant.
## Quick setup
Create a bot token on the ClickClack server:
```bash
clickclack admin bot create \
--workspace <workspace_id> \
--name "OpenClaw" \
--handle openclaw \
--scopes bot:write \
--plain
```
For a user-owned bot, add `--owner <user_id>`.
Configure OpenClaw:
```json5
{
channels: {
clickclack: {
enabled: true,
baseUrl: "https://clickclack.example.com",
token: { source: "env", provider: "default", id: "CLICKCLACK_BOT_TOKEN" },
workspace: "default",
defaultTo: "channel:general",
},
},
}
```
Then run:
```bash
export CLICKCLACK_BOT_TOKEN="ccb_..."
openclaw gateway
```
An account counts as configured only when `baseUrl`, `token`, and `workspace` are all set. `workspace` accepts a workspace id (`wsp_...`), slug, or name; the gateway resolves it to the id at startup.
### Account config keys
| Key | Default | Notes |
| ----------------------- | ------------------- | --------------------------------------------------------------------------------------- |
| `baseUrl` | none (required) | ClickClack server URL. |
| `token` | none (required) | Plain string or secret ref (`source: "env" \| "file" \| "exec"`). |
| `workspace` | none (required) | Workspace id, slug, or name. |
| `replyMode` | `"agent"` | `"agent"` runs the full agent pipeline; `"model"` sends short direct model completions. |
| `defaultTo` | `"channel:general"` | Target used when an outbound path gives no target. |
| `allowFrom` | `["*"]` | User-id allowlist for inbound DMs and channel messages. |
| `botUserId` | auto-detected | Resolved from the bot token identity at startup. |
| `agentId` | route default | Pin this account's inbound messages to one agent. |
| `toolsAllow` | none | Tool allowlist for agent replies from this account. |
| `model`, `systemPrompt` | none | Used by `replyMode: "model"` completions. |
| `reconnectMs` | `1500` | Realtime reconnect delay (100 to 60000). |
If `plugins.allow` is a non-empty restrictive list, explicitly selecting
ClickClack in channel setup or running `openclaw plugins enable clickclack`
appends `clickclack` to that list. Onboarding installation uses the same
explicit-selection behavior. These paths do not override `plugins.deny` or a
global `plugins.enabled: false` setting. Direct
`openclaw plugins install @openclaw/clickclack` follows the normal
plugin-install policy and also records ClickClack in an existing allowlist.
## Multiple bots
Each account opens its own ClickClack realtime connection and uses its own bot token.
```json5
{
channels: {
clickclack: {
enabled: true,
baseUrl: "https://clickclack.example.com",
defaultAccount: "service",
accounts: {
service: {
token: { source: "env", provider: "default", id: "CLICKCLACK_SERVICE_BOT_TOKEN" },
workspace: "default",
defaultTo: "channel:general",
agentId: "service-bot",
},
support: {
token: { source: "env", provider: "default", id: "CLICKCLACK_SUPPORT_BOT_TOKEN" },
workspace: "default",
defaultTo: "dm:usr_...",
agentId: "support-bot",
},
},
},
},
}
```
## Reply modes
- `replyMode: "agent"` (default) dispatches inbound messages through the normal agent pipeline, including session recording and tool policy.
- `replyMode: "model"` skips the agent pipeline and uses the plugin runtime's `llm.complete` for short direct bot replies (optionally shaped by `model` and `systemPrompt`).
Model mode runs completions against the resolved bot agent id, which requires
the explicit `plugins.entries.clickclack.llm.allowAgentIdOverride: true` trust
bit:
```json5
{
plugins: {
entries: {
clickclack: {
llm: {
allowAgentIdOverride: true,
},
},
},
},
}
```
Keep the trust bit off if you only use the default `agent` reply mode; it is
not needed there.
## Targets
- `channel:<name-or-id>` sends to a workspace channel. Bare targets default to `channel:`.
- `dm:<user_id>` creates or reuses a direct conversation with that user.
- `thread:<message_id>` replies in the thread rooted at that message.
Explicit outbound targets may also carry the `clickclack:` or `cc:` provider prefix.
Examples:
```bash
openclaw message send --channel clickclack --target channel:general --message "hello"
openclaw message send --channel clickclack --target dm:usr_123 --message "hello"
openclaw message send --channel clickclack --target thread:msg_123 --message "following up"
```
## Permissions
ClickClack token scopes are enforced by the ClickClack API.
- `bot:read`: read workspace/channel/message/thread/DM/realtime/profile data.
- `bot:write`: `bot:read` plus channel messages, thread replies, DMs, and uploads.
- `bot:admin`: `bot:write` plus channel creation.
OpenClaw only needs `bot:write` for normal agent chat.
## Troubleshooting
- `ClickClack is not configured for account "<id>"`: set `baseUrl`, `token` (for example via `CLICKCLACK_BOT_TOKEN`), and `workspace` for that account.
- `ClickClack workspace not found: <value>`: set `workspace` to the workspace id, slug, or name returned by ClickClack.
- No inbound replies: confirm the token has realtime read access and note that the bot ignores its own messages and messages from other bots.
- Channel sends fail: verify the bot is a member of the workspace and has `bot:write`.

1724
docs/channels/discord.md Normal file

File diff suppressed because it is too large Load Diff

669
docs/channels/feishu.md Normal file
View File

@@ -0,0 +1,669 @@
---
summary: "Feishu bot overview, features, and configuration"
read_when:
- You want to connect a Feishu/Lark bot
- You are configuring the Feishu channel
title: Feishu
---
OpenClaw connects to Feishu/Lark (the all-in-one collaboration platform) through the official `@openclaw/feishu` plugin: bot DMs, group chats, streaming card replies, and Feishu doc/wiki/drive/Bitable tools.
**Status:** production-ready for bot DMs + group chats. WebSocket is the default event transport (no public URL needed); webhook mode is optional.
## Quick start
<Note>
Requires OpenClaw 2026.5.29 or above. Run `openclaw --version` to check. Upgrade with `openclaw update`.
</Note>
<Steps>
<Step title="Run the channel setup wizard">
```bash
openclaw channels login --channel feishu
```
This installs the `@openclaw/feishu` plugin if it is missing, then walks through setup:
- **Manual setup**: paste an App ID and App Secret from Feishu Open Platform (`https://open.feishu.cn`) or Lark Developer (`https://open.larksuite.com`).
- **QR setup**: scan a QR code in the Feishu app to create a bot automatically. This flow locks DMs to your own account (`dmPolicy: "allowlist"` with your `open_id`).
The wizard also asks for the API domain (Feishu vs Lark) and the group policy. If the domestic Feishu mobile app does not react to the QR code, rerun setup and choose manual setup.
</Step>
<Step title="After setup completes, restart the gateway to apply the changes">
```bash
openclaw gateway restart
```
</Step>
</Steps>
## Access control
### Direct messages
Configure `channels.feishu.dmPolicy` (default: `pairing`) to control who can DM the bot:
| Value | Behavior |
| ------------- | ------------------------------------------------------------------------------------------------------------- |
| `"pairing"` | Unknown users receive a pairing code; approve via CLI |
| `"allowlist"` | Only users listed in `allowFrom` can chat |
| `"open"` | Public DMs; config validation requires `allowFrom` to include `"*"`. Non-wildcard entries still narrow access |
**Approve a pairing request:**
```bash
openclaw pairing list feishu
openclaw pairing approve feishu <CODE>
```
### Group chats
**Group policy** (`channels.feishu.groupPolicy`, default: `allowlist`):
| Value | Behavior |
| ------------- | -------------------------------------------------------------------------------------------- |
| `"open"` | Respond to all messages in groups |
| `"allowlist"` | Only respond to groups in `groupAllowFrom` or explicitly configured under `groups.<chat_id>` |
| `"disabled"` | Disable all group messages; explicit `groups.<chat_id>` entries do not override this |
**Mention requirement** (`channels.feishu.requireMention`):
- Default: @mention required, except when the effective group policy is `"open"`; there it defaults to `false` so messages that cannot carry mentions (for example images) still reach the agent.
- Set `true` or `false` explicitly to override; per-group override: `channels.feishu.groups.<chat_id>.requireMention`.
- Broadcast-only `@all` and `@_all` are not treated as bot mentions. A message that mentions both `@all` and the bot directly still counts as a bot mention.
## Group configuration examples
### Allow all groups, no @mention required
```json5
{
channels: {
feishu: {
groupPolicy: "open", // requireMention defaults to false under "open"
},
},
}
```
### Allow all groups, still require @mention
```json5
{
channels: {
feishu: {
groupPolicy: "open",
requireMention: true,
},
},
}
```
### Allow specific groups only
```json5
{
channels: {
feishu: {
groupPolicy: "allowlist",
// Group IDs look like: oc_xxx
groupAllowFrom: ["oc_xxx", "oc_yyy"],
},
},
}
```
In `allowlist` mode, you can also admit a group by adding an explicit `groups.<chat_id>` entry. Explicit entries do not override `groupPolicy: "disabled"`. Wildcard defaults under `groups.*` configure matching groups, but they do not admit groups by themselves.
```json5
{
channels: {
feishu: {
groupPolicy: "allowlist",
groups: {
oc_xxx: {
requireMention: false,
},
},
},
},
}
```
### Restrict senders within a group
```json5
{
channels: {
feishu: {
groupPolicy: "allowlist",
groupAllowFrom: ["oc_xxx"],
groups: {
oc_xxx: {
// User open_ids look like: ou_xxx
allowFrom: ["ou_user1", "ou_user2"],
},
},
},
},
}
```
`channels.feishu.groupSenderAllowFrom` sets the same sender allowlist for all groups; a per-group `allowFrom` takes precedence.
<a id="get-groupuser-ids"></a>
## Get group/user IDs
### Group IDs (`chat_id`, format: `oc_xxx`)
Open the group in Feishu/Lark, click the menu icon in the top-right corner, and go to **Settings**. The group ID (`chat_id`) is listed on the settings page.
![Get Group ID](/images/feishu-get-group-id.png)
### User IDs (`open_id`, format: `ou_xxx`)
Start the gateway, send a DM to the bot, then check the logs:
```bash
openclaw logs --follow
```
Look for `open_id` in the log output. You can also check pending pairing requests:
```bash
openclaw pairing list feishu
```
## Common commands
| Command | Description |
| --------- | --------------------------- |
| `/status` | Show bot status |
| `/reset` | Reset the current session |
| `/model` | Show or switch the AI model |
<Note>
Feishu/Lark does not support native slash-command menus, so send these as plain text messages.
</Note>
## Troubleshooting
### Bot does not respond in group chats
1. Ensure the bot is added to the group
2. Ensure you @mention the bot (required by default)
3. Verify `groupPolicy` is not `"disabled"`
4. Check logs: `openclaw logs --follow`
### Bot does not receive messages
1. Ensure the bot is published and approved in Feishu Open Platform / Lark Developer
2. Ensure event subscription includes `im.message.receive_v1`
3. Ensure **persistent connection** (WebSocket) is selected
4. Ensure all required permission scopes are granted
5. Ensure the gateway is running: `openclaw gateway status`
6. Check logs: `openclaw logs --follow`
### QR setup does not react in the Feishu mobile app
1. Rerun setup: `openclaw channels login --channel feishu`
2. Choose manual setup
3. In Feishu Open Platform, create a self-built app and copy its App ID and App Secret
4. Paste those credentials into the setup wizard
### App Secret leaked
1. Reset the App Secret in Feishu Open Platform / Lark Developer
2. Update the value in your config
3. Restart the gateway: `openclaw gateway restart`
## Advanced configuration
### Multiple accounts
```json5
{
channels: {
feishu: {
defaultAccount: "main",
accounts: {
main: {
appId: "cli_xxx",
appSecret: "xxx",
name: "Primary bot",
tts: {
providers: {
openai: { voice: "shimmer" },
},
},
},
backup: {
appId: "cli_yyy",
appSecret: "yyy",
name: "Backup bot",
enabled: false,
},
},
},
},
}
```
`defaultAccount` controls which account is used when outbound APIs do not specify an `accountId`. Account entries inherit top-level settings; most top-level keys can be overridden per account.
`accounts.<id>.tts` uses the same shape as `messages.tts` and deep-merges over global TTS config, so multi-bot Feishu setups can keep shared provider credentials globally while overriding only voice, model, persona, or auto mode per account.
### Message limits
- `textChunkLimit` - outbound text chunk size (default: `4000` chars)
- `chunkMode` - `"length"` (default) splits at the limit; `"newline"` prefers newline boundaries
- `mediaMaxMb` - media upload/download limit (default: `30` MB)
### Streaming
Feishu/Lark supports streaming replies via interactive cards (Card Kit streaming API). When enabled, the bot updates the card in real time as it generates text.
```json5
{
channels: {
feishu: {
streaming: true, // enable streaming card output (default: true)
blockStreaming: true, // opt into completed-block streaming
},
},
}
```
Set `streaming: false` to send the complete reply in one message; `renderMode: "raw"` (plain text instead of cards) also disables streaming cards. `blockStreaming` is off by default; enable it only when you want completed assistant blocks flushed before the final reply.
### Quota optimization
Reduce the number of Feishu/Lark API calls with two optional flags:
- `typingIndicator` (default `true`): set `false` to skip typing reaction calls
- `resolveSenderNames` (default `true`): set `false` to skip sender profile lookups
```json5
{
channels: {
feishu: {
typingIndicator: false,
resolveSenderNames: false,
},
},
}
```
### Group session scope and topic threads
`channels.feishu.groupSessionScope` (top-level, per account, or per group) controls how group messages map to agent sessions:
| Value | Session |
| ---------------------- | ---------------------------------------------------------------- |
| `"group"` (default) | One session per group chat |
| `"group_sender"` | One session per (group + sender) |
| `"group_topic"` | One session per topic thread; falls back to the group session |
| `"group_topic_sender"` | One session per (topic + sender); falls back to (group + sender) |
For the topic scopes, native Feishu/Lark topic groups use the event `thread_id` (`omt_*`) as the canonical topic session key. If a native topic starter event omits `thread_id`, OpenClaw hydrates it from Feishu before routing the turn. Normal group replies that OpenClaw turns into threads keep using the reply root message ID (`om_*`) so the first turn and follow-up turns stay in the same session.
Set `replyInThread: "enabled"` (top-level or per group) to make bot replies create or continue a Feishu topic thread instead of replying inline. `topicSessionMode` is the deprecated predecessor of `groupSessionScope`; prefer `groupSessionScope`.
### Feishu workspace tools
The plugin ships agent tools for Feishu documents, chats, knowledge base, cloud storage, permissions, and Bitable, plus matching skills (`feishu-doc`, `feishu-drive`, `feishu-perm`, `feishu-wiki`). Tool families are gated by `channels.feishu.tools`:
| Key | Tools | Default |
| --------------- | --------------------------------------------- | ------------------- |
| `tools.doc` | `feishu_doc` document operations | `true` |
| `tools.chat` | `feishu_chat` chat info + member queries | `true` |
| `tools.wiki` | `feishu_wiki` knowledge base (requires `doc`) | `true` |
| `tools.drive` | `feishu_drive` cloud storage | `true` |
| `tools.perm` | `feishu_perm` permission management | `false` (sensitive) |
| `tools.scopes` | `feishu_app_scopes` app scope diagnostics | `true` |
| `tools.bitable` | `feishu_bitable_*` Bitable/Base operations | `true` |
`tools.base` is an alias for `tools.bitable`; the explicit `bitable` value wins when both are set. Per-account gates live under `accounts.<id>.tools`.
### ACP sessions
Feishu/Lark supports ACP for DMs and group thread messages. Feishu/Lark ACP is text-command driven - there are no native slash-command menus, so use `/acp ...` messages directly in the conversation.
#### Persistent ACP binding
```json5
{
agents: {
list: [
{
id: "codex",
runtime: {
type: "acp",
acp: {
agent: "codex",
backend: "acpx",
mode: "persistent",
cwd: "/workspace/openclaw",
},
},
},
],
},
bindings: [
{
type: "acp",
agentId: "codex",
match: {
channel: "feishu",
accountId: "default",
peer: { kind: "direct", id: "ou_1234567890" },
},
},
{
type: "acp",
agentId: "codex",
match: {
channel: "feishu",
accountId: "default",
peer: { kind: "group", id: "oc_group_chat:topic:om_topic_root" },
},
acp: { label: "codex-feishu-topic" },
},
],
}
```
#### Spawn ACP from chat
In a Feishu/Lark DM or thread:
```text
/acp spawn codex --thread here
```
`--thread here` works for DMs and Feishu/Lark thread messages. Follow-up messages in the bound conversation route directly to that ACP session.
### Multi-agent routing
Use `bindings` to route Feishu/Lark DMs or groups to different agents.
```json5
{
agents: {
list: [
{ id: "main" },
{ id: "agent-a", workspace: "/home/user/agent-a" },
{ id: "agent-b", workspace: "/home/user/agent-b" },
],
},
bindings: [
{
agentId: "agent-a",
match: {
channel: "feishu",
peer: { kind: "direct", id: "ou_xxx" },
},
},
{
agentId: "agent-b",
match: {
channel: "feishu",
peer: { kind: "group", id: "oc_zzz" },
},
},
],
}
```
Routing fields:
- `match.channel`: `"feishu"`
- `match.peer.kind`: `"direct"` (DM) or `"group"` (group chat)
- `match.peer.id`: user Open ID (`ou_xxx`) or group ID (`oc_xxx`)
See [Get group/user IDs](#get-groupuser-ids) for lookup tips.
## Per-user agent isolation (Dynamic Agent Creation)
Enable `dynamicAgentCreation` to automatically create **isolated agent instances** for each DM user. Each user gets their own:
- Independent workspace directory
- Separate `USER.md` / `SOUL.md` / `MEMORY.md`
- Private conversation history
- Isolated skills and state
This is essential for public bots where you want each user to have their own private AI assistant experience.
<Note>
Dynamic bindings include the normalized Feishu `accountId`, so default and named accounts route each sender to the correct dynamic agent.
If a named account created an unscoped dynamic agent on an older release, that legacy agent still counts toward `maxAgents`. Confirm that it is not used by the default account before removing it, or temporarily increase `maxAgents`; OpenClaw cannot safely infer which account owns ambiguous legacy state.
</Note>
### Quick setup
```json5
{
channels: {
feishu: {
dmPolicy: "open",
allowFrom: ["*"],
dynamicAgentCreation: {
enabled: true,
workspaceTemplate: "~/.openclaw/workspace-{agentId}",
agentDirTemplate: "~/.openclaw/agents/{agentId}/agent",
},
},
},
session: {
// Critical: makes each user's DM their "main session"
// Automatically loads USER.md / SOUL.md / MEMORY.md
// For stronger isolation, use "per-channel-peer" instead
dmScope: "main",
},
}
```
### How it works
When a new user sends their first DM:
1. The channel generates a unique `agentId`: `feishu-{user_open_id}` for the default account, or a bounded account-prefixed identity digest for a named account
2. Creates a new workspace at `workspaceTemplate` path
3. Registers the agent and creates a binding for this user
4. The workspace helper ensures bootstrap files (`AGENTS.md`, `SOUL.md`, `USER.md`, etc.) on first access
5. Routes all future messages from this user to their dedicated agent
### Configuration options
| Setting | Description | Default |
| -------------------------------------------------------- | ------------------------------------------ | ------------------------------------ |
| `channels.feishu.dynamicAgentCreation.enabled` | Enable automatic per-user agent creation | `false` |
| `channels.feishu.dynamicAgentCreation.workspaceTemplate` | Path template for dynamic agent workspaces | `~/.openclaw/workspace-{agentId}` |
| `channels.feishu.dynamicAgentCreation.agentDirTemplate` | Agent directory name template | `~/.openclaw/agents/{agentId}/agent` |
| `channels.feishu.dynamicAgentCreation.maxAgents` | Maximum number of dynamic agents to create | unlimited |
Template variables:
- `{agentId}` - the generated agent ID (e.g., `feishu-ou_xxxxxx` or `feishu-support-<identity_digest>`)
- `{userId}` - the sender's Feishu open_id (e.g., `ou_xxxxxx`)
### Session scope
`session.dmScope` controls how direct messages are mapped to agent sessions. This is a **global setting** that affects all channels.
| Value | Behavior | Best for |
| ---------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `"main"` | Each user's DM maps to their agent's main session | Single-user bots where you want `USER.md` / `SOUL.md` to auto-load |
| `"per-peer"` | Each peer gets a separate session (regardless of channel) | Isolation keyed by sender identity only |
| `"per-channel-peer"` | Each (channel + user) combination gets a separate session | Public multi-user bots needing stronger isolation |
| `"per-account-channel-peer"` | Each (account + channel + user) combination gets a separate session | Multi-account bots needing account-level session isolation |
**Tradeoff**: Using `"main"` enables automatic bootstrap file loading (`USER.md`, `SOUL.md`, `MEMORY.md`), but means all DMs across all channels share the same session key pattern. For public multi-user bots where isolation matters more than bootstrap auto-loading, consider `"per-channel-peer"` and manage bootstrap files manually.
<Note>
Use `"per-account-channel-peer"` when named Feishu accounts should keep separate sessions for the same sender. Dynamic bindings preserve the account scope.
</Note>
### Typical multi-user deployment
```json5
{
channels: {
feishu: {
appId: "cli_xxx",
appSecret: "xxx",
dmPolicy: "open",
allowFrom: ["*"],
groupPolicy: "open",
requireMention: true,
dynamicAgentCreation: {
enabled: true,
workspaceTemplate: "~/.openclaw/workspace-{agentId}",
agentDirTemplate: "~/.openclaw/agents/{agentId}/agent",
},
},
},
session: {
// Choose dmScope based on your isolation needs:
// "main" for bootstrap auto-loading, "per-channel-peer" for stronger isolation
dmScope: "main",
},
bindings: [], // Empty - dynamic agents auto-bind
}
```
### Verification
Check gateway logs to confirm dynamic creation is working:
```text
feishu: creating dynamic agent "feishu-ou_xxxxxx" for user ou_xxxxxx
workspace: /home/user/.openclaw/workspace-feishu-ou_xxxxxx
agentDir: /home/user/.openclaw/agents/feishu-ou_xxxxxx/agent
```
List all created workspaces:
```bash
ls -la ~/.openclaw/workspace-*
```
### Notes
- **Workspace isolation**: Each user gets their own workspace directory and agent instance. Users cannot see each other's conversation history or files within the normal messaging flow.
- **Security boundary**: This is a messaging-context isolation mechanism, not a hostile co-tenant security boundary. The agent process and host environment are shared.
- **Config writes must stay enabled**: Dynamic agent creation writes agents and bindings into the config; it is skipped when `channels.feishu.configWrites` is `false` (default: enabled).
- **`bindings` should be empty**: Dynamic agents auto-register their own bindings
- **Upgrade path**: Existing manual bindings continue to work alongside dynamic agents
- **`session.dmScope` is global**: This affects all channels, not just Feishu
## Configuration reference
Full configuration: [Gateway configuration](/gateway/configuration)
| Setting | Description | Default |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------ |
| `channels.feishu.enabled` | Enable/disable the channel | `true` |
| `channels.feishu.domain` | API domain (`feishu`, `lark`, or an `https://` base URL) | `feishu` |
| `channels.feishu.connectionMode` | Event transport (`websocket` or `webhook`) | `websocket` |
| `channels.feishu.defaultAccount` | Default account for outbound routing | `default` |
| `channels.feishu.verificationToken` | Required for webhook mode | - |
| `channels.feishu.encryptKey` | 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.<id>.appId` | App ID | - |
| `channels.feishu.accounts.<id>.appSecret` | App Secret | - |
| `channels.feishu.accounts.<id>.domain` | Per-account domain override | `feishu` |
| `channels.feishu.accounts.<id>.tts` | Per-account TTS override | `messages.tts` |
| `channels.feishu.dmPolicy` | DM policy (`pairing`, `allowlist`, `open`) | `pairing` |
| `channels.feishu.allowFrom` | DM allowlist (open_id list) | - |
| `channels.feishu.groupPolicy` | Group policy (`open`, `allowlist`, `disabled`) | `allowlist` |
| `channels.feishu.groupAllowFrom` | Group allowlist | - |
| `channels.feishu.groupSenderAllowFrom` | Sender allowlist applied to all groups | - |
| `channels.feishu.requireMention` | Require @mention in groups | `true` (`false` when policy `open`) |
| `channels.feishu.groups.<chat_id>.requireMention` | Per-group @mention override; explicit IDs also admit the group in allowlist mode | inherited |
| `channels.feishu.groups.<chat_id>.enabled` | Enable/disable a specific group | `true` |
| `channels.feishu.groups.<chat_id>.allowFrom` | Per-group sender allowlist (overrides `groupSenderAllowFrom`) | - |
| `channels.feishu.groupSessionScope` | Group session mapping (`group`, `group_sender`, `group_topic`, `group_topic_sender`) | `group` |
| `channels.feishu.replyInThread` | Bot replies create/continue topic threads (`disabled`, `enabled`) | `disabled` |
| `channels.feishu.reactionNotifications` | Inbound reaction events (`off`, `own`, `all`) | `own` |
| `channels.feishu.dynamicAgentCreation.enabled` | Enable automatic per-user agent creation | `false` |
| `channels.feishu.dynamicAgentCreation.workspaceTemplate` | Path template for dynamic agent workspaces | `~/.openclaw/workspace-{agentId}` |
| `channels.feishu.dynamicAgentCreation.agentDirTemplate` | Agent directory name template | `~/.openclaw/agents/{agentId}/agent` |
| `channels.feishu.dynamicAgentCreation.maxAgents` | Maximum number of dynamic agents to create | unlimited |
| `channels.feishu.textChunkLimit` | Message chunk size | `4000` |
| `channels.feishu.chunkMode` | Chunk splitting (`length` or `newline`) | `length` |
| `channels.feishu.mediaMaxMb` | Media size limit | `30` |
| `channels.feishu.renderMode` | Reply rendering (`auto`, `raw`, `card`) | `auto` |
| `channels.feishu.streaming` | Streaming card output | `true` |
| `channels.feishu.blockStreaming` | Completed-block reply streaming | `false` |
| `channels.feishu.typingIndicator` | Send typing reactions | `true` |
| `channels.feishu.resolveSenderNames` | Resolve sender display names | `true` |
| `channels.feishu.configWrites` | Allow channel-initiated config writes (needed by dynamic agents) | `true` |
| `channels.feishu.tools.doc` | Enable document tools | `true` |
| `channels.feishu.tools.chat` | Enable chat info tools | `true` |
| `channels.feishu.tools.wiki` | Enable knowledge base tools (requires `doc`) | `true` |
| `channels.feishu.tools.drive` | Enable cloud storage tools | `true` |
| `channels.feishu.tools.perm` | Enable permission management tools | `false` |
| `channels.feishu.tools.scopes` | Enable app scopes diagnostic tool | `true` |
| `channels.feishu.tools.bitable` | Enable Bitable/Base tools | `true` |
| `channels.feishu.tools.base` | Alias for `channels.feishu.tools.bitable`; explicit `bitable` wins when both set | `true` |
| `channels.feishu.accounts.<id>.tools.bitable` | Per-account Bitable/Base tool gate | inherited |
| `channels.feishu.accounts.<id>.tools.base` | Per-account alias for `tools.bitable` | inherited |
## Supported message types
### Receive
- ✅ Text
- ✅ Rich text (post)
- ✅ Images
- ✅ Files
- ✅ Audio
- ✅ Video/media
- ✅ Stickers
Inbound Feishu/Lark audio messages are normalized as media placeholders instead
of raw `file_key` JSON. When `tools.media.audio` is configured, OpenClaw
downloads the voice-note resource and runs shared audio transcription before the
agent turn, so the agent receives the spoken transcript. If Feishu includes
transcript text directly in the audio payload, that text is used without another
ASR call. Without an audio transcription provider, the agent still receives a
`<media:audio>` placeholder plus the saved attachment, not the raw Feishu
resource payload.
### Send
- ✅ Text
- ✅ Images
- ✅ Files
- ✅ Audio
- ✅ Video/media
- ✅ Interactive cards (including streaming updates)
- ⚠️ Rich text (post-style formatting; doesn't support full Feishu/Lark authoring capabilities)
Native Feishu/Lark audio bubbles use the Feishu `audio` message type and require
Ogg/Opus upload media (`file_type: "opus"`). Existing `.opus` and `.ogg` media
is sent directly as native audio. MP3/WAV/M4A and other likely audio formats are
transcoded to 48kHz Ogg/Opus with `ffmpeg` only when the reply requests voice
delivery (`audioAsVoice` / message tool `asVoice`, including TTS voice-note
replies). Ordinary MP3 attachments stay regular files. If `ffmpeg` is missing or
conversion fails, OpenClaw falls back to a file attachment and logs the reason.
### Threads and replies
- ✅ Inline replies
- ✅ Thread replies
- ✅ Media replies stay thread-aware when replying to a thread message
Topic-group session routing is covered under
[Group session scope and topic threads](#group-session-scope-and-topic-threads).
## Related
- [Channels Overview](/channels) - all supported channels
- [Pairing](/channels/pairing) - DM authentication and pairing flow
- [Groups](/channels/groups) - group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) - session routing for messages
- [Security](/gateway/security) - access model and hardening

261
docs/channels/googlechat.md Normal file
View File

@@ -0,0 +1,261 @@
---
summary: "Google Chat app support status, capabilities, and configuration"
read_when:
- Working on Google Chat channel features
title: "Google Chat"
---
Google Chat runs as the official `@openclaw/googlechat` plugin: DMs and spaces through Google Chat API webhooks (HTTP endpoint only, no Pub/Sub).
## Install
```bash
openclaw plugins install @openclaw/googlechat
```
Local checkout (when running from a git repo):
```bash
openclaw plugins install ./path/to/local/googlechat-plugin
```
## 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 and principals blank (**Continue**, then **Done**).
3. Create and download the **JSON key**:
- Click the new service account > **Keys** tab > **Add Key** > **Create new key** > **JSON** > **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 **Application info** (app name, avatar URL, description).
- 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 public gateway URL followed by `/googlechat` (see [Public URL](#public-url-webhook-only)).
- Under **Visibility**, check **Make this Chat app available to specific people and groups in `<Your Domain>`** and enter your email address.
- Click **Save**.
6. Enable the app status: refresh the page, find **App status**, set it to **Live - available to users**, and **Save** again.
7. Configure OpenClaw with the service account and the webhook audience (must match the Chat app config):
- Env: `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE=/path/to/service-account.json` (default account only), or
- Config: see [Config highlights](#config-highlights). `openclaw channels add --channel googlechat` also accepts `--audience-type`, `--audience`, `--webhook-path`, and `--webhook-url`.
8. Start the gateway. Google Chat will POST to your webhook path (default `/googlechat`).
## Add to Google Chat
Once the gateway is running and your email is on the visibility list:
1. Go to [Google Chat](https://chat.google.com/).
2. Click the **+** (plus) icon next to **Direct Messages**.
3. Search for the **App name** you configured in the Google Cloud Console.
- The bot does _not_ appear in the Marketplace browse list because it is a private app; search for it by name.
4. Select the bot, click **Add** or **Chat**, and send a message.
## Public URL (Webhook-only)
Google Chat webhooks require a public HTTPS endpoint. For security, expose **only the `/googlechat` path** to the internet and keep the OpenClaw dashboard and other endpoints private.
### Option A: Tailscale Funnel (Recommended)
Use Tailscale Serve for the private dashboard and Funnel for the public webhook path.
1. Check what address your gateway is bound to:
```bash
ss -tlnp | grep 18789
```
Note the IP (e.g., `127.0.0.1`, `0.0.0.0`, or a Tailscale `100.x.x.x` address).
2. Expose the dashboard to the tailnet only (port 8443):
```bash
# 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 a Tailscale IP only:
tailscale serve --bg --https 8443 http://100.x.x.x:18789
```
3. Expose only the webhook path publicly:
```bash
# 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 a Tailscale IP only:
tailscale funnel --bg --set-path /googlechat http://100.x.x.x:18789/googlechat
```
4. If prompted, visit the authorization URL shown in the output to enable Funnel for this node.
5. Verify:
```bash
tailscale serve status
tailscale funnel status
```
Your public webhook URL is `https://<node-name>.<tailnet>.ts.net/googlechat`; the dashboard stays tailnet-only at `https://<node-name>.<tailnet>.ts.net:8443/`. Use the public URL (without `:8443`) in the Google Chat app config.
> Note: This configuration persists across reboots. Remove it later with `tailscale funnel reset` and `tailscale serve reset`.
### Option B: Reverse Proxy (Caddy)
Proxy only the webhook path:
```caddy
your-domain.com {
reverse_proxy /googlechat* localhost:18789
}
```
Requests to `your-domain.com/` are ignored or 404, while `your-domain.com/googlechat` routes to OpenClaw.
### Option C: Cloudflare Tunnel
Configure the tunnel ingress rules to route only the webhook path:
- **Path**: `/googlechat` -> `http://localhost:18789/googlechat`
- **Default rule**: HTTP 404 (Not Found)
## How it works
1. Google Chat POSTs JSON to the gateway webhook path (POST only, JSON content type required, per-IP rate limited).
2. OpenClaw authenticates every request before dispatch:
- Chat app events carry `Authorization: Bearer <token>`; the token is verified before the full body is parsed.
- Google Workspace Add-on events carry the token in the body (`authorizationEventObject.systemIdToken`) and are read under a stricter pre-auth budget (16 KB, 3 s) before verification.
3. The token is checked against `audienceType` + `audience`:
- `audienceType: "app-url"` → audience is your HTTPS webhook URL.
- `audienceType: "project-number"` → audience is the Cloud project number.
- Add-on tokens under `app-url` additionally require `appPrincipal` set to the app's numeric OAuth 2.0 client ID (21 digits, not an email); otherwise verification fails with a logged warning.
4. Messages route by space:
- Spaces get per-space sessions `agent:<agentId>:googlechat:group:<spaceId>`; replies go to the message thread.
- DMs collapse into the agent's main session by default; set `session.dmScope` for per-peer DM sessions (see [Session](/concepts/session)).
5. DM access is pairing by default. Unknown senders receive a pairing code; approve with:
- `openclaw pairing approve googlechat <code>`
6. Group spaces require @-mention by default. Mentions are detected from Chat `USER_MENTION` annotations targeting the app; set `botUser` (e.g., `users/1234567890`) if detection needs the app's user resource name.
7. When an exec or plugin approval starts from Google Chat and a stable `users/<id>` approver is configured, OpenClaw posts a native approval card (`cardsV2`) in the originating space or thread. Card buttons carry opaque callback tokens; the manual `/approve <id> <decision>` prompt appears only when native delivery is unavailable.
## Targets
Use these identifiers for delivery and allowlists:
- Direct messages: `users/<userId>` (recommended).
- Spaces: `spaces/<spaceId>`.
- Raw email `name@example.com` is mutable and only used for allowlist matching when `channels.googlechat.dangerouslyAllowNameMatching: true`.
- Deprecated: `users/<email>` is treated as a user id, not an email allowlist entry.
- Prefixes `googlechat:`, `google-chat:`, and `gchat:` are accepted and stripped.
## Config highlights
```json5
{
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",
appPrincipal: "123456789012345678901", // add-on verification only; numeric OAuth client ID
webhookPath: "/googlechat",
botUser: "users/1234567890", // optional; helps mention detection
allowBots: false,
dm: {
policy: "pairing",
allowFrom: ["users/1234567890"],
},
groupPolicy: "allowlist",
groups: {
"spaces/AAAA": {
enabled: true,
requireMention: true,
users: ["users/1234567890"],
systemPrompt: "Short answers only.",
},
},
actions: { reactions: true },
typingIndicator: "message",
mediaMaxMb: 20,
},
},
}
```
Notes:
- Service account credentials: `serviceAccountFile` (path), `serviceAccount` (inline JSON string or object), or `serviceAccountRef` (env/file SecretRef). Env vars `GOOGLE_CHAT_SERVICE_ACCOUNT` (inline JSON) and `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE` (path) apply to the default account only. Multi-account setups use `channels.googlechat.accounts.<id>` with the same keys, including per-account `serviceAccountRef`.
- Default webhook path is `/googlechat` when `webhookPath` is unset; `webhookUrl` can supply the path instead.
- Group keys must be stable space ids (`spaces/<spaceId>`). Display-name keys are deprecated and logged as such.
- `dangerouslyAllowNameMatching` re-enables mutable email principal matching for allowlists (break-glass compatibility mode); doctor warns about email entries.
- Reactions are enabled by default and exposed through the `reactions` tool and `channels action`; disable with `actions.reactions: false`.
- Native approval cards use Google Chat `cardsV2` button clicks, not reaction events. Approvers come from `dm.allowFrom` or `defaultTo` and must be stable numeric `users/<id>` values.
- Message actions expose `send` for text and `upload-file` for explicit attachment sends. `upload-file` accepts `media` / `filePath` / `path` plus optional `message`, `filename`, and thread targeting (`threadId` / `replyTo`).
- `typingIndicator`: `message` (default) posts a `_<Bot> is typing..._` placeholder and edits it into the first reply; `none` disables it; `reaction` requires user OAuth and currently falls back to `message` with a logged error under service-account auth.
- Inbound attachments (first attachment per message) are downloaded through the Chat API into the media pipeline, capped by `mediaMaxMb` (default 20).
- Bot-authored messages are ignored by default. With `allowBots: true`, accepted bot messages use shared [bot loop protection](/channels/bot-loop-protection): configure `channels.defaults.botLoopProtection`, then override with `channels.googlechat.botLoopProtection` or `channels.googlechat.groups.<space>.botLoopProtection`.
Secrets reference details: [Secrets Management](/gateway/secrets).
## Troubleshooting
### 405 Method Not Allowed
If Google Cloud Logs Explorer shows errors like:
```text
status code: 405, reason phrase: HTTP error response: HTTP/1.1 405 Method Not Allowed
```
The webhook handler is not registered. Common causes:
1. **Channel not configured**: the `channels.googlechat` section is missing. Verify with:
```bash
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
openclaw plugins list | grep googlechat
```
If it shows "disabled", add `plugins.entries.googlechat.enabled: true` to your config.
3. **Gateway not restarted** after config changes:
```bash
openclaw gateway restart
```
Verify the channel is running:
```bash
openclaw channels status
# Should show: Google Chat default: enabled, configured, ...
```
### Other issues
- `openclaw channels status --probe` surfaces auth errors and missing audience config (`audience` and `audienceType` are both required).
- If no messages arrive, confirm the Chat app's webhook URL and trigger configuration.
- If mention gating blocks replies, set `botUser` to the app's user resource name and check `requireMention`.
- `openclaw logs --follow` while sending a test message shows whether requests reach the gateway.
## Related
- [Channels Overview](/channels) — all supported channels
- [Channel Routing](/channels/channel-routing) — session routing for messages
- [Gateway configuration](/gateway/configuration)
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Reactions](/tools/reactions)
- [Security](/gateway/security) — access model and hardening

View File

@@ -0,0 +1,97 @@
---
summary: "WhatsApp group message handling — activation, allowlists, sessions, and context injection"
read_when:
- Configuring WhatsApp groups specifically
- Changing WhatsApp activation modes (`mention` vs `always`)
- Tuning WhatsApp group session keys or pending-message context
title: "WhatsApp group messages"
sidebarTitle: "WhatsApp groups"
---
For the cross-channel groups model (Discord, iMessage, Matrix, Microsoft Teams, QQBot, Signal, Slack, Telegram, WhatsApp, Zalo), see [Groups](/channels/groups). This page covers the WhatsApp-specific behavior on top of that model: activation, group allowlists, per-group session keys, and pending-message context injection.
Goal: let OpenClaw 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 shared with the other channels' mention gating. For multi-agent setups, set it per agent, or use `messages.groupChat.mentionPatterns` as a global fallback. With neither set, patterns are derived from the agent identity name/emoji.
</Note>
## Behavior
- Activation modes: `mention` (default) or `always`. `mention` requires a ping: a real WhatsApp @-mention (`mentionedJids`), a configured regex pattern, the bot's E.164 digits anywhere in the text, or a quoted reply to one of the bot's messages (except shared-number self-chat setups). `always` wakes the agent on every message, but the injected group prompt tells it to reply only when it adds value and to return the exact silent token `NO_REPLY` (case-insensitive) otherwise. Defaults come from config (`channels.whatsapp.groups` `requireMention`) and can be overridden per group via `/activation`.
- Group allowlist: when `channels.whatsapp.groups` is set, only listed group JIDs are admitted (include `"*"` to allow all); messages from unlisted groups are dropped with a log hint.
- 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:<agentId>:whatsapp:group:<jid>` (non-default accounts append `:thread:whatsapp-account-<accountId>`), so directives such as `/verbose on`, `/trace on`, or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched.
- 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]`. The pending window is cleared after the run; messages already in the session are not re-injected.
- Sender attribution: each group line carries the sender label inside the message envelope, e.g. `[WhatsApp <groupJid> <timestamp>] Alice (+447700900123): text`, and sender identity plus group subject/members ride along in the untrusted conversation-metadata block.
- Ephemeral/view-once: wrappers are unwrapped before extracting text/mentions, so pings inside them still trigger.
- Group system prompt: the first turn of a group session (and any turn after `/activation` changes the mode) injects activation guidance into the system prompt (`Activation: trigger-only ...` or `Activation: always-on ...`, plus "address the specific sender"). Persistent group-chat delivery guidance ("You are in a WhatsApp group chat...") is always included.
## Config example (WhatsApp)
Make display-name pings work even when WhatsApp strips the visual `@` from the text body:
```json5
{
channels: {
whatsapp: {
groups: {
"*": { requireMention: true },
},
historyLimit: 50, // pending group context window (default 50)
},
},
agents: {
list: [
{
id: "main",
groupChat: {
mentionPatterns: ["@?openclaw", "\\+?15555550123"],
},
},
],
},
}
```
Notes:
- The regexes are case-insensitive and use the same safe-regex guardrails as other config regex surfaces; invalid patterns and unsafe nested repetition are ignored.
- 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.
- The pending-context window resolves as `channels.whatsapp.accounts.<id>.historyLimit``channels.whatsapp.historyLimit``messages.groupChat.historyLimit` → 50.
### Activation command (owner-only)
Use the group chat command:
- `/activation mention`
- `/activation always`
Only owner numbers (from `channels.whatsapp.allowFrom`, or the bot's own E.164 when unset) can change this; `/activation` from anyone else is ignored and stored as context only. 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 includes the pending group context plus sender-labeled lines so it can address the right person.
4. Session directives (`/verbose on`, `/trace 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 stays 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`) for `inbound web message` entries showing `from: <groupJid>` and the sender-labeled body.
## Known considerations
- Heartbeats run in the agent's main session; group sessions never get heartbeat runs.
- Echo suppression remembers the combined prompt (history + current message) per session so the bot's own delivered messages do not retrigger it; an identical repeated batch can be skipped as an echo.
- Session store entries appear as `agent:<agentId>:whatsapp:group:<jid>` in the session store (`~/.openclaw/agents/<agentId>/sessions/sessions.json` by default); a missing entry just means the group has not triggered a run yet.
- Typing indicators follow `session.typingMode` / `agents.defaults.typingMode`. When visible replies are opted into message-tool-only mode, typing starts immediately by default so group members can see the agent working even if no automatic final reply is posted. Explicit typing-mode config still wins.
## Related
- [Groups](/channels/groups)
- [Channel routing](/channels/channel-routing)
- [Broadcast groups](/channels/broadcast-groups)

578
docs/channels/groups.md Normal file
View File

@@ -0,0 +1,578 @@
---
summary: "Group chat behavior across surfaces (Discord/iMessage/Matrix/Microsoft Teams/QQBot/Signal/Slack/Telegram/WhatsApp/Zalo)"
read_when:
- Changing group chat behavior or mention gating
- Scoping mentionPatterns to specific group conversations
title: "Groups"
sidebarTitle: "Groups"
---
OpenClaw applies the same group rules across group-capable channels, including Discord, iMessage, Matrix, Microsoft Teams, QQBot, Signal, Slack, Telegram, WhatsApp, and Zalo.
For always-on rooms that should provide quiet context unless the agent explicitly sends a visible message, see [Ambient room events](/channels/ambient-room-events).
## 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"`); group senders are blocked until allowlisted.
- Replies require a mention unless you disable mention gating for a group.
- Final reply text posts to the room automatically (`visibleReplies: "automatic"`).
Translation: allowlisted senders can trigger OpenClaw by mentioning it.
<Note>
**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`).
</Note>
Quick flow (what happens to a group message):
```text
groupPolicy? disabled -> drop
groupPolicy? allowlist -> group allowed? no -> drop
requireMention? yes -> mentioned? no -> store for context only
mention/reply/command/DM -> user request
always-on group chatter -> user request, or room event when configured
```
## Visible replies
For normal group/channel requests, OpenClaw defaults to `messages.groupChat.visibleReplies: "automatic"`: the final assistant text posts to the room as the visible reply.
Use `messages.groupChat.visibleReplies: "message_tool"` when a shared room should let the agent decide when to speak by calling `message(action=send)`. This works best with tool-reliable models (for example GPT 5.5). If the model misses the tool and returns substantive final text, OpenClaw keeps that text private instead of posting it to the room.
Use `"automatic"` for models or runtimes that do not reliably follow tool-only delivery: normal text finals post directly to the room, and the agent may still call `message(action=send)` for files, images, or other attachments that cannot ride along with the final text.
If the message tool is unavailable under the active tool policy, OpenClaw falls back to automatic visible replies instead of silently suppressing the response. `openclaw doctor` warns about this mismatch.
For direct chats and any other source event, `messages.visibleReplies: "message_tool"` applies the same tool-only behavior globally; `messages.groupChat.visibleReplies` remains the more specific override for group/channel rooms. Internal WebChat direct turns default to automatic final-reply delivery so Pi and Codex receive the same visible-reply contract.
Tool-only mode replaces the old pattern of forcing the model to answer `NO_REPLY` for most lurk-mode turns. In tool-only mode the prompt does not define a `NO_REPLY` contract; doing nothing visible simply means not calling the message tool.
Plugin-owned conversation bindings are the exception. Once a plugin binds a thread and claims the inbound turn, the plugin's returned reply is the visible binding response; it does not need `message(action=send)`. That reply is plugin runtime output, not private model final text.
Typing indicators are still sent for direct group requests. Ambient always-on room events, when enabled, stay strict and quiet unless the agent calls the message tool.
Sessions suppress verbose tool/progress summaries by default. Use `/verbose on` (or `/verbose full`) to show them for the current session while debugging, and `/verbose off` to return to final-reply-only behavior. Verbose state is per session and works the same in direct chats, groups, channels, and forum topics.
To submit unmentioned always-on group chatter as quiet room context instead of user requests, use [Ambient room events](/channels/ambient-room-events):
```json5
{
messages: {
groupChat: {
unmentionedInbound: "room_event",
},
},
}
```
The default is `unmentionedInbound: "user_request"`. Mentioned messages, commands, abort requests, and DMs stay user requests.
To require visible output to go through the message tool for group/channel requests:
```json5
{
messages: {
groupChat: {
visibleReplies: "message_tool",
},
},
}
```
To require it for every source chat:
```json5
{
messages: {
visibleReplies: "message_tool",
},
}
```
The gateway picks up `messages` config changes without a restart after the file is saved. Restart only when config reload is disabled (`gateway.reload.mode: "off"`).
Command turns bypass `visibleReplies: "message_tool"` and always reply visibly: native slash commands (Discord, Telegram, and other surfaces with native command support) and authorized text `/...` commands both post their response to the source chat. Unauthorized text `/...` turns in groups stay message-tool-only; ordinary chat turns follow the configured default.
## Context visibility and allowlists
Two different controls are involved in group safety:
- **Trigger authorization**: who can trigger the agent (`groupPolicy`, `groups`, `groupAllowFrom`, channel-specific allowlists).
- **Context visibility**: what supplemental context is injected into the model (reply/quote text, thread history, forwarded metadata).
By default OpenClaw keeps context as received: allowlists decide who can trigger actions, not what quoted or historical snippets the model sees. To also filter supplemental context, set `contextVisibility`:
| Mode | Behavior |
| ------------------- | -------------------------------------------------------------------------------- |
| `"all"` (default) | Keep supplemental context as received. |
| `"allowlist"` | Only inject history/thread/quote/forwarded context from allowlisted senders. |
| `"allowlist_quote"` | `allowlist`, plus keep the explicitly quoted/replied-to message from any sender. |
Set it per channel (`channels.<channel>.contextVisibility`), per account (`channels.<channel>.accounts.<accountId>.contextVisibility`), or globally (`channels.defaults.contextVisibility`). Channels that fetch supplemental context (Discord, Feishu, iMessage, Matrix, Microsoft Teams, Signal, Slack, Telegram, WhatsApp) apply the policy when building inbound context; unknown policy combinations fail closed and omit the context.
![Group message flow](/images/groups-flow.svg)
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: { "<group-id>": { ... } }` (no `"*"` key) |
| Only you can trigger in groups | `groupPolicy: "allowlist"`, `groupAllowFrom: ["+1555..."]` |
| Reuse one trusted sender set across channels | `groupAllowFrom: ["accessGroup:operators"]` |
For reusable sender allowlists, see [Access groups](/channels/access-groups).
## Session keys
- Group sessions use `agent:<agentId>:<channel>:group:<id>` session keys (rooms/channels use `agent:<agentId>:<channel>:channel:<id>`).
- Telegram forum topics add `:topic:<threadId>` to the group id so each topic has its own session.
- Direct chats use the main session (or per-sender sessions if `session.dmScope` is configured).
- Heartbeats run in the configured heartbeat session (default: the agent main session); group sessions do not run their own heartbeats.
<a id="pattern-personal-dms-public-groups-single-agent"></a>
## 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:<channel>:group:<id>`). If you enable sandboxing with `mode: "non-main"`, those group sessions run in the configured sandbox backend while your main DM session stays on-host. Docker is the default backend if you do not choose one.
This gives you one agent "brain" (shared workspace + memory), but two execution postures:
- **DMs**: full tools (host)
- **Groups**: sandbox + restricted tools
<Note>
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).
</Note>
<Tabs>
<Tab title="DMs on host, groups sandboxed">
```json5
{
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"],
},
},
},
}
```
</Tab>
<Tab title="Groups see only an allowlisted folder">
Want "groups can only see folder X" instead of "no host access"? Keep `workspaceAccess: "none"` and mount only allowlisted paths into the sandbox:
```json5
{
agents: {
defaults: {
sandbox: {
mode: "non-main",
scope: "session",
workspaceAccess: "none",
docker: {
binds: [
// hostPath:containerPath:mode
"/home/user/FriendsShared:/data:ro",
],
},
},
},
},
}
```
</Tab>
</Tabs>
Related:
- Configuration keys and defaults: [Gateway configuration](/gateway/config-agents#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 `<channel>:<token>`.
- `#room` is reserved for rooms/channels; group chats use `g-<slug>` (lowercase, spaces -> `-`, keep `#@+._-`). Very long opaque ids are shortened into a stable token instead of leaking full route ids into the UI.
## Group policy
Control how group/room messages are handled per channel:
```json5
{
channels: {
whatsapp: {
groupPolicy: "disabled", // "open" | "disabled" | "allowlist"
groupAllowFrom: ["+15551234567"],
},
telegram: {
groupPolicy: "disabled",
groupAllowFrom: ["123456789"], // numeric Telegram user id (setup resolves @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: { enabled: true } } },
},
},
slack: {
groupPolicy: "allowlist",
channels: { "#general": { enabled: true } },
},
matrix: {
groupPolicy: "allowlist",
groupAllowFrom: ["@owner:example.org"],
groups: {
"!roomId:example.org": { enabled: true },
"#alias:example.org": { enabled: 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. |
<AccordionGroup>
<Accordion title="Per-channel notes">
- `groupPolicy` is separate from mention-gating (which requires @mentions).
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams/Zalo: use `groupAllowFrom` (fallback: explicit `allowFrom`).
- Signal: `groupAllowFrom` can match either the inbound Signal group id or the sender phone/UUID.
- 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.<id>.channels`.
- Slack: allowlist uses `channels.slack.channels`.
- Matrix: allowlist uses `channels.matrix.groups`. Use room IDs (`!room:server`) or aliases (`#alias:server`); room-name keys match only with `channels.matrix.dangerouslyAllowNameMatching: true`, and unresolved entries are ignored at runtime. 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.*`: `groupEnabled`, `groupChannels`).
- Telegram: sender allowlists accept numeric user IDs only (`"123456789"`; `telegram:`/`tg:` prefixes are stripped case-insensitively). `@username` entries do not match at runtime and log a warning; setup resolves `@username` to IDs. Negative chat IDs belong under `channels.telegram.groups`, not sender allowlists.
- Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked.
- Runtime safety: when a provider block is completely missing (`channels.<provider>` absent), group policy fails closed to `allowlist` instead of inheriting `channels.defaults.groupPolicy`, and the gateway logs the fallback once per account.
</Accordion>
</AccordionGroup>
Quick mental model (evaluation order for group messages):
<Steps>
<Step title="groupPolicy">
`groupPolicy` (open/disabled/allowlist).
</Step>
<Step title="Group allowlists">
Group allowlists (`*.groups`, `*.groupAllowFrom`, channel-specific allowlist).
</Step>
<Step title="Mention gating">
Mention gating (`requireMention`, `/activation`).
</Step>
</Steps>
## 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 exposes reply metadata; quoting a bot message can also count on channels that expose quote metadata. Current built-in cases: Discord, Microsoft Teams, QQBot, Slack, Telegram, WhatsApp, and Zalo personal.
```json5
{
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,
},
},
],
},
}
```
## Scope configured mention patterns
Configured `mentionPatterns` are regex fallback triggers. Use them when the platform does not expose a native bot mention, or when plain text such as `openclaw:` should count as a mention. Native platform mentions are separate: when Discord, Slack, Telegram, Matrix, or another channel can prove the message explicitly mentioned the bot, that native mention still triggers even where configured regex patterns are denied.
By default, configured mention patterns apply everywhere the channel passes provider and conversation facts into mention detection. To keep broad patterns from waking the agent in every group, scope them per channel with `channels.<channel>.mentionPatterns`.
Use `mode: "deny"` when regex mention patterns should be off by default for a channel, then opt in specific rooms with `allowIn`:
```json5
{
messages: {
groupChat: {
mentionPatterns: ["\\bopenclaw\\b", "\\bops bot\\b"],
},
},
channels: {
slack: {
mentionPatterns: {
mode: "deny",
allowIn: ["C0123OPS"],
},
},
},
}
```
Use the default `mode: "allow"` (or omit `mode`) when regex mention patterns should apply broadly, then turn them off in noisy rooms with `denyIn`:
```json5
{
messages: {
groupChat: {
mentionPatterns: ["\\bopenclaw\\b"],
},
},
channels: {
telegram: {
mentionPatterns: {
denyIn: ["-1001234567890", "-1001234567890:topic:42"],
},
},
},
}
```
Policy resolution:
| Field | Effect |
| --------------- | --------------------------------------------------------------------------------------------------------------------- |
| `mode: "allow"` | Regex mention patterns are enabled unless the conversation ID is in `denyIn`. This is the default. |
| `mode: "deny"` | Regex mention patterns are disabled unless the conversation ID is in `allowIn`. |
| `allowIn` | Conversation IDs where regex mention patterns are enabled in deny mode. |
| `denyIn` | Conversation IDs where regex mention patterns are disabled. `denyIn` wins over `allowIn` if both include the same ID. |
Supported scoped regex policy today:
| Channel | IDs used in `allowIn` / `denyIn` |
| -------- | ------------------------------------------------------------ |
| Discord | Discord channel IDs. |
| Matrix | Matrix room IDs. |
| Slack | Slack channel IDs. |
| Telegram | Group chat IDs, or `chatId:topic:threadId` for forum topics. |
| WhatsApp | WhatsApp conversation IDs such as `123@g.us`. |
Account-level channel configs can set the same policy under `channels.<channel>.accounts.<accountId>.mentionPatterns` when that channel supports multiple accounts. Account policy takes precedence over the top-level channel policy for that account.
<AccordionGroup>
<Accordion title="Mention gating notes">
- `mentionPatterns` are case-insensitive safe regex patterns; invalid patterns and unsafe nested-repetition forms are ignored (with a warning).
- Pattern precedence: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group) overrides `messages.groupChat.mentionPatterns`; when neither is set, patterns are derived from the agent identity name/emoji.
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
- Allowlisting a group or sender does not disable mention gating; set that group's `requireMention` to `false` when all messages should trigger.
- Automatic group chat prompt context carries the resolved silent-reply instruction every turn; workspace files should not duplicate `NO_REPLY` mechanics.
- Groups where automatic silent replies are allowed treat clean empty or reasoning-only model turns as silent, equivalent to `NO_REPLY`. Direct chats never receive `NO_REPLY` guidance, and message-tool-only group replies stay quiet by not calling `message(action=send)`.
- Ambient always-on group chatter uses user-request semantics by default. Set `messages.groupChat.unmentionedInbound: "room_event"` to submit it as quiet context instead. See [Ambient room events](/channels/ambient-room-events) for setup examples.
- Room events are not stored as fake user requests, and private assistant text from no-message-tool room events is not replayed as chat history.
- Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
- Group history context is wrapped uniformly across channels. Mention-gated groups keep pending skipped messages; always-on groups may also retain recent processed room messages when the channel supports it. Use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
</Accordion>
</AccordionGroup>
## 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 (`allow`, `alsoAllow`, `deny`; deny wins).
- `toolsBySender`: per-sender overrides within the group. Use explicit key prefixes: `channel:<channelId>:<senderId>`, `id:<senderId>`, `e164:<phone>`, `username:<handle>`, `name:<displayName>`, and `"*"` wildcard. Channel ids use canonical OpenClaw channel ids; aliases such as `teams` normalize to `msteams`. Legacy unprefixed keys are still accepted, matched as `id:` only, and log a deprecation warning.
Resolution order (most specific wins):
<Steps>
<Step title="Group toolsBySender">
Group/channel `toolsBySender` match.
</Step>
<Step title="Group tools">
Group/channel `tools`.
</Step>
<Step title="Default toolsBySender">
Default (`"*"`) `toolsBySender` match.
</Step>
<Step title="Default tools">
Default (`"*"`) `tools`.
</Step>
</Steps>
Example (Telegram):
```json5
{
channels: {
telegram: {
groups: {
"*": { tools: { deny: ["exec"] } },
"-1001234567890": {
tools: { deny: ["exec", "read", "write"] },
toolsBySender: {
"id:123456789": { alsoAllow: ["exec"] },
},
},
},
},
},
}
```
<Note>
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.*`, Microsoft Teams `teams.*.channels.*`).
</Note>
## 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.
<Warning>
Common confusion: DM pairing approval is not the same as group authorization. For channels that support DM pairing, the pairing store unlocks DMs only. Group commands still require explicit group sender authorization from config allowlists such as `groupAllowFrom` or the documented config fallback for that channel.
</Warning>
Common intents (copy/paste):
<Tabs>
<Tab title="Disable all group replies">
```json5
{
channels: { whatsapp: { groupPolicy: "disabled" } },
}
```
</Tab>
<Tab title="Allow only specific groups (WhatsApp)">
```json5
{
channels: {
whatsapp: {
groups: {
"123@g.us": { requireMention: true },
"456@g.us": { requireMention: false },
},
},
},
}
```
</Tab>
<Tab title="Allow all groups but require mention">
```json5
{
channels: {
whatsapp: {
groups: { "*": { requireMention: true } },
},
},
}
```
</Tab>
<Tab title="Owner-only triggers (WhatsApp)">
```json5
{
channels: {
whatsapp: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"],
groups: { "*": { requireMention: true } },
},
},
}
```
</Tab>
</Tabs>
## Activation (owner-only)
Group owners can toggle per-group activation with a standalone message:
- `/activation mention`
- `/activation always`
`/activation` is a core owner-gated command and only applies in group chats. Owner means the sender matches the channel's `allowFrom` / `commands.ownerAllowFrom` (when no allowlist is configured, the account's own id counts as owner). The stored mode overrides that group's `requireMention` on channels that consult it (Google Chat, QQBot, Telegram, WhatsApp), and the group system-prompt intro reflects the active mode everywhere.
## 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 (and after `/activation` changes). It reminds the model to respond like a human, minimize empty lines and follow normal chat spacing, and avoid typing literal `\n` sequences. Non-Telegram groups also discourage Markdown tables; Telegram rich-text guidance comes from the Telegram channel prompt. Channel-sourced group names and participant labels are rendered as fenced untrusted metadata, not inline system instructions.
## iMessage specifics
- Prefer `chat_id:<id>` when routing or allowlisting.
- List chats: `imsg chats --limit 20`.
- Group replies always go back to the same `chat_id`.
## WhatsApp system prompts
See [WhatsApp](/channels/whatsapp#system-prompts) for the canonical WhatsApp system prompt rules, including group and direct prompt resolution, wildcard behavior, and account override semantics.
## WhatsApp specifics
See [Group messages](/channels/group-messages) for WhatsApp-only behavior (history injection, mention handling details).
## Related
- [Broadcast groups](/channels/broadcast-groups)
- [Channel routing](/channels/channel-routing)
- [Group messages](/channels/group-messages)
- [Pairing](/channels/pairing)

View File

@@ -0,0 +1,230 @@
---
summary: "Translate old BlueBubbles configs to the bundled iMessage plugin: key mapping, group allowlist gates, and cutover verification."
read_when:
- Planning a move from BlueBubbles to the bundled iMessage plugin
- Translating BlueBubbles config keys to iMessage equivalents
- Verifying imsg before enabling the iMessage plugin
title: "Coming from BlueBubbles"
---
BlueBubbles support was removed. OpenClaw supports iMessage only through the bundled `imessage` plugin, which drives [`steipete/imsg`](https://github.com/steipete/imsg) over JSON-RPC and reaches the same private API surface BlueBubbles had (`react`, `edit`, `unsend`, `reply`, `sendWithEffect`, native polls, group management, attachments). One CLI binary replaces the BlueBubbles server + client app + webhook plumbing: no REST endpoint, no webhook auth.
This guide migrates old `channels.bluebubbles` configs to `channels.imessage`. There is no other supported migration path. On current OpenClaw a leftover `channels.bluebubbles` block is inert — no runtime reads it.
<Note>
For the short announcement and operator summary, see [BlueBubbles removal and the imsg iMessage path](/announcements/bluebubbles-imessage).
</Note>
## Migration checklist
The shortest safe path when you already know your old BlueBubbles config:
1. Verify `imsg` directly on the Mac that runs Messages.app (`imsg chats`, `imsg history`, `imsg send`, `imsg rpc --help`).
2. Copy behavior keys from `channels.bluebubbles` to `channels.imessage`: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`, `includeAttachments`, `attachmentRoots`, `mediaMaxMb`, `textChunkLimit`, `coalesceSameSenderDms`, and `actions`.
3. Drop transport keys that no longer exist: `serverUrl`, `password`, webhook URLs, and BlueBubbles server setup.
4. If the Gateway is not running on the Messages Mac, set `channels.imessage.cliPath` to an SSH wrapper and set `remoteHost` for remote attachment fetches.
5. Enable `channels.imessage`, restart the Gateway, then run `openclaw channels status --probe --channel imessage`.
6. Test one DM, one allowed group, attachments if enabled, and every private API action you expect the agent to use.
7. Delete the BlueBubbles server and the old `channels.bluebubbles` config after the iMessage path is verified.
## What imsg does
`imsg` is a local macOS CLI for Messages. OpenClaw starts `imsg rpc` as a child process and talks JSON-RPC over stdin/stdout. There is no HTTP server, webhook URL, background daemon, launch agent, or port to expose.
- Reads come from `~/Library/Messages/chat.db` using a read-only SQLite handle.
- Live inbound messages come from `imsg watch` / `watch.subscribe`, which follows `chat.db` filesystem events with a polling fallback.
- Sends use Messages.app automation for normal text and file sends.
- Advanced actions use `imsg launch` to inject the `imsg` helper into Messages.app. That is what unlocks read receipts, typing indicators, rich sends, edit, unsend, threaded reply, tapbacks, polls, and group management.
- Linux builds can inspect a copied `chat.db`, but cannot send, watch the live Mac database, or drive Messages.app. For OpenClaw iMessage, run `imsg` on the signed-in Mac or through an SSH wrapper to that Mac.
## Before you start
1. Install `imsg` on the Mac that runs Messages.app:
```bash
brew install steipete/tap/imsg
imsg --version
imsg chats --limit 3
```
If `imsg chats` fails with `unable to open database file`, empty output, or `authorization denied`, grant Full Disk Access to the terminal, editor, Node process, Gateway service, or SSH parent process that launches `imsg`, then reopen that parent process.
2. Verify the read, watch, send, and RPC surfaces before changing OpenClaw config:
```bash
imsg chats --limit 10 --json | jq -s
imsg history --chat-id 42 --limit 10 --attachments --json | jq -s
imsg watch --chat-id 42 --reactions --json
imsg send --chat-id 42 --text "OpenClaw imsg test"
imsg rpc --help
```
Replace `42` with a real chat id from `imsg chats`. Sending requires Automation permission for Messages.app. If OpenClaw will run through SSH, run these commands through the same SSH wrapper or user context that OpenClaw will use. If reads work but sends fail with AppleEvents `-1743`, check whether Automation landed on `/usr/libexec/sshd-keygen-wrapper`; see [SSH wrapper sends fail with AppleEvents -1743](/channels/imessage#requirements-and-permissions-macos).
3. Enable the private API bridge when you need advanced actions:
```bash
imsg launch
imsg status --json
```
`imsg launch` requires SIP to be disabled (and on modern macOS, library validation relaxed — see [Enabling the imsg private API](/channels/imessage#enabling-the-imsg-private-api)). Basic send, history, and watch work without `imsg launch`; advanced actions do not.
4. After you enable `channels.imessage` and start the Gateway, verify the bridge through OpenClaw:
```bash
openclaw channels status --probe
```
The iMessage account should report `works`; with `--json`, the probe payload includes `privateApi.available: true`. If it reports `false`, fix that first — see [Capability detection](/channels/imessage#private-api-actions). Probing needs a reachable Gateway (the CLI falls back to config-only output otherwise) and only probes configured, enabled accounts.
5. Snapshot your config:
```bash
cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.bak
```
## Config translation
iMessage and BlueBubbles share most channel-level behavior keys. What changes is transport (REST server vs local CLI) and the group registry key format.
| BlueBubbles | bundled iMessage | Notes |
| ---------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `channels.bluebubbles.enabled` | `channels.imessage.enabled` | Same semantics (default `true` once the block exists). |
| `channels.bluebubbles.serverUrl` | _(removed)_ | No REST server — the plugin spawns `imsg rpc` over stdio. |
| `channels.bluebubbles.password` | _(removed)_ | No webhook authentication needed. |
| _(implicit)_ | `channels.imessage.cliPath` | Path to `imsg` (default `imsg`); use a wrapper script for SSH. |
| _(implicit)_ | `channels.imessage.dbPath` | Optional Messages.app `chat.db` override; auto-detected when omitted. |
| _(implicit)_ | `channels.imessage.remoteHost` | `host` or `user@host` — only needed when `cliPath` is an SSH wrapper and you want SCP attachment fetches. |
| `channels.bluebubbles.dmPolicy` | `channels.imessage.dmPolicy` | Same values (`pairing` / `allowlist` / `open` / `disabled`); default `pairing`. |
| `channels.bluebubbles.allowFrom` | `channels.imessage.allowFrom` | Same handle formats (`+15555550123`, `user@example.com`). Pairing-store approvals do not transfer — see below. |
| `channels.bluebubbles.groupPolicy` | `channels.imessage.groupPolicy` | Same values (`allowlist` / `open` / `disabled`); default `allowlist`. |
| `channels.bluebubbles.groupAllowFrom` | `channels.imessage.groupAllowFrom` | Same. Does not fall back to `allowFrom` — an empty `groupAllowFrom` under `groupPolicy: "allowlist"` drops all groups regardless of `allowFrom`. |
| `channels.bluebubbles.groups` | `channels.imessage.groups` | Copy the `"*"` wildcard entry verbatim; re-key per-group entries by numeric iMessage `chat_id` — see "Group registry footgun". `requireMention`, `tools`, `toolsBySender`, `systemPrompt` carry over. |
| `channels.bluebubbles.sendReadReceipts` | `channels.imessage.sendReadReceipts` | Default `true`. With the bundled plugin this only fires when the private API probe is up. |
| `channels.bluebubbles.includeAttachments` | `channels.imessage.includeAttachments` | Same shape, same off-by-default. If attachments flowed on BlueBubbles, set this explicitly — inbound photos/media are silently dropped (no `Inbound message` log line) until you do. |
| `channels.bluebubbles.attachmentRoots` | `channels.imessage.attachmentRoots` | Local roots; same wildcard rules. |
| _(N/A)_ | `channels.imessage.remoteAttachmentRoots` | Only used when `remoteHost` is set for SCP fetches. |
| `channels.bluebubbles.mediaMaxMb` | `channels.imessage.mediaMaxMb` | Default 16 MB on iMessage (BlueBubbles default was 8 MB). Set explicitly to keep the lower cap. |
| `channels.bluebubbles.textChunkLimit` | `channels.imessage.textChunkLimit` | Default 4000 on both. |
| `channels.bluebubbles.coalesceSameSenderDms` | `channels.imessage.coalesceSameSenderDms` | Same opt-in. DM-only — groups keep per-message dispatch. Widens the default inbound debounce to 7000 ms unless `messages.inbound.byChannel.imessage` or a global `messages.inbound.debounceMs` is set. See [Coalescing split-send DMs](/channels/imessage#coalescing-split-send-dms-command--url-in-one-composition). |
| `channels.bluebubbles.enrichGroupParticipantsFromContacts` | _(N/A)_ | `imsg` already surfaces sender display names from `chat.db`. |
| `channels.bluebubbles.actions.*` | `channels.imessage.actions.*` | Same per-action toggles (`reactions`, `edit`, `unsend`, `reply`, `sendWithEffect`, `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, `sendAttachment`) plus new `polls`. All default to enabled; private API actions still require the bridge. |
Multi-account configs (`channels.bluebubbles.accounts.*`) translate one-to-one to `channels.imessage.accounts.*`.
## Group registry footgun
The bundled iMessage plugin runs two group gates back to back. A group message must pass both to reach the agent:
1. **Sender / chat-target allowlist** (`channels.imessage.groupAllowFrom`) — matches the sender handle or the chat target (`chat_id:`, `chat_guid:`, `chat_identifier:` entries). Does not fall back to `allowFrom`: with `groupPolicy: "allowlist"` and an empty `groupAllowFrom`, every group message drops here regardless of `allowFrom`.
2. **Group registry** (`channels.imessage.groups`) — keyed by numeric iMessage `chat_id`:
- No `groups` block (or an empty one): groups pass this gate as long as gate 1 has a non-empty `groupAllowFrom`; sender filtering governs access. The gateway still logs a startup warning nudging you to add a `groups` block.
- `groups` with entries but no `"*"`: only the listed `chat_id` keys pass. Listing any group turns the registry into an allowlist even under `groupPolicy: "open"`.
- `groups: { "*": { ... } }`: every group passes this gate.
The migration trap: BlueBubbles keyed `groups` entries by chat GUID / chat identifier, while the iMessage registry keys by numeric `chat_id`. Per-group entries copied verbatim create a non-empty registry whose keys never match, so every group message drops at gate 2. Copy the `"*"` wildcard verbatim; re-key specific group entries with `chat_id` values from `imsg chats`.
Both drop paths are visible at the default log level via `warn` lines:
- Once per account at startup, when `groupPolicy: "allowlist"` is set and `channels.imessage.groups` is empty: `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty for account "<id>"`.
- Once per `chat_id` at runtime, when the registry drops a group: `imessage: dropping group message from chat_id=<id> ... not in channels.imessage.groups allowlist`, naming the exact key to add.
DMs keep working either way — they take a different code path, so DM success does not prove group routing.
The safe minimum with `groupPolicy: "allowlist"`:
```json5
{
channels: {
imessage: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15555550123", "chat_guid:any;-;..."],
groups: {
"*": { requireMention: true },
},
},
},
}
```
`requireMention: true` under `"*"` is harmless when no mention patterns are configured: the runtime cannot detect mentions without patterns and skips the mention drop. With `agents.list[].groupChat.mentionPatterns` (fallback `messages.groupChat.mentionPatterns`) configured, mention gating applies as expected.
## Step-by-step
1. Translate the config. Keep the new block disabled while you edit; the old `channels.bluebubbles` block is ignored by current OpenClaw and can sit alongside as reference:
```json5
{
channels: {
imessage: {
enabled: false, // flip to true when ready to cut over
cliPath: "/opt/homebrew/bin/imsg",
dmPolicy: "pairing",
allowFrom: ["+15555550123"], // copy from bluebubbles.allowFrom
groupPolicy: "allowlist",
groupAllowFrom: [], // copy from bluebubbles.groupAllowFrom
groups: { "*": { requireMention: true } }, // wildcard copies verbatim; re-key per-chat entries by chat_id
// actions default to enabled; set individual toggles false to disable
},
},
}
```
2. **Cut over and probe.** Set `channels.imessage.enabled: true`, restart the Gateway, and confirm the channel reports healthy:
```bash
openclaw gateway restart
openclaw channels status --probe --channel imessage # expect "works"; --json shows privateApi.available: true
```
The probe requires a reachable Gateway and only probes configured, enabled accounts. Use the direct `imsg` commands in [Before you start](#before-you-start) to validate the Mac itself.
3. **Verify DMs.** Send the agent a direct message; confirm the reply lands.
4. **Verify groups separately.** DMs and groups take different code paths — DM success does not prove groups are routing. Send a message in an allowed group chat and confirm the reply lands. If the group goes silent (no agent reply, no error), check the gateway log for the two `warn` lines from "Group registry footgun" above; either one means the `groups` block is missing, empty, or keyed wrong.
5. **Verify the action surface.** From a paired DM, ask the agent to react, edit, unsend, reply, send a photo, and (in a group) rename the group or add/remove a participant. Each action should land natively in Messages.app. If any action throws `iMessage <action> requires the imsg private API bridge`, run `imsg launch` again and refresh with `openclaw channels status --probe`.
6. **Remove the BlueBubbles server and the `channels.bluebubbles` block** once iMessage DMs, groups, and actions are verified. OpenClaw does not read `channels.bluebubbles`.
## Action parity at a glance
| Action | legacy BlueBubbles | bundled iMessage |
| --------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------- |
| Send text / SMS fallback | ✅ | ✅ |
| Send media (photo, video, file, voice) | ✅ | ✅ |
| Threaded reply (`reply_to_guid`) | ✅ | ✅ (closes [#51892](https://github.com/openclaw/openclaw/issues/51892)) |
| Tapback (`react`) | ✅ | ✅ |
| Edit / unsend (macOS 13+ recipients) | ✅ | ✅ |
| Send with screen effect | ✅ | ✅ (closes part of [#9394](https://github.com/openclaw/openclaw/issues/9394)) |
| Rich text bold / italic / underline / strikethrough | ✅ | ✅ (typed-run formatting via attributedBody) |
| Native Messages polls (create and vote) | ❌ | ✅ (`actions.polls`; recipients need iOS/macOS 26+ for native rendering) |
| Rename group / set group icon | ✅ | ✅ |
| Add / remove participant, leave group | ✅ | ✅ |
| Read receipts and typing indicator | ✅ | ✅ (gated on private API probe) |
| Same-sender DM coalescing | ✅ | ✅ (DM-only; opt-in via `channels.imessage.coalesceSameSenderDms`) |
| Inbound recovery after a restart | ✅ | ✅ (automatic: `since_rowid` replay + GUID dedupe; wider window on local) |
iMessage recovers messages missed while the gateway was down: on startup it replays from the last dispatched rowid via `imsg watch.subscribe` `since_rowid`, dedupes by GUID, and a stale-backlog age fence suppresses the Push-flush "backlog bomb". This runs over the `imsg` RPC connection, so it works for remote SSH `cliPath` setups too; local setups get a wider recovery window because they can read `chat.db`. See [Inbound recovery after a bridge or gateway restart](/channels/imessage#inbound-recovery-after-a-bridge-or-gateway-restart).
## Pairing, sessions, and ACP bindings
- **Allowlists carry over by handle.** `channels.imessage.allowFrom` recognizes the same `+15555550123` / `user@example.com` strings BlueBubbles used — copy them verbatim.
- **Pairing-store approvals do not transfer.** The pairing store is per channel and nothing migrates the old BlueBubbles store. Senders who were approved only through pairing must pair once more under iMessage, or you add their handles to `allowFrom`.
- **Sessions** stay scoped per agent + chat. DMs collapse into the agent main session under default `session.dmScope=main`; group sessions stay isolated per `chat_id` (`agent:<agentId>:imessage:group:<chat_id>`). Old conversation history under BlueBubbles session keys does not carry into iMessage sessions.
- **ACP bindings** referencing `match.channel: "bluebubbles"` must change to `"imessage"`. The `match.peer.id` shapes (`chat_id:`, `chat_guid:`, `chat_identifier:`, bare handle) are identical.
## No rollback channel
There is no supported BlueBubbles runtime to switch back to. If iMessage verification fails, set `channels.imessage.enabled: false`, restart the Gateway, fix the `imsg` blocker, and retry the cutover.
The reply cache lives in SQLite plugin state. `openclaw doctor --fix` imports and archives the old `imessage/reply-cache.jsonl` sidecar when present.
## Related
- [BlueBubbles removal and the imsg iMessage path](/announcements/bluebubbles-imessage) — short announcement and operator summary.
- [iMessage](/channels/imessage) — full iMessage channel reference, including `imsg launch` setup and capability detection.
- `/channels/bluebubbles` — legacy URL that redirects to this migration guide.
- [Pairing](/channels/pairing) — DM authentication and pairing flow.
- [Channel Routing](/channels/channel-routing) — how the gateway picks a channel for outbound replies.

867
docs/channels/imessage.md Normal file
View File

@@ -0,0 +1,867 @@
---
summary: "Native iMessage support via imsg (JSON-RPC over stdio), with private API actions for replies, tapbacks, effects, polls, attachments, and group management. Preferred for new OpenClaw iMessage setups when host requirements fit."
read_when:
- Setting up iMessage support
- Debugging iMessage send/receive
title: "iMessage"
---
<Note>
For OpenClaw iMessage deployments, use `imsg` on a signed-in macOS Messages host. If your Gateway runs on Linux or Windows, point `channels.imessage.cliPath` at an SSH wrapper that runs `imsg` on the Mac.
**Inbound recovery is automatic.** After a bridge or gateway restart, iMessage replays the messages missed while it was down and suppresses the stale "backlog bomb" Apple can flush after a Push recovery, deduping so nothing is dispatched twice. There is no config to enable — see [Inbound recovery after a bridge or gateway restart](#inbound-recovery-after-a-bridge-or-gateway-restart).
</Note>
<Warning>
BlueBubbles support was removed. Migrate `channels.bluebubbles` configs to `channels.imessage`; OpenClaw supports iMessage through `imsg` only. Start with [BlueBubbles removal and the imsg iMessage path](/announcements/bluebubbles-imessage) for the short announcement, or [Coming from BlueBubbles](/channels/imessage-from-bluebubbles) for the full migration table.
</Warning>
Status: native external CLI integration. The Gateway spawns `imsg rpc` and speaks JSON-RPC over stdio — no separate daemon or port. Advanced actions require `imsg launch` and a successful private API probe.
<CardGroup cols={3}>
<Card title="Private API actions" icon="wand-sparkles" href="#private-api-actions">
Replies, tapbacks, effects, polls, attachments, and group management.
</Card>
<Card title="Pairing" icon="link" href="/channels/pairing">
iMessage DMs default to pairing mode.
</Card>
<Card title="Remote Mac" icon="terminal" href="#remote-mac-over-ssh">
Use an SSH wrapper when the Gateway is not running on the Messages Mac.
</Card>
<Card title="Configuration reference" icon="settings" href="/gateway/config-channels#imessage">
Full iMessage field reference.
</Card>
</CardGroup>
## Quick setup
<Tabs>
<Tab title="Local Mac (fast path)">
<Steps>
<Step title="Install and verify imsg">
```bash
brew install steipete/tap/imsg
imsg rpc --help
imsg launch
openclaw channels status --probe
```
</Step>
<Step title="Configure OpenClaw">
```json5
{
channels: {
imessage: {
enabled: true,
cliPath: "/usr/local/bin/imsg",
dbPath: "/Users/user/Library/Messages/chat.db",
},
},
}
```
</Step>
<Step title="Start gateway">
```bash
openclaw gateway
```
</Step>
<Step title="Approve first DM pairing (default dmPolicy)">
```bash
openclaw pairing list imessage
openclaw pairing approve imessage <CODE>
```
Pairing requests expire after 1 hour.
</Step>
</Steps>
</Tab>
<Tab title="Remote Mac over SSH">
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
#!/usr/bin/env bash
exec ssh -T gateway-host imsg "$@"
```
Recommended config when attachments are enabled:
```json5
{
channels: {
imessage: {
enabled: true,
cliPath: "~/.openclaw/scripts/imsg-ssh",
remoteHost: "user@gateway-host", // used for SCP attachment fetches
includeAttachments: true,
// Optional: extra allowed attachment roots (merged with the default
// /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); unsafe values are ignored.
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`).
<Warning>
Any `cliPath` wrapper or SSH proxy you put in front of `imsg` MUST behave like a transparent stdio pipe for long-lived JSON-RPC. OpenClaw exchanges small newline-framed JSON-RPC messages over the wrapper's stdin/stdout for the lifetime of the channel:
- Forward each stdin chunk/line **as soon as bytes are available** — don't wait for EOF.
- Forward each stdout chunk/line promptly in the reverse direction.
- Preserve newlines.
- Avoid fixed-size blocking reads (`read(4096)`, `cat | buffer`, default shell `read`) that can starve small frames.
- Keep stderr separate from the JSON-RPC stdout stream.
A wrapper that buffers stdin until a large block fills will produce symptoms that look like an iMessage outage — `imsg rpc timeout (chats.list)` or repeated channel restarts — even though `imsg rpc` itself is healthy. `ssh -T host imsg "$@"` (above) is safe because it forwards OpenClaw's `cliPath` arguments such as `rpc` and `--db`. Pipelines like `ssh host imsg | grep -v '^DEBUG'` are NOT — line-buffered tools can still hold frames; use `stdbuf -oL -eL` on every stage if you must filter.
</Warning>
</Tab>
</Tabs>
## 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.
- For advanced actions (react / edit / unsend / threaded reply / effects / polls / group ops), System Integrity Protection must be disabled — see [Enabling the imsg private API](#enabling-the-imsg-private-api). Basic text and media send/receive work without it.
<Tip>
Permissions are granted per process context. If the gateway runs headless (LaunchAgent/SSH), run a one-time interactive command in that same context to trigger prompts:
```bash
imsg chats --limit 1
# or
imsg send <handle> "test"
```
</Tip>
<Accordion title="SSH wrapper sends fail with AppleEvents -1743">
A remote-SSH setup can read chats, pass `channels status --probe`, and process inbound messages while outbound sends still fail with an AppleEvents authorization error:
```text
Not authorized to send Apple events to Messages. (-1743)
```
Check the signed-in Mac user's TCC database or System Settings > Privacy & Security > Automation. If the Automation entry is recorded for `/usr/libexec/sshd-keygen-wrapper` instead of the `imsg` or local shell process, macOS may not expose a usable Messages toggle for that SSH server-side client:
```text
kTCCServiceAppleEvents | /usr/libexec/sshd-keygen-wrapper | auth_value=0 | com.apple.MobileSMS
```
In that state, repeating `tccutil reset AppleEvents` or rerunning `imsg send` through the same SSH wrapper may keep failing because the process context that needs Messages Automation is the SSH wrapper, not an app the UI can grant.
Use one of the supported `imsg` process contexts instead:
- Run the Gateway, or at least the `imsg` bridge, in the logged-in Messages user's local session.
- Start the Gateway with a LaunchAgent for that user after granting Full Disk Access and Automation from the same session.
- If you keep the two-user SSH topology, verify that a real outbound `imsg send` succeeds through the exact wrapper before enabling the channel. If it cannot be granted Automation, reconfigure to a single-user `imsg` setup instead of relying on the SSH wrapper for sends.
</Accordion>
## Enabling the imsg private API
`imsg` ships in two operational modes:
- **Basic mode** (default, no SIP changes needed): outbound text and media via `send`, inbound watch/history, chat list. This is what you get out of the box from a fresh `brew install steipete/tap/imsg` plus the standard macOS permissions above.
- **Private API mode**: `imsg` injects a helper dylib into `Messages.app` to call internal `IMCore` functions. This unlocks `react`, `edit`, `unsend`, `reply` (threaded), `sendWithEffect`, `poll` and `poll-vote` (native Messages polls), `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, plus typing indicators and read receipts.
The advanced action surface on this page requires Private API mode. The `imsg` README is explicit about the requirement:
> Advanced features such as `read`, `typing`, `launch`, bridge-backed rich send, message mutation, and chat management are opt-in. They require SIP to be disabled and a helper dylib to be injected into `Messages.app`. `imsg launch` refuses to inject when SIP is enabled.
The helper-injection technique uses `imsg`'s own dylib to reach Messages private APIs. There is no third-party server or BlueBubbles runtime in the OpenClaw iMessage path.
<Warning>
**Disabling SIP is a real security tradeoff.** SIP is one of macOS's core protections against running modified system code; turning it off system-wide opens up additional attack surface and side effects. Notably, **disabling SIP on Apple Silicon Macs also disables the ability to install and run iOS apps on your Mac**.
Treat this as a deliberate operational choice, not a default. If your threat model cannot tolerate SIP being off, bundled iMessage is limited to basic mode — text and media send/receive only, no reactions / edit / unsend / effects / group ops.
</Warning>
### Setup
1. **Install (or upgrade) `imsg`** on the Mac that runs Messages.app:
```bash
brew install steipete/tap/imsg
imsg --version
imsg status --json
```
The `imsg status --json` output reports `bridge_version`, `rpc_methods`, and per-method `selectors` so you can see what the current build supports before you start.
2. **Disable System Integrity Protection, and (on modern macOS) Library Validation.** Injecting a non-Apple helper dylib into the Apple-signed `Messages.app` needs SIP off **and** library validation relaxed. The Recovery-mode SIP step is macOS-version-specific:
- **macOS 10.13-10.15 (Sierra-Catalina):** disable Library Validation via Terminal, reboot to Recovery Mode, run `csrutil disable`, restart.
- **macOS 11+ (Big Sur and later), Intel:** Recovery Mode (or Internet Recovery), `csrutil disable`, restart.
- **macOS 11+, Apple Silicon:** power-button startup sequence to enter Recovery; on recent macOS versions hold the **Left Shift** key when you click Continue, then `csrutil disable`. Virtual-machine setups follow a separate flow, so take a VM snapshot first.
**On macOS 11 and later, `csrutil disable` alone is usually not enough.** Apple still enforces library validation against `Messages.app` as a platform binary, so an adhoc-signed helper is rejected (`Library Validation failed: ... platform binary, but mapped file is not`) even with SIP off. After disabling SIP, also disable library validation and reboot:
```bash
sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist DisableLibraryValidation -bool true
```
**macOS 26 (Tahoe), verified on 26.5.1:** SIP off **plus** the `DisableLibraryValidation` command above is sufficient to inject the helper across 26.0 through 26.5.x. **No boot-args are required.** The plist is the decisive factor and the most common missing step when injection fails on Tahoe:
- **With the plist:** `imsg launch` injects and `imsg status` reports `advanced_features: true`.
- **Without the plist (even with SIP off):** `imsg launch` fails with `Failed to launch: Timeout waiting for Messages.app to initialize`. AMFI rejects the adhoc helper at load, so the bridge never becomes ready and the launch times out. That timeout is the symptom most people hit on Tahoe; the fix is the plist above, not anything more drastic.
If `imsg launch` injection or specific `selectors` start returning false after a macOS upgrade, this gate is the usual cause. Check your SIP and library-validation state before assuming the SIP step itself failed. If those settings are correct and the bridge still cannot inject, collect `imsg status --json` plus the `imsg launch` output and report it to the `imsg` project instead of weakening additional system-wide security controls.
3. **Inject the helper.** With SIP disabled and Messages.app signed in:
```bash
imsg launch
```
`imsg launch` refuses to inject when SIP is still enabled, so this also doubles as a confirmation that step 2 took.
4. **Verify the bridge from OpenClaw:**
```bash
openclaw channels status --probe
```
The iMessage entry should report `works`, and `imsg status --json | jq '{rpc_methods, selectors}'` should show the capabilities exposed by your macOS build. Poll creation requires `selectors.pollPayloadMessage`; voting requires both `selectors.pollVoteMessage` and the `poll.vote` RPC method. The OpenClaw plugin advertises only actions supported by the cached probe, while an empty cache stays optimistic and probes on first dispatch.
If `openclaw channels status --probe` reports the channel as `works` but specific actions throw "iMessage `<action>` requires the imsg private API bridge" at dispatch time, run `imsg launch` again — the helper can fall out (Messages.app restart, OS update, etc.) and the cached `available: true` status will keep advertising actions until the next probe refreshes.
### When SIP stays enabled
If disabling SIP is not acceptable for your threat model:
- `imsg` falls back to basic mode — text + media + receive only.
- The OpenClaw plugin still advertises text/media send and inbound monitoring; it hides `react`, `edit`, `unsend`, `reply`, `sendWithEffect`, and group ops from the action surface (per the per-method capability gate).
- You can run a separate non-Apple-Silicon Mac (or a dedicated bot Mac) with SIP off for the iMessage workload, while keeping SIP enabled on your primary devices. See [Dedicated bot macOS user (separate iMessage identity)](#deployment-patterns) below.
## Access control and routing
<Tabs>
<Tab title="DM policy">
`channels.imessage.dmPolicy` controls direct messages:
- `pairing` (default)
- `allowlist` (requires at least one `allowFrom` entry)
- `open` (requires `allowFrom` to include `"*"`)
- `disabled`
Allowlist field: `channels.imessage.allowFrom`.
Allowlist entries must identify senders: handles or static sender access groups (`accessGroup:<name>`). Use `channels.imessage.groupAllowFrom` for chat targets such as `chat_id:*`, `chat_guid:*`, or `chat_identifier:*`; use `channels.imessage.groups` for numeric `chat_id` registry keys.
</Tab>
<Tab title="Group policy + mentions">
`channels.imessage.groupPolicy` controls group handling:
- `allowlist` (default)
- `open`
- `disabled`
Group sender allowlist: `channels.imessage.groupAllowFrom`.
`groupAllowFrom` entries can also reference static sender access groups (`accessGroup:<name>`).
Runtime fallback: if `groupAllowFrom` is unset, iMessage group sender checks use `allowFrom`; set `groupAllowFrom` when DM and group admission should differ. An explicitly empty `groupAllowFrom: []` does not fall back — it blocks all group senders under `allowlist`.
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).
<Warning>
Group routing under `groupPolicy: "allowlist"` runs **two** gates back-to-back:
1. **Sender allowlist** (`channels.imessage.groupAllowFrom`) — handle, `accessGroup:<name>`, `chat_guid`, `chat_identifier`, or `chat_id`. Empty (and no `allowFrom` fallback) means every group message is dropped (`groupPolicy allowlist (empty groupAllowFrom)` in verbose logs).
2. **Group registry** (`channels.imessage.groups`) — enforced once the map has entries: the chat must match an explicit per-`chat_id` entry or a `groups: { "*": { ... } }` wildcard (`allowAll`). A group chat that matches neither is dropped. When `groups` is empty or missing, the sender allowlist alone decides admission.
The plugin emits `warn`-level signals at the default log level:
- one-time per account at startup when `groups` is empty: `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty for account "<id>"`
- one-time per `chat_id` when the registry gate drops a chat: `imessage: dropping group message from chat_id=<id> ...`
DMs are unaffected — they take a different code path.
Recommended config for group flow under `groupPolicy: "allowlist"`:
```json5
{
channels: {
imessage: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15555550123"],
groups: { "*": { "requireMention": true } },
},
},
}
```
If the per-`chat_id` drop lines appear in the gateway log, the registry gate is dropping that chat — add it to `groups` (or add the `"*"` wildcard).
</Warning>
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 bypass mention gating
Per-group `systemPrompt`:
Each entry under `channels.imessage.groups.*` accepts an optional `systemPrompt` string, injected into the agent's system prompt on every turn that handles a message in that group. Resolution mirrors `channels.whatsapp.groups`:
1. **Group-specific system prompt** (`groups["<chat_id>"].systemPrompt`): used when the specific group entry exists in the map **and** its `systemPrompt` key is defined. If `systemPrompt` is an empty string (`""`) the wildcard is suppressed and no system prompt is applied to that group.
2. **Group wildcard system prompt** (`groups["*"].systemPrompt`): used when the specific group entry is absent from the map entirely, or when it exists but defines no `systemPrompt` key.
```json5
{
channels: {
imessage: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15555550123"],
groups: {
"*": { systemPrompt: "Use British spelling." },
"8421": {
requireMention: true,
systemPrompt: "This is the on-call rotation chat. Keep replies under 3 sentences.",
},
"9907": {
// explicit suppression: the wildcard "Use British spelling." does not apply here
systemPrompt: "",
},
},
},
},
}
```
Per-group prompts only apply to group messages — direct messages are unaffected.
</Tab>
<Tab title="Sessions and deterministic replies">
- 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:<agentId>:imessage:group:<chat_id>`).
- 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).
</Tab>
</Tabs>
## ACP conversation bindings
iMessage chats can be bound to ACP sessions.
Fast operator flow:
- Run `/acp spawn codex --bind here` inside the DM or allowed group chat.
- Future messages in that same iMessage conversation route to the spawned ACP session.
- `/new` and `/reset` reset the same bound ACP session in place.
- `/acp close` closes the ACP session and removes the binding.
Configured persistent bindings use top-level `bindings[]` entries with `type: "acp"` and `match.channel: "imessage"`.
`match.peer.id` can use:
- normalized DM handle such as `+15555550123` or `user@example.com`
- `chat_id:<id>` (recommended for stable group bindings)
- `chat_guid:<guid>`
- `chat_identifier:<identifier>`
Example:
```json5
{
agents: {
list: [
{
id: "codex",
runtime: {
type: "acp",
acp: { agent: "codex", backend: "acpx", mode: "persistent" },
},
},
],
},
bindings: [
{
type: "acp",
agentId: "codex",
match: {
channel: "imessage",
accountId: "default",
peer: { kind: "group", id: "chat_id:123" },
},
acp: { label: "codex-group" },
},
],
}
```
See [ACP Agents](/tools/acp-agents) for shared ACP binding behavior.
## Deployment patterns
<AccordionGroup>
<Accordion title="Dedicated bot macOS user (separate iMessage identity)">
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 an SSH wrapper so OpenClaw can run `imsg` in that user context.
5. Point `channels.imessage.accounts.<id>.cliPath` and `.dbPath` to that user profile.
First run may require GUI approvals (Automation + Full Disk Access) in that bot user session.
</Accordion>
<Accordion title="Remote Mac over Tailscale (example)">
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
{
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
#!/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.
</Accordion>
<Accordion title="Multi-account pattern">
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.
</Accordion>
<Accordion title="Direct-message history">
Set `channels.imessage.dmHistoryLimit` to seed new direct-message sessions with recent decoded `imsg` history for that conversation. Use `channels.imessage.dms["<sender>"].historyLimit` for per-sender overrides, including `0` to disable history for a sender.
iMessage DM history is fetched on demand from `imsg`. Leaving `dmHistoryLimit` unset disables global DM history seeding, but a positive per-sender `channels.imessage.dms["<sender>"].historyLimit` still enables seeding for that sender.
</Accordion>
</AccordionGroup>
## Media, chunking, and delivery targets
<AccordionGroup>
<Accordion title="Attachments and media">
- inbound attachment ingestion is **off by default** — set `channels.imessage.includeAttachments: true` to forward photos, voice memos, video, and other attachments to the agent. With it disabled, attachment-only iMessages are dropped before reaching the agent and may produce no `Inbound message` log line at all.
- 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)
- configured roots extend the default root pattern `/Users/*/Library/Messages/Attachments` (merged, not replaced)
- SCP uses strict host-key checking (`StrictHostKeyChecking=yes`)
- outbound media size uses `channels.imessage.mediaMaxMb` (default 16 MB)
</Accordion>
<Accordion title="Outbound text and chunking">
- text chunk limit: `channels.imessage.textChunkLimit` (default 4000)
- chunk mode: `channels.imessage.chunkMode`
- `length` (default)
- `newline` (paragraph-first splitting)
- outbound markdown bold/italic/underline/strikethrough is converted to native styled text (macOS 15+ recipients render the styling; older recipients see plain text without the markers); markdown tables are converted per the channel markdown table mode
- `channels.imessage.sendTransport` (`auto` default, `bridge`, `applescript`) selects how `imsg` delivers sends
</Accordion>
<Accordion title="Addressing formats">
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
imsg chats --limit 20
```
</Accordion>
</AccordionGroup>
## Private API actions
When `imsg launch` is running and `openclaw channels status --probe` reports `privateApi.available: true`, the message tool can use iMessage-native actions in addition to normal text sends.
All actions are enabled by default; use `channels.imessage.actions` to turn individual actions off:
```json5
{
channels: {
imessage: {
actions: {
reactions: true,
edit: true,
unsend: true,
reply: true,
sendWithEffect: true,
sendAttachment: true,
renameGroup: true,
setGroupIcon: true,
addParticipant: true,
removeParticipant: true,
leaveGroup: true,
polls: true,
},
},
},
}
```
<AccordionGroup>
<Accordion title="Available actions">
- **react**: Add/remove iMessage tapbacks (`messageId`, `emoji`, `remove`). Supported tapbacks map to love, like, dislike, laugh, emphasize, and question. Removing without an emoji clears whichever tapback was set.
- **reply**: Send a threaded reply to an existing message (`messageId`, `text` or `message`, plus `chatGuid`, `chatId`, `chatIdentifier`, or `to`). Reply-with-attachment additionally needs an `imsg` build whose `send-rich` supports `--file`.
- **sendWithEffect**: Send text with an iMessage effect (`text` or `message`, `effect` or `effectId`). Short names: slam, loud, gentle, invisibleink, confetti, lasers, fireworks, balloon, heart, echo, happybirthday, shootingstar, sparkles, spotlight.
- **edit**: Edit a sent message on supported macOS/private API versions (`messageId`, `text` or `newText`). Only messages the gateway itself sent can be edited.
- **unsend**: Retract a sent message on supported macOS/private API versions (`messageId`). Only messages the gateway itself sent can be unsent.
- **upload-file**: Send media/files (`buffer` as base64 or a hydrated `media`/`path`/`filePath`, `filename`, optional `asVoice`). Legacy alias: `sendAttachment`.
- **renameGroup**, **setGroupIcon**, **addParticipant**, **removeParticipant**, **leaveGroup**: Manage group chats when the current target is a group conversation. These mutate the host's Messages identity, so they require an owner sender or an `operator.admin` Gateway client.
- **poll**: Create a native Apple Messages poll (`pollQuestion`, `pollOption` repeated 2 to 12 times, plus `chatGuid`, `chatId`, `chatIdentifier`, or `to`). Recipients on iOS/iPadOS/macOS 26+ see and vote on it natively; older OS versions get a "Sent a poll" text fallback. Requires `selectors.pollPayloadMessage`.
- **poll-vote**: Vote on an existing poll (`pollId` or `messageId`, plus exactly one of `pollOptionIndex`, `pollOptionId`, or `pollOptionText`). Requires `selectors.pollVoteMessage` and the `poll.vote` RPC method.
Accepted inbound polls are rendered for the agent with the question, numbered option labels, vote counts, and the poll message ID needed by `poll-vote`.
</Accordion>
<Accordion title="Message IDs">
Inbound iMessage context includes both short `MessageSid` values and full message GUIDs (`MessageSidFull`) when available. Short IDs are scoped to the recent SQLite-backed reply cache and are checked against the current chat before use. If a short ID has expired or belongs to another chat, retry with the full `MessageSidFull`.
</Accordion>
<Accordion title="Capability detection">
OpenClaw hides private API actions only when the cached probe status says the bridge is unavailable. If the status is unknown, actions remain visible and dispatch probes lazily so the first action can succeed after `imsg launch` without a separate manual status refresh.
</Accordion>
<Accordion title="Read receipts and typing">
When the private API bridge is up, accepted inbound chats are marked read and direct chats show a typing bubble as soon as the turn is accepted, while the agent prepares context and generates. Disable read-marking with:
```json5
{
channels: {
imessage: {
sendReadReceipts: false,
},
},
}
```
Older `imsg` builds that pre-date the per-method capability list gate off typing/read silently; OpenClaw logs a one-time warning per restart so the missing receipt is attributable.
</Accordion>
<Accordion title="Inbound tapbacks">
OpenClaw subscribes to iMessage tapbacks and routes accepted reactions as system events instead of normal message text, so a user tapback does not trigger an ordinary reply loop.
Notification mode is controlled by `channels.imessage.reactionNotifications`:
- `"own"` (default): notify only when users react to bot-authored messages.
- `"all"`: notify for all inbound tapbacks from authorized senders.
- `"off"`: ignore inbound tapbacks.
Per-account overrides use `channels.imessage.accounts.<id>.reactionNotifications`.
</Accordion>
<Accordion title="Approval reactions (👍 / 👎)">
When `approvals.exec.enabled` or `approvals.plugin.enabled` is true and the request routes to iMessage, the gateway delivers an approval prompt natively and accepts a tapback to resolve it:
- `👍` (Like tapback) → `allow-once`
- `👎` (Dislike tapback) → `deny`
- `allow-always` remains a manual fallback: send `/approve <id> allow-always` as a regular reply.
Reaction handling requires the reacting user's handle to be an explicit approver. The approver list is read from `channels.imessage.allowFrom` (or `channels.imessage.accounts.<id>.allowFrom`); add the user's phone number in E.164 form or their Apple ID email (chat targets such as `chat_id:*` are not valid approver entries). The wildcard entry `"*"` is honored but allows any sender to approve; an empty approver list disables the reaction shortcut entirely. The reaction shortcut intentionally bypasses `reactionNotifications`, `dmPolicy`, and `groupAllowFrom` because the explicit-approver allowlist is the only gate that matters for approval resolution.
`/approve` text command authorization follows the same list: when `channels.imessage.allowFrom` is non-empty, `/approve <id> <decision>` is authorized against that approver list (not the broader DM allowlist), and senders permitted on the DM allowlist but not in `allowFrom` receive an explicit denial. When `allowFrom` is empty, the same-chat fallback stays in effect and `/approve` authorizes anyone the DM allowlist permits. Add every operator who should approve — via `/approve` or via reactions — to `allowFrom`.
Operator notes:
- The reaction binding is stored both in memory and in the gateway's persistent keyed store (TTL matched to the approval expiry), and the gateway also polls pending prompts for tapbacks, so a tapback that lands shortly after a gateway restart still resolves the approval.
- The operator's own `is_from_me=true` tapback (for example from a paired Apple device) resolves the approval when that handle is an explicit approver.
- Approval prompts route into a group conversation only when explicit approvers are configured; otherwise any group member could approve.
- Legacy text-style tapbacks (`Liked "…"` plain text from very old Apple clients) cannot resolve approvals because they carry no message GUID; reaction resolution requires the structured tapback metadata that current macOS / iOS clients emit.
</Accordion>
</AccordionGroup>
## Config writes
iMessage allows channel-initiated config writes by default (for `/config set|unset` when `commands.config: true`).
Disable:
```json5
{
channels: {
imessage: {
configWrites: false,
},
},
}
```
<a id="coalescing-split-send-dms-command--url-in-one-composition"></a>
## Coalescing split-send DMs (command + URL in one composition)
When a user types a command and a URL together — e.g. `Dump https://example.com/article` — Apple's Messages app splits the send into **two separate `chat.db` rows**:
1. A text message (`"Dump"`).
2. A URL-preview balloon (`"https://..."`) with OG-preview images as attachments.
The two rows arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalescing, the agent gets the command alone on turn 1 (and often replies "send me the URL") before the URL arrives on turn 2. This is Apple's send pipeline, not anything OpenClaw or `imsg` introduces.
`channels.imessage.coalesceSameSenderDms` opts a DM into buffering consecutive same-sender rows. When `imsg` exposes the structural URL-preview marker `balloon_bundle_id: "com.apple.messages.URLBalloonProvider"` on one of the source rows, OpenClaw merges only that real split-send and keeps any other buffered rows as separate turns. On older `imsg` builds that emit no balloon metadata at all, OpenClaw cannot tell a split-send from separate sends, so it falls back to merging the bucket. That preserves the pre-metadata behavior rather than regressing `Dump <url>` split-sends into two turns. Group chats continue to dispatch per-message so multi-user turn structure is preserved.
<Tabs>
<Tab title="When to enable">
Enable when:
- You ship skills that expect `command + payload` in one message (dump, paste, save, queue, etc.).
- Your users paste URLs alongside commands.
- You can accept the added DM turn latency (see below).
Leave disabled when:
- You need minimum command latency for single-word DM triggers.
- All your flows are one-shot commands without payload follow-ups.
</Tab>
<Tab title="Enabling">
```json5
{
channels: {
imessage: {
coalesceSameSenderDms: true, // opt in (default: false)
},
},
}
```
With the flag on and no explicit `messages.inbound.byChannel.imessage` or global `messages.inbound.debounceMs`, the debounce window widens to **7000 ms** (the legacy default is 0 ms — no debouncing). The wider window is required because Apple's URL-preview split-send cadence can stretch to several seconds while Messages.app emits the preview row.
To tune the window yourself:
```json5
{
messages: {
inbound: {
byChannel: {
// 7000 ms covers observed Messages.app URL-preview delays.
imessage: 7000,
},
},
},
}
```
</Tab>
<Tab title="Trade-offs">
- **Precise merging needs current `imsg` payload metadata.** With `balloon_bundle_id` present, only the real split-send merges; the metadata-less fallback merge described above is interim back-compat, removed once `imsg` coalesces split-sends upstream.
- **Added latency for DM messages.** With the flag on, every DM (including standalone control commands and single-text follow-ups) waits up to the debounce window before dispatching, in case a URL-preview row is coming. Group-chat messages keep instant dispatch.
- **Merged output is bounded.** Merged text caps at 4000 chars with an explicit `…[truncated]` marker; attachments cap at 20; source entries cap at 10 (first-plus-latest retained beyond that). Every source GUID is tracked in `coalescedMessageGuids` for downstream telemetry.
- **DM-only.** Group chats fall through to per-message dispatch so the bot stays responsive when multiple people are typing.
- **Opt-in, per-channel.** Other channels (Discord, Slack, Telegram, WhatsApp, …) are unaffected. Legacy BlueBubbles configs that set `channels.bluebubbles.coalesceSameSenderDms` should migrate that value to `channels.imessage.coalesceSameSenderDms`.
</Tab>
</Tabs>
### Scenarios and what the agent sees
The "Flag on" column shows behavior on an `imsg` build that emits `balloon_bundle_id`. On older `imsg` builds that emit no balloon metadata at all, the rows below marked "Two turns" / "N turns" instead fall back to a legacy merge (one turn): OpenClaw cannot structurally tell a split-send from separate sends, so it preserves the pre-metadata merge. Precise separation activates once the build emits balloon metadata.
| User composes | `chat.db` produces | Flag off (default) | Flag on + window (imsg emits balloon metadata) |
| ------------------------------------------------------------------ | ----------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `Dump https://example.com` (one send) | 2 rows ~1 s apart | Two agent turns: "Dump" alone, then URL | One turn: merged text `Dump https://example.com` |
| `Save this 📎image.jpg caption` (attachment + text) | 2 rows without URL balloon metadata | Two turns | Two turns after metadata is observed; one merged turn on old/pre-latch metadata-less sessions |
| `/status` (standalone command) | 1 row | Instant dispatch | **Wait up to window, then dispatch** |
| URL pasted alone | 1 row | Instant dispatch | Wait up to window, then dispatch |
| Text + URL sent as two deliberate separate messages, minutes apart | 2 rows outside window | Two turns | Two turns (window expires between them) |
| Rapid flood (>10 small DMs inside window) | N rows without URL balloon metadata | N turns | N turns after metadata is observed; one bounded merged turn on old/pre-latch metadata-less sessions |
| Two people typing in a group chat | N rows from M senders | M+ turns (one per sender bucket) | M+ turns — group chats are not coalesced |
## Inbound recovery after a bridge or gateway restart
iMessage recovers messages missed while the gateway was down, and at the same time suppresses the stale "backlog bomb" Apple can flush after a Push recovery. The default behavior is always on, built on the inbound dedupe.
- **Replay dedupe.** Every dispatched inbound message is recorded by its Apple GUID in persistent plugin state (`imessage.inbound-dedupe`), claimed at ingestion and committed after handling (released on a transient failure so it can retry). Anything already handled is dropped instead of dispatched twice. This is what lets recovery replay aggressively without per-message bookkeeping.
- **Downtime recovery.** On startup the monitor remembers the last dispatched `chat.db` rowid (a persisted per-account cursor) and passes it to `imsg watch.subscribe` as `since_rowid`, so imsg replays the rows that landed while the gateway was down, then tails live. Replay is bounded to the most recent 500 rows and to messages up to ~2 hours old, and the dedupe drops anything already handled.
- **Stale-backlog age fence.** Rows above the startup boundary are genuinely live; one whose send date is more than ~15 minutes older than its arrival is the Push-flush backlog and is suppressed. Replayed rows (at or below the boundary) use the wider recovery window instead, so a recently-missed message is delivered while ancient history is not.
Recovery works over both local and remote `cliPath` setups, because `since_rowid` replay runs over the same `imsg` RPC connection. The difference is the window: when the gateway can read `chat.db` (local), it anchors the startup rowid boundary, caps the replay span, and delivers missed messages up to a couple of hours old. Over a remote SSH `cliPath` it cannot read the database, so the replay is uncapped and every row uses the live age fence — it still recovers recently-missed messages and still suppresses old backlog, just with the narrower live window. Run the gateway on the Messages Mac for the wider recovery window.
### Operator-visible signal
Suppressed backlog is logged at the default level, never silently dropped (the `recovery` flag shows which window applied):
```text
imessage: suppressed stale inbound backlog account=<id> sent=<iso> recovery=<bool> (<N> suppressed since start)
```
### Migration
`channels.imessage.catchup.*` is deprecated — downtime recovery is automatic and needs no config for new setups. Existing configs with `catchup.enabled: true` remain honored as a compatibility profile for the recovery replay window. Disabled catchup blocks (`enabled: false` or no `enabled: true`) are retired; `openclaw doctor --fix` removes those.
## Troubleshooting
<AccordionGroup>
<Accordion title="imsg not found or RPC unsupported">
Validate the binary and RPC support:
```bash
imsg rpc --help
imsg status --json
openclaw channels status --probe
```
If the probe reports RPC unsupported, update `imsg`. If private API actions are unavailable, run `imsg launch` in the logged-in macOS user session and probe again. If the Gateway is not running on macOS, use the Remote Mac over SSH setup above instead of the default local `imsg` path.
</Accordion>
<Accordion title="Messages send but inbound iMessages do not arrive">
First prove whether the message reached the local Mac. If `chat.db` does not change, OpenClaw cannot receive the message even when `imsg status --json` reports a healthy bridge.
```bash
imsg chats --limit 10 --json
imsg watch --chat-id <chat-id> --json
sqlite3 ~/Library/Messages/chat.db \
"select datetime(max(date)/1000000000 + 978307200, 'unixepoch', 'localtime'), max(ROWID) from message;"
```
If phone-sent messages create no new rows, repair the macOS Messages and Apple Push layer before changing OpenClaw config. A one-shot service refresh is often enough:
```bash
launchctl kickstart -k system/com.apple.apsd
launchctl kickstart -k gui/$(id -u)/com.apple.CommCenter
launchctl kickstart -k gui/$(id -u)/com.apple.identityservicesd
launchctl kickstart -k gui/$(id -u)/com.apple.imagent
imsg launch
openclaw gateway restart
```
Send a fresh iMessage from the phone and confirm a new `chat.db` row or `imsg watch` event before debugging OpenClaw sessions. Do not run this as a periodic bridge-relaunch loop; repeated `imsg launch` plus gateway restarts during active work can interrupt deliveries and strand in-flight channel runs.
</Accordion>
<Accordion title="Gateway is not running on macOS">
The default `cliPath: "imsg"` must run on the Mac signed into Messages. On Linux or Windows, set `channels.imessage.cliPath` to a wrapper script that SSHes to that Mac and runs `imsg "$@"`.
```bash
#!/usr/bin/env bash
exec ssh -T messages-mac imsg "$@"
```
Then run:
```bash
openclaw channels status --probe --channel imessage
```
</Accordion>
<Accordion title="DMs are ignored">
Check:
- `channels.imessage.dmPolicy`
- `channels.imessage.allowFrom`
- pairing approvals (`openclaw pairing list imessage`)
</Accordion>
<Accordion title="Group messages are ignored">
Check:
- `channels.imessage.groupPolicy`
- `channels.imessage.groupAllowFrom`
- `channels.imessage.groups` allowlist behavior
- mention pattern configuration (`agents.list[].groupChat.mentionPatterns`)
</Accordion>
<Accordion title="Remote attachments fail">
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
</Accordion>
<Accordion title="macOS permission prompts were missed">
Re-run in an interactive GUI terminal in the same user/session context and approve prompts:
```bash
imsg chats --limit 1
imsg send <handle> "test"
```
Confirm Full Disk Access + Automation are granted for the process context that runs OpenClaw/`imsg`.
</Accordion>
</AccordionGroup>
## Configuration reference pointers
- [Configuration reference - iMessage](/gateway/config-channels#imessage)
- [Gateway configuration](/gateway/configuration)
- [Pairing](/channels/pairing)
## Related
- [Channels Overview](/channels) — all supported channels
- [BlueBubbles removal and the imsg iMessage path](/announcements/bluebubbles-imessage) — announcement and migration summary
- [Coming from BlueBubbles](/channels/imessage-from-bluebubbles) — config translation table and step-by-step cutover
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) — session routing for messages
- [Security](/gateway/security) — access model and hardening

72
docs/channels/index.md Normal file
View File

@@ -0,0 +1,72 @@
---
summary: "Messaging platforms OpenClaw can connect to"
read_when:
- You want to choose a chat channel for OpenClaw
- You need a quick overview of supported messaging platforms
title: "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.
iMessage, Telegram, and the WebChat UI ship with the core install. Channels marked
"official plugin" install with one command (`openclaw plugins install @openclaw/<id>`)
or on demand during `openclaw onboard` / `openclaw channels add`, then need a Gateway
restart. "External plugin" channels are maintained outside the OpenClaw repo.
## Supported channels
- [Discord](/channels/discord) - Discord Bot API + Gateway; supports servers, channels, and DMs (official plugin).
- [Feishu](/channels/feishu) - Feishu/Lark bot via WebSocket (official plugin).
- [Google Chat](/channels/googlechat) - Google Chat API app via HTTP webhook (official plugin).
- [iMessage](/channels/imessage) - Included in core. Native macOS integration via the `imsg` bridge on a signed-in Mac (or SSH wrapper when the Gateway runs elsewhere), including private API actions for replies, tapbacks, effects, attachments, and group management.
- [IRC](/channels/irc) - Classic IRC servers; channels + DMs with pairing/allowlist controls (official plugin).
- [LINE](/channels/line) - LINE Messaging API bot (official plugin).
- [Matrix](/channels/matrix) - Matrix protocol (official plugin).
- [Mattermost](/channels/mattermost) - Bot API + WebSocket; channels, groups, DMs (official plugin).
- [Microsoft Teams](/channels/msteams) - Bot Framework; enterprise support (official plugin).
- [Nextcloud Talk](/channels/nextcloud-talk) - Self-hosted chat via Nextcloud Talk (official plugin).
- [Nostr](/channels/nostr) - Decentralized DMs via NIP-04 (official plugin).
- [QQ Bot](/channels/qqbot) - QQ Bot API; private chat, group chat, and rich media (official plugin).
- [Raft](/channels/raft) - Raft CLI wake bridge for human and agent collaboration (official plugin).
- [Signal](/channels/signal) - signal-cli; privacy-focused (official plugin).
- [Slack](/channels/slack) - Bolt SDK; workspace apps (official plugin).
- [SMS](/channels/sms) - Twilio-backed SMS through the Gateway webhook (official plugin).
- [Synology Chat](/channels/synology-chat) - Synology NAS Chat via outgoing+incoming webhooks (official plugin).
- [Telegram](/channels/telegram) - Included in core. Bot API via grammY; supports groups.
- [Tlon](/channels/tlon) - Urbit-based messenger (official plugin).
- [Twitch](/channels/twitch) - Twitch chat via IRC connection (official plugin).
- [Voice Call](/plugins/voice-call) - Telephony via Plivo, Telnyx, or Twilio (official plugin).
- [WebChat](/web/webchat) - Included in core. Gateway WebChat UI over WebSocket.
- [WeChat](/channels/wechat) - Tencent iLink bot via QR login; private chats only (external plugin).
- [WhatsApp](/channels/whatsapp) - Most popular; uses Baileys and requires QR pairing (official plugin).
- [Yuanbao](/channels/yuanbao) - Tencent Yuanbao bot (external plugin).
- [Zalo](/channels/zalo) - Zalo Bot API; Vietnam's popular messenger (official plugin).
- [Zalo ClawBot](/channels/zaloclawbot) - Personal Zalo assistant via QR login; owner-bound (external plugin).
- [Zalo Personal](/channels/zalouser) - Zalo personal account via QR login (official plugin).
## Delivery notes
- Telegram replies that contain markdown image syntax, such as `![alt](url)`,
are converted into media replies on the final outbound path when possible.
- Slack multi-person DMs route as group chats, so group policy, mention
behavior, and group-session rules apply to MPIM conversations.
- WhatsApp setup is install-on-demand: onboarding can show the setup flow before
the plugin package is installed, and the Gateway loads the external
ClawHub/npm plugin only when the channel is actually active.
- Channels that accept bot-authored inbound messages can use shared
[bot loop protection](/channels/bot-loop-protection) to prevent bot pairs from
replying to each other indefinitely.
- Supported always-on rooms can use [ambient room events](/channels/ambient-room-events)
so unmentioned room chatter becomes quiet context unless the agent sends with
the `message` tool.
## Notes
- Channels can run simultaneously; configure multiple and OpenClaw will route per chat.
- Fastest setup is usually **Telegram** (simple bot token, no plugin install). 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).

273
docs/channels/irc.md Normal file
View File

@@ -0,0 +1,273 @@
---
summary: "IRC plugin setup, access controls, and troubleshooting"
title: IRC
read_when:
- You want to connect OpenClaw to IRC channels or DMs
- You are configuring IRC allowlists, group policy, or mention gating
---
Use IRC when you want OpenClaw in classic channels (`#room`) and direct messages.
Install the official IRC plugin, then configure it under `channels.irc`.
## Quick start
1. Install the plugin:
```bash
openclaw plugins install @openclaw/irc
```
2. Set at least host, nick, and the channels to join in `~/.openclaw/openclaw.json`:
```json5
{
channels: {
irc: {
enabled: true,
host: "irc.example.com",
port: 6697,
tls: true,
nick: "openclaw-bot",
channels: ["#openclaw"],
},
},
}
```
3. Start/restart the Gateway:
```bash
openclaw gateway run
```
Prefer a private IRC server for bot coordination. If you intentionally use a public IRC network, common choices include Libera.Chat, OFTC, and Snoonet. Avoid predictable public channels for bot or swarm backchannel traffic.
## Connection settings
| Key | Default | Notes |
| ----------------------------- | ----------------------------- | ----------------------------------------------------------- |
| `host` | none (required) | IRC server hostname |
| `port` | `6697` with TLS, `6667` plain | 1-65535 |
| `tls` | `true` | Set `false` only for intentional plaintext |
| `nick` | none (required) | Bot nick |
| `username` | nick, else `openclaw` | IRC username |
| `realname` | `OpenClaw` | Realname/GECOS field |
| `password` / `passwordFile` | none | Server password; file must be a regular file |
| `channels` | none | Channels to join (`["#openclaw"]`) |
| `accounts` / `defaultAccount` | none | Multi-account setup; env vars fill only the default account |
## Security defaults
- IRC uses raw TCP/TLS sockets outside OpenClaw operator-managed forward proxy routing. In deployments that require all egress through that forward proxy, set `channels.irc.enabled=false` unless direct IRC egress is explicitly approved.
- `channels.irc.dmPolicy` defaults to `"pairing"`: unknown DM senders get a pairing code you approve with `openclaw pairing approve irc <code>`.
- `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"]` with `requireMention`, `allowFrom`, `enabled`, `tools`, `toolsBySender`, `skills`, and `systemPrompt`
- `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 `#openclaw` to talk to the bot):
```json5
{
channels: {
irc: {
groupPolicy: "allowlist",
groups: {
"#openclaw": { 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. The bot counts as mentioned when the message contains the connected bot nick or matches your configured mention patterns.
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
{
channels: {
irc: {
groupPolicy: "allowlist",
groups: {
"#openclaw": {
requireMention: false,
allowFrom: ["*"],
},
},
},
},
}
```
Or to allow **all** IRC channels (no per-channel allowlist) and still reply without mentions:
```json5
{
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
{
channels: {
irc: {
groups: {
"#openclaw": {
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
{
channels: {
irc: {
groups: {
"#openclaw": {
allowFrom: ["*"],
toolsBySender: {
"*": {
deny: ["group:runtime", "group:fs", "gateway", "nodes", "cron", "browser"],
},
"id:alice": {
deny: ["gateway", "nodes", "cron"],
},
},
},
},
},
},
}
```
Notes:
- `toolsBySender` keys should use explicit prefixes (`channel:`, `id:`, `e164:`, `username:`, `name:`). For IRC use `id:` with the sender identity value: `id:alice` or `id:alice!~alice@203.0.113.7` for stronger matching.
- Legacy unprefixed keys are still accepted, matched as `id:` only, and emit a deprecation warning.
- 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:
```json5
{
channels: {
irc: {
nickserv: {
enabled: true,
service: "NickServ",
password: "your-nickserv-password",
},
},
},
}
```
NickServ identify runs by default whenever a password is set (`enabled` only needs to be `false` to opt out). `service` defaults to `NickServ`; `passwordFile` is an alternative to inline `password`.
Optional one-time registration on connect (`register: true` requires `registerEmail`):
```json5
{
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`
`IRC_HOST` cannot be set from a workspace `.env`; see [Workspace `.env` files](/gateway/security).
## 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.
## Related
- [Channels Overview](/channels) — all supported channels
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) — session routing for messages
- [Security](/gateway/security) — access model and hardening

248
docs/channels/line.md Normal file
View File

@@ -0,0 +1,248 @@
---
summary: "LINE Messaging API plugin setup, config, and usage"
read_when:
- You want to connect OpenClaw to LINE
- You need LINE webhook + credential setup
- You want LINE-specific message options
title: LINE
---
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: official plugin, installed separately. Direct messages, group chats, media,
locations, Flex messages, template messages, and quick replies are supported.
Reactions and threads are not supported.
## Install
Install LINE before configuring the channel:
```bash
openclaw plugins install @openclaw/line
```
Local checkout (when running from a git repo):
```bash
openclaw plugins install ./path/to/local/line-plugin
```
## 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):
```text
https://gateway-host/line/webhook
```
The Gateway answers LINE's webhook verification (GET) and acknowledges signed
inbound events (POST) immediately after signature and payload validation; agent
processing continues asynchronously.
If you need a custom path, set `channels.line.webhookPath` or
`channels.line.accounts.<id>.webhookPath` and update the URL accordingly.
Security notes:
- LINE signature verification is body-dependent (HMAC over the raw body), so OpenClaw applies a strict pre-auth body limit (64 KB) and read timeout before verification.
- OpenClaw processes webhook events from the verified raw request bytes. Upstream middleware-transformed `req.body` values are ignored for signature-integrity safety.
## Configure
Minimal config:
```json5
{
channels: {
line: {
enabled: true,
channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN",
channelSecret: "LINE_CHANNEL_SECRET",
dmPolicy: "pairing",
},
},
}
```
Public DM config:
```json5
{
channels: {
line: {
enabled: true,
channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN",
channelSecret: "LINE_CHANNEL_SECRET",
dmPolicy: "open",
allowFrom: ["*"],
},
},
}
```
Env vars (default account only):
- `LINE_CHANNEL_ACCESS_TOKEN`
- `LINE_CHANNEL_SECRET`
Token/secret files:
```json5
{
channels: {
line: {
tokenFile: "/path/to/line-token.txt",
secretFile: "/path/to/line-secret.txt",
},
},
}
```
`tokenFile` and `secretFile` must point to regular files. Symlinks are rejected.
Inline config values win over files; env vars are the last fallback for the default account.
Multiple accounts:
```json5
{
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
openclaw pairing list line
openclaw pairing approve line <CODE>
```
Allowlists and policies:
- `channels.line.dmPolicy`: `pairing | allowlist | open | disabled` (default `pairing`)
- `channels.line.allowFrom`: allowlisted LINE user IDs for DMs; `dmPolicy: "open"` requires `["*"]`
- `channels.line.groupPolicy`: `allowlist | open | disabled` (default `allowlist`)
- `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups
- Per-group overrides: `channels.line.groups.<groupId>.allowFrom` (plus `enabled`, `requireMention`, `systemPrompt`, `skills`)
- Static sender access groups can be referenced from `allowFrom`, `groupAllowFrom`, and per-group `allowFrom` with `accessGroup:<name>`; see [Access groups](/channels/access-groups).
- 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).
- Inbound media is saved under `~/.openclaw/media/inbound/` before it is passed
to the agent, matching the shared media store used by other channel plugins.
## Channel data (rich messages)
Use `channelData.line` to send quick replies, locations, Flex cards, or template
messages.
```json5
{
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:
```text
/card info "Welcome" "Thanks for joining!"
```
## ACP support
LINE supports ACP (Agent Communication Protocol) conversation bindings:
- `/acp spawn <agent> --bind here` binds the current LINE chat to an ACP session without creating a child thread.
- Configured ACP bindings and active conversation-bound ACP sessions work on LINE like other conversation channels.
See [ACP agents](/tools/acp-agents) for details.
## Outbound media
The LINE plugin sends images, videos, and audio through the agent message tool:
- **Images**: sent as LINE image messages; the preview image defaults to the media URL.
- **Videos**: require a preview image; set `channelData.line.previewImageUrl` to an image URL.
- **Audio**: sent as LINE audio messages; duration defaults to 60 seconds unless `channelData.line.durationMs` is set.
The media kind is taken from `channelData.line.mediaKind` when set, otherwise inferred
from the other LINE options or the URL file suffix, with image as the fallback.
Outbound media URLs must be public HTTPS URLs of at most 2000 characters. OpenClaw
validates the target hostname before handing the URL to LINE and rejects loopback,
link-local, and private-network targets.
Generic media sends without LINE-specific options use the image route.
## 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.
## Related
- [Channels Overview](/channels) — all supported channels
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) — session routing for messages
- [Security](/gateway/security) — access model and hardening

77
docs/channels/location.md Normal file
View File

@@ -0,0 +1,77 @@
---
summary: "Inbound channel location parsing (Telegram, WhatsApp, Matrix, LINE) and context fields"
read_when:
- Adding or modifying channel location parsing
- Using location context fields in agent prompts or tools
title: "Channel location parsing"
---
OpenClaw normalizes shared locations from chat channels into:
- terse coordinate text appended to the inbound body, and
- structured fields in the auto-reply context payload. Channel-provided labels, addresses, and captions/comments are rendered into the prompt by the shared untrusted metadata JSON block, not inline in the user body.
Currently supported:
- **LINE** (location messages with title/address)
- **Matrix** (`m.location` with `geo_uri`)
- **Telegram** (location pins + venues + live locations)
- **WhatsApp** (`locationMessage` + `liveLocationMessage`)
## Text formatting
Locations are rendered as friendly lines without brackets. Coordinates use six decimal places; accuracy is rounded to whole meters:
- Pin:
- `📍 48.858844, 2.294351 ±12m`
- Named place (same line; the name/address go to the metadata block only):
- `📍 48.858844, 2.294351 ±12m`
- Live share:
- `🛰 Live location: 48.858844, 2.294351 ±12m`
If the channel includes a label, address, or caption/comment, it is preserved in the context payload and appears in the prompt as fenced untrusted JSON (fields are omitted when absent):
````text
Location (untrusted metadata):
```json
{
"latitude": 48.858844,
"longitude": 2.294351,
"accuracy_m": 12,
"source": "place",
"name": "Eiffel Tower",
"address": "Champ de Mars, Paris",
"caption": "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)
- `LocationCaption` (string; optional)
When the channel does not set an explicit source, OpenClaw infers it: live shares become `live`, locations with a name or address become `place`, everything else is `pin`.
The prompt renderer treats `LocationName`, `LocationAddress`, and `LocationCaption` as untrusted metadata and serializes them through the same bounded JSON path used for other channel context.
## Channel notes
- **LINE**: location message `title`/`address` map to `LocationName`/`LocationAddress`; no live locations.
- **Matrix**: `geo_uri` is parsed as a pin location; the `u` (uncertainty) parameter maps to `LocationAccuracy`, the event body populates `LocationCaption`, altitude is ignored, and `LocationIsLive` is always false.
- **Telegram**: venues map to `LocationName`/`LocationAddress`; live locations are detected via `live_period`.
- **WhatsApp**: `locationMessage.comment` and `liveLocationMessage.caption` populate `LocationCaption`.
## Related
- [Location command (nodes)](/nodes/location-command)
- [Camera capture](/nodes/camera)
- [Media understanding](/nodes/media-understanding)

View File

@@ -0,0 +1,331 @@
---
summary: "How OpenClaw upgrades the previous Matrix plugin in place, including encrypted-state recovery limits and manual recovery steps."
read_when:
- Upgrading an existing Matrix installation
- Migrating encrypted Matrix history and device state
title: "Matrix migration"
---
Upgrade from the previous public `matrix` plugin to the current implementation.
For most users, the upgrade is in place:
- the plugin stays `@openclaw/matrix`
- the channel stays `matrix`
- your config stays under `channels.matrix`
- cached credentials stay under `~/.openclaw/credentials/matrix/`
- runtime state stays under `~/.openclaw/matrix/`
You do not need to rename config keys or reinstall the plugin under a new name.
The root `openclaw` package no longer bundles Matrix runtime code or Matrix SDK
dependencies. If `openclaw channels status` shows Matrix is configured but the
plugin is not installed, run `openclaw doctor --fix` or
`openclaw plugins install @openclaw/matrix`; do not install Matrix SDK packages
into the root OpenClaw package.
## What the migration does automatically
Matrix migration runs when the gateway starts (through the loaded Matrix plugin), when you run [`openclaw doctor --fix`](/gateway/doctor), and as a fallback when the Matrix client starts and still finds old on-disk state. Before any actionable migration step mutates on-disk state, OpenClaw creates or reuses a focused recovery snapshot.
When you use `openclaw update`, the exact trigger depends on how OpenClaw is installed:
- source installs run a non-interactive `openclaw doctor --fix` pass during the update flow, then restart the gateway by default
- package-manager installs update the package, run `openclaw doctor --non-interactive --fix`, then rely on the default gateway restart so startup can finish Matrix migration
- if you use `openclaw update --no-restart`, startup-backed Matrix migration is deferred until you later run `openclaw doctor --fix` and restart the gateway
Automatic migration covers:
- creating or reusing a pre-migration snapshot under `~/Backups/openclaw-migrations/`
- reusing your cached Matrix credentials
- keeping the same account selection and `channels.matrix` config
- moving the old flat Matrix sync store and crypto store into the current account-scoped location when the target account can be resolved safely
- importing file-based sidecar state (`bot-storage.json` sync cache, `recovery-key.json`, `legacy-crypto-migration.json`, IndexedDB snapshots) into Matrix SQLite state; migrated files are archived with a `.migrated` suffix
- extracting a previously saved Matrix room-key backup decryption key from the old rust crypto store, when that key exists locally
- reusing the most complete existing token-hash storage root for the same Matrix account, homeserver, user, and device when the access token changes later
- scanning sibling token-hash storage roots for pending encrypted-state restore metadata when the Matrix access token changed but the account/device identity stayed the same
- restoring backed-up room keys into the new crypto store on the next Matrix startup
Snapshot details:
- OpenClaw writes a marker file at `~/.openclaw/matrix/migration-snapshot.json` after a successful snapshot so later startup and repair passes can reuse the same archive.
- These automatic Matrix migration snapshots back up config + state only (`includeWorkspace: false`).
- If Matrix only has warning-only migration state, for example because `userId` or `accessToken` is still missing, OpenClaw does not create the snapshot yet because no Matrix mutation is actionable.
- If the snapshot step fails, OpenClaw skips Matrix migration for that run instead of mutating state without a recovery point.
About multi-account upgrades:
- the flat Matrix store (`~/.openclaw/matrix/bot-storage.json` and `~/.openclaw/matrix/crypto/`) came from a single-store layout, so OpenClaw can only migrate it into one resolved Matrix account target
- already account-scoped legacy Matrix stores are detected and prepared per configured Matrix account
## What the migration cannot do automatically
The previous public Matrix plugin did **not** automatically create Matrix room-key backups. It persisted local crypto state and requested device verification, but it did not guarantee that your room keys were backed up to the homeserver.
That means some encrypted installs can only be migrated partially.
OpenClaw cannot automatically recover:
- local-only room keys that were never backed up
- encrypted state when the target Matrix account cannot be resolved yet because `homeserver`, `userId`, or `accessToken` are still unavailable
- encrypted state when the old crypto store has no recorded device ID for the account
- automatic migration of one shared flat Matrix store when multiple Matrix accounts are configured but `channels.matrix.defaultAccount` is not set
- custom plugin path installs that are pinned to a repo path instead of the standard Matrix package (surfaced by `openclaw doctor`)
- a missing recovery key when the old store had backed-up keys but did not keep the decryption key locally
If your old installation had local-only encrypted history that was never backed up, some older encrypted messages may remain unreadable after the upgrade.
## Recommended upgrade flow
1. Update OpenClaw and the Matrix plugin normally.
Prefer plain `openclaw update` without `--no-restart` so startup can finish the Matrix migration immediately.
2. Run:
```bash
openclaw doctor --fix
```
If Matrix has actionable migration work, doctor will create or reuse the pre-migration snapshot first and print the archive path.
3. Start or restart the gateway.
4. Check current verification and backup state:
```bash
openclaw matrix verify status
openclaw matrix verify backup status
```
5. Put the recovery key for the Matrix account you are repairing in an account-specific environment variable. For a single default account, `MATRIX_RECOVERY_KEY` is fine. For multiple accounts, use one variable per account, for example `MATRIX_RECOVERY_KEY_ASSISTANT`, and add `--account assistant` to the command.
6. If OpenClaw tells you a recovery key is needed, run the command for the matching account:
```bash
printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin
printf '%s\n' "$MATRIX_RECOVERY_KEY_ASSISTANT" | openclaw matrix verify backup restore --recovery-key-stdin --account assistant
```
7. If this device is still unverified, run the command for the matching account:
```bash
printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin
printf '%s\n' "$MATRIX_RECOVERY_KEY_ASSISTANT" | openclaw matrix verify device --recovery-key-stdin --account assistant
```
If the recovery key is accepted and backup is usable, but `Cross-signing verified`
is still `no`, complete self-verification from another Matrix client:
```bash
openclaw matrix verify self
```
Accept the request in another Matrix client, compare the emoji or decimals,
and type `yes` only when they match. The command waits for full Matrix
identity trust before reporting success.
8. If you are intentionally abandoning unrecoverable old history and want a fresh backup baseline for future messages, run:
```bash
openclaw matrix verify backup reset --yes
```
Add `--rotate-recovery-key` only when the old recovery key should stop unlocking the fresh backup.
9. If no server-side key backup exists yet, create one for future recoveries:
```bash
openclaw matrix verify bootstrap
```
## How encrypted migration works
Encrypted migration is a two-stage process:
1. Startup or `openclaw doctor --fix` creates or reuses the pre-migration snapshot if encrypted migration is actionable, then inspects the old Matrix rust crypto store through the crypto inspector bundled with the Matrix plugin.
2. If a backup decryption key is found, OpenClaw imports it into Matrix SQLite state and marks room-key restore as pending.
3. On the next Matrix startup, OpenClaw restores backed-up room keys into the new crypto store automatically. Pending restore state is also picked up from sibling token-hash storage roots when the access token rotated in between.
If the old store reports room keys that were never backed up, OpenClaw warns instead of pretending recovery succeeded.
## Common messages and what they mean
### Upgrade and detection messages
`Matrix plugin upgraded in place.` (doctor) or `matrix: plugin upgraded in place for account "..."` (startup)
- Meaning: the old on-disk Matrix state was detected and migrated into the current layout.
- What to do: nothing unless the same output also includes warnings.
`Matrix migration snapshot created before applying Matrix upgrades.` / `Matrix migration snapshot reused before applying Matrix upgrades.`
- Meaning: doctor created a recovery archive before mutating Matrix state, or found an existing snapshot marker and reused that archive instead of creating a duplicate backup. Startup logs the same as `matrix: created pre-migration backup snapshot: ...` / `matrix: reusing existing pre-migration backup snapshot: ...`.
- What to do: keep the printed archive path until you confirm migration succeeded.
`Legacy Matrix state detected at ... but channels.matrix is not configured yet.`
- Meaning: old Matrix state exists, but OpenClaw cannot map it to a current Matrix account because Matrix is not configured.
- What to do: configure `channels.matrix`, then rerun `openclaw doctor --fix` or restart the gateway.
`Legacy Matrix state detected at ... but the new account-scoped target could not be resolved yet (need homeserver, userId, and access token for channels.matrix...).`
- Meaning: OpenClaw found old state, but it still cannot determine the exact current account/device root.
- What to do: start the gateway once with a working Matrix login, or rerun `openclaw doctor --fix` after cached credentials exist.
`Legacy Matrix state detected at ... but multiple Matrix accounts are configured and channels.matrix.defaultAccount is not set.`
- Meaning: OpenClaw found one shared flat Matrix store, but it refuses to guess which named Matrix account should receive it.
- What to do: set `channels.matrix.defaultAccount` to the intended account, then rerun `openclaw doctor --fix` or restart the gateway.
The same three warnings also appear with the prefix `Legacy Matrix encrypted state detected at ...` when the blocked store is the old encrypted crypto store.
`Matrix legacy sync store not migrated because the target already exists (...)` / `Matrix legacy crypto store not migrated because the target already exists (...)`
- Meaning: the new account-scoped location already has a sync or crypto store, so OpenClaw did not overwrite it automatically.
- What to do: verify that the current account is the correct one before manually removing or moving the conflicting target.
`Failed migrating Matrix legacy sync store (...)` or `Failed migrating Matrix legacy crypto store (...)`
- Meaning: OpenClaw tried to move old Matrix state but the filesystem operation failed.
- What to do: inspect filesystem permissions and disk state, then rerun `openclaw doctor --fix`.
`Matrix migration warnings are present, but no on-disk Matrix mutation is actionable yet. No pre-migration snapshot was needed.`
- Meaning: OpenClaw detected old Matrix state, but the migration is still blocked on missing identity or credential data. Startup logs this as `matrix: migration remains in a warning-only state; no pre-migration snapshot was needed yet`.
- What to do: finish Matrix login or config setup, then rerun `openclaw doctor --fix` or restart the gateway.
`Legacy Matrix encrypted state was detected, but the Matrix crypto inspector is unavailable.`
- Meaning: OpenClaw found old encrypted Matrix state, but the Matrix plugin build is missing the crypto inspector module that inspects the old rust crypto store.
- What to do: reinstall or repair the Matrix plugin (`openclaw plugins install @openclaw/matrix`, or `openclaw plugins install ./path/to/local/matrix-plugin` for a repo checkout), then rerun `openclaw doctor --fix` or restart the gateway.
`- Failed creating a Matrix migration snapshot before repair: ...`
`- Skipping Matrix migration changes for now. Resolve the snapshot failure, then rerun "openclaw doctor --fix".`
- Meaning: OpenClaw refused to mutate Matrix state because it could not create the recovery snapshot first.
- What to do: resolve the backup error, then rerun `openclaw doctor --fix` or restart the gateway.
`Failed migrating legacy Matrix client storage: ...`
- Meaning: the Matrix client-side fallback found old storage, but the migration failed. OpenClaw rolls back completed moves and aborts that fallback instead of silently starting with a fresh store. This error also appears when the flat store targets a different account than the one currently starting.
- What to do: inspect filesystem permissions or conflicts, keep the old state intact, and retry after fixing the error.
`Matrix is installed from a custom path: ...`
- Meaning: Matrix is pinned to a path install, so mainline updates do not automatically replace it with the default Matrix package.
- What to do: reinstall with `openclaw plugins install @openclaw/matrix` when you want to return to the default Matrix plugin.
### Encrypted-state recovery messages
`matrix: restored X/Y room key(s) from legacy encrypted-state backup`
- Meaning: backed-up room keys were restored successfully into the new crypto store.
- What to do: usually nothing.
`matrix: N legacy local-only room key(s) were never backed up and could not be restored automatically`
- Meaning: some old room keys existed only in the old local store and had never been uploaded to Matrix backup. During preparation the same limit is reported as `Legacy Matrix encrypted state for account "..." contains N room key(s) that were never backed up.`
- What to do: expect some old encrypted history to remain unavailable unless you can recover those keys manually from another verified client.
`Legacy Matrix encrypted state detected at ... but no device ID was found for account "..."`
- Meaning: the old crypto store does not record which Matrix device it belonged to, so OpenClaw cannot inspect it safely.
- What to do: old encrypted history cannot be recovered automatically; OpenClaw continues without it.
`Legacy Matrix encrypted state for account "..." has backed-up room keys, but no local backup decryption key was found. Ask the operator to run "openclaw matrix verify backup restore --recovery-key <key>" after upgrade if they have the recovery key.`
- Meaning: backup exists, but OpenClaw could not recover the recovery key automatically.
- What to do: run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin` (preferred over passing the key as an argument).
`Failed inspecting legacy Matrix encrypted state for account "..." (...): ...`
- Meaning: OpenClaw found the old encrypted store, but it could not inspect it safely enough to prepare recovery.
- What to do: rerun `openclaw doctor --fix`. If it repeats, keep the old state directory intact and recover using another verified Matrix client plus `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin`.
`Legacy Matrix backup key was found for account "...", but Matrix SQLite state already contains a different recovery key. Leaving the existing state unchanged.`
- Meaning: OpenClaw detected a backup key conflict and refused to overwrite the current recovery-key state automatically.
- What to do: verify which recovery key is correct before retrying any restore command.
`Legacy Matrix encrypted state for account "..." cannot be fully converted automatically because the old rust crypto store does not expose all local room keys for export.`
- Meaning: this is the hard limit of the old storage format.
- What to do: backed-up keys can still be restored, but local-only encrypted history may remain unavailable.
`matrix: failed restoring room keys from legacy encrypted-state backup: ...`
- Meaning: the new plugin attempted restore but Matrix returned an error.
- What to do: run `openclaw matrix verify backup status`, then retry with `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin` if needed.
### Manual recovery messages
`openclaw matrix verify status` and `openclaw matrix verify backup status` print a `Backup issue:` line plus `Next steps:` guidance when the room-key backup is not healthy on this device:
| Backup issue | Meaning | Fix |
| --------------------------------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `no room-key backup exists on the homeserver` | nothing to restore from | `openclaw matrix verify bootstrap` to create a room key backup |
| `backup decryption key is not loaded on this device` | key exists but is not active here | `openclaw matrix verify backup restore`; if it still cannot load the key, pipe the recovery key via `--recovery-key-stdin` |
| `backup decryption key could not be loaded from secret storage (...)` | secret storage load failed or is unsupported | pipe the recovery key: `printf '%s\n' "$MATRIX_RECOVERY_KEY" \| openclaw matrix verify backup restore --recovery-key-stdin` |
| `backup key mismatch (...)` | stored key does not match the active server backup | rerun `verify backup restore --recovery-key-stdin` with the active server backup key, or `verify backup reset --yes` for a fresh baseline |
| `backup signature chain is not trusted by this device` | device does not trust the cross-signing chain yet | `verify device --recovery-key-stdin`, then `verify self` from another verified client if trust is still incomplete |
| `backup exists but is not active on this device` | server backup present, local session inactive | verify the device first, then recheck with `openclaw matrix verify backup status` |
| `backup trust state could not be fully determined` | diagnostics were inconclusive | `openclaw matrix verify status --verbose` |
Other recovery errors:
`Matrix recovery key is required`
- Meaning: you tried a recovery step without supplying a recovery key when one was required.
- What to do: rerun the command with `--recovery-key-stdin`, for example `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin`.
`Invalid Matrix recovery key: ...`
- Meaning: the provided key could not be parsed or did not match the expected format.
- What to do: retry with the exact recovery key from your Matrix client or recovery-key export.
`Matrix recovery key was applied, but this device still lacks full Matrix identity trust.`
- Meaning: the recovery key unlocked usable backup material, but Matrix has not established full cross-signing identity trust for this device. Check the command output for `Recovery key accepted`, `Backup usable`, `Cross-signing verified`, and `Device verified by owner`.
- What to do: run `openclaw matrix verify self`, accept the request in another Matrix client, compare the SAS, and type `yes` only when it matches. Use `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify bootstrap --recovery-key-stdin --force-reset-cross-signing` only when you intentionally want to replace the current cross-signing identity.
If you accept losing unrecoverable old encrypted history, you can instead reset the
current backup baseline with `openclaw matrix verify backup reset --yes`. When the
stored backup secret is broken, that reset also repairs secret storage so the
new backup key can load correctly after restart.
### Custom plugin install messages
`Matrix is installed from a custom path that no longer exists: ...`
- Meaning: your plugin install record points at a local path that is gone.
- What to do: reinstall with `openclaw plugins install @openclaw/matrix`, or if you are running from a repo checkout, `openclaw plugins install ./path/to/local/matrix-plugin`. `openclaw doctor --fix` can also remove the stale Matrix plugin references for you.
## If encrypted history still does not come back
Run these checks in order:
```bash
openclaw matrix verify status --verbose
openclaw matrix verify backup status --verbose
printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin --verbose
```
If the backup restores successfully but some old rooms are still missing history, those missing keys were probably never backed up by the previous plugin.
## If you want to start fresh for future messages
If you accept losing unrecoverable old encrypted history and only want a clean backup baseline going forward, run these commands in order:
```bash
openclaw matrix verify backup reset --yes
openclaw matrix verify backup status --verbose
openclaw matrix verify status
```
If the device is still unverified after that, finish verification from your Matrix client by comparing the SAS emoji or decimal codes and confirming that they match.
## Related
- [Matrix](/channels/matrix): channel setup and config.
- [Matrix push rules](/channels/matrix-push-rules): notification routing.
- [Doctor](/gateway/doctor): health check and automatic migration trigger.
- [Migration guide](/install/migrating): all migration paths (machine moves, cross-system imports).
- [Plugins](/tools/plugin): plugin install and registration.

View File

@@ -0,0 +1,84 @@
---
summary: "Matrix MessagePresentation metadata for OpenClaw-aware clients"
read_when:
- Building Matrix clients that render OpenClaw rich responses
- Debugging com.openclaw.presentation event content
title: "Matrix presentation metadata"
---
OpenClaw attaches normalized `MessagePresentation` metadata to outbound Matrix `m.room.message` events under the `com.openclaw.presentation` content key.
Stock Matrix clients keep rendering the plain text `body`. OpenClaw-aware clients can read the structured metadata and render native UI such as buttons, selects, context rows, and dividers.
## Event content
```json
{
"msgtype": "m.text",
"body": "Select model\n\nChoose model:\n- DeepSeek",
"com.openclaw.presentation": {
"version": 1,
"type": "message.presentation",
"title": "Select model",
"tone": "info",
"blocks": [
{
"type": "select",
"placeholder": "Choose model",
"options": [
{
"label": "DeepSeek",
"value": "/model deepseek/deepseek-chat"
}
]
}
]
}
}
```
- `version` is the metadata schema version; the current version is `1`. `type` is a stable discriminator, always `"message.presentation"`. The Matrix adapter only emits payloads with exactly this version and type; clients should likewise ignore unknown versions they cannot safely interpret, unknown `type` values, and unknown block types.
- `title` and `tone` (`info`, `success`, `warning`, `danger`, `neutral`) are optional hints.
- Buttons and select options can carry a typed `action` (`{ "type": "command", "command": "/..." }` or `{ "type": "callback", "value": "..." }`) alongside the legacy string `value`. Prefer `action` when both are present.
## Fallback behavior
OpenClaw always renders a readable plain text fallback into `body`. The structured metadata is additive and must not be required for basic Matrix interoperability.
Fallback rendering rules:
- `title`, `text`, and `context` content renders as plain lines.
- Buttons with a `command` action render as ``label: `/command` `` so the command stays copyable. Buttons with a `callback` action or only a legacy `value` render label-only so opaque callback values stay private; disabled buttons are always label-only. URL and web-app buttons render as `label: URL`.
- Select blocks render the placeholder (or `Options:`) as a heading plus label-only option lines.
- If nothing renders, for example a divider-only presentation, the body falls back to `---`.
Unsupported clients keep showing the fallback text. OpenClaw-aware clients may prefer the structured metadata for display while preserving the fallback for copy, search, notifications, and accessibility.
## Supported blocks
The Matrix outbound adapter advertises native support for:
- `buttons`
- `select`
- `context`
- `divider`
`text` blocks are always supported through the fallback body. Treat all blocks as best-effort presentation hints; ignore unknown fields and block types rather than failing the whole message.
## Interactions
This metadata does not add Matrix callback semantics. Button and select values are fallback interaction payloads, usually slash commands or text commands. A Matrix client that wants to support interaction resolves the control value (`action.command`, then `action.value`, then `value`) and sends it back to the room as a normal message.
For example, a button with value `/model deepseek/deepseek-chat` can be handled by sending that value as an encrypted Matrix text message in the same room.
## Relationship to approval metadata
`com.openclaw.presentation` is for general rich message presentation.
Approval prompts use the dedicated `com.openclaw.approval` metadata because approvals carry safety-sensitive state, decisions, and exec/plugin details. If both metadata keys are present on the same event, clients should prefer the dedicated approval renderer.
## Media messages
When a reply contains multiple media URLs, OpenClaw sends one Matrix event per media URL. Caption text and presentation metadata attach only to the first event so clients get one stable structured payload without duplicate renderers. The same rule applies when long text is chunked across events: the metadata rides on the first event only.
Keep presentation metadata compact. Large user-visible text should stay in `body` and use the normal Matrix text chunking path.

View File

@@ -0,0 +1,152 @@
---
summary: "Per-recipient Matrix push rules for quiet finalized preview edits"
read_when:
- Setting up Matrix quiet streaming for self-hosted Synapse or Tuwunel
- Users want notifications only on finished blocks, not on every preview edit
title: "Matrix push rules for quiet previews"
---
When `channels.matrix.streaming` is `"quiet"`, OpenClaw streams the reply by editing a single preview event in place. Previews are sent as non-notifying `m.notice` events, and the finalized edit is marked with `content["com.openclaw.finalized_preview"] = true`. Matrix clients notify on that final edit only if a per-user push rule matches the marker. This page is for operators who self-host Matrix and want to install that rule for each recipient account.
`streaming: "progress"` finalizes its drafts through the same path, so the same rule also fires for progress-mode finalized edits.
If you only want stock Matrix notification behavior, use `streaming: "partial"` or leave streaming off. See [Matrix channel setup](/channels/matrix#streaming-previews).
## Prerequisites
- recipient user = the person who should receive the notification
- bot user = the OpenClaw Matrix account that sends the reply
- use the recipient user's access token for the API calls below
- match `sender` in the push rule against the bot user's full MXID
- the recipient account must already have working pushers; quiet preview rules only work when normal Matrix push delivery is healthy
## Steps
<Steps>
<Step title="Configure quiet previews">
```json5
{
channels: {
matrix: {
streaming: "quiet",
},
},
}
```
</Step>
<Step title="Get the recipient's access token">
Reuse an existing client session token where possible. To mint a fresh one:
```bash
curl -sS -X POST \
"https://matrix.example.org/_matrix/client/v3/login" \
-H "Content-Type: application/json" \
--data '{
"type": "m.login.password",
"identifier": { "type": "m.id.user", "user": "@alice:example.org" },
"password": "REDACTED"
}'
```
</Step>
<Step title="Verify pushers exist">
```bash
curl -sS \
-H "Authorization: Bearer $USER_ACCESS_TOKEN" \
"https://matrix.example.org/_matrix/client/v3/pushers"
```
If no pushers come back, fix normal Matrix push delivery for this account before continuing.
</Step>
<Step title="Install the override push rule">
Install a rule that matches the finalized-preview marker plus the bot MXID as sender:
```bash
curl -sS -X PUT \
"https://matrix.example.org/_matrix/client/v3/pushrules/global/override/openclaw-finalized-preview-botname" \
-H "Authorization: Bearer $USER_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
--data '{
"conditions": [
{ "kind": "event_match", "key": "type", "pattern": "m.room.message" },
{
"kind": "event_property_is",
"key": "content.m\\.relates_to.rel_type",
"value": "m.replace"
},
{
"kind": "event_property_is",
"key": "content.com\\.openclaw\\.finalized_preview",
"value": true
},
{ "kind": "event_match", "key": "sender", "pattern": "@bot:example.org" }
],
"actions": [
"notify",
{ "set_tweak": "sound", "value": "default" },
{ "set_tweak": "highlight", "value": false }
]
}'
```
Replace before running:
- `https://matrix.example.org`: your homeserver base URL
- `$USER_ACCESS_TOKEN`: the recipient user's access token
- `openclaw-finalized-preview-botname`: a rule ID unique per bot per recipient (pattern: `openclaw-finalized-preview-<botname>`)
- `@bot:example.org`: your OpenClaw bot MXID, not the recipient's
</Step>
<Step title="Verify">
```bash
curl -sS \
-H "Authorization: Bearer $USER_ACCESS_TOKEN" \
"https://matrix.example.org/_matrix/client/v3/pushrules/global/override/openclaw-finalized-preview-botname"
```
Then test a streamed reply. In quiet mode the room shows a quiet draft preview and notifies once the block or turn finishes.
</Step>
</Steps>
To remove the rule later, `DELETE` the same rule URL with the recipient's token.
## Multi-bot notes
Push rules are keyed by `ruleId`: re-running `PUT` against the same ID updates a single rule. For multiple OpenClaw bots notifying the same recipient, create one rule per bot with a distinct sender match.
New user-defined `override` rules are inserted ahead of server-default suppress rules, so no extra ordering parameter is needed. The rule only affects text-only preview edits that can be finalized in place; media replies, stale-preview fallbacks, and final texts that would activate Matrix mentions are delivered as normal notifying messages instead.
## Homeserver notes
<AccordionGroup>
<Accordion title="Synapse">
No special `homeserver.yaml` change is required. If normal Matrix notifications already reach this user, the recipient token + `pushrules` call above is the main setup step.
If you run Synapse behind a reverse proxy or workers, make sure `/_matrix/client/.../pushrules/` reaches Synapse correctly. Push delivery is handled by the main process or `synapse.app.pusher` / configured pusher workers — ensure those are healthy.
The rule uses the `event_property_is` push-rule condition (MSC3758, push rule v1.10), which was added to Synapse in 2023. Older Synapse releases accept the `PUT pushrules/...` call but silently never match the condition — upgrade Synapse if no notification arrives on a finalized preview edit.
</Accordion>
<Accordion title="Tuwunel">
Same flow as Synapse; no Tuwunel-specific config is needed for the finalized preview marker.
If notifications disappear while the user is active on another device, check whether `suppress_push_when_active` is enabled. Tuwunel added this option in 1.4.2 (September 2025) and it can intentionally suppress pushes to other devices while one device is active.
</Accordion>
</AccordionGroup>
## Related
- [Matrix channel setup](/channels/matrix)
- [Streaming concepts](/concepts/streaming)

926
docs/channels/matrix.md Normal file
View File

@@ -0,0 +1,926 @@
---
summary: "Matrix support status, setup, and configuration examples"
read_when:
- Setting up Matrix in OpenClaw
- Configuring Matrix E2EE and verification
title: "Matrix"
---
Matrix is a downloadable channel plugin (`@openclaw/matrix`) built on the official `matrix-js-sdk`. It supports DMs, rooms, threads, media, reactions, polls, location, and E2EE.
## Install
```bash
openclaw plugins install @openclaw/matrix
```
Bare plugin specs try ClawHub first, then npm fallback. Force a source with `openclaw plugins install clawhub:@openclaw/matrix` or `npm:@openclaw/matrix`. From a local checkout: `openclaw plugins install ./path/to/local/matrix-plugin`.
`plugins install` registers and enables the plugin; no separate `enable` step is needed. The channel still does nothing until configured below. See [Plugins](/tools/plugin) for general install rules.
## Setup
1. Create a Matrix account on your homeserver.
2. Configure `channels.matrix` with `homeserver` + `accessToken`, or `homeserver` + `userId` + `password`.
3. Restart the gateway.
4. Start a DM with the bot, or invite it to a room. Fresh invites only land when [`autoJoin`](#auto-join) allows them.
### Interactive setup
```bash
openclaw channels add
openclaw configure --section channels
```
The wizard asks for homeserver URL, auth method (token or password), user ID (password auth only), optional device name, whether to enable E2EE, and room access/auto-join. If matching `MATRIX_*` env vars already exist and the account has no saved auth, the wizard offers an env-var shortcut. Resolve room names before saving an allowlist with `openclaw channels resolve --channel matrix "Project Room"`. Enabling E2EE in the wizard runs the same bootstrap as [`openclaw matrix encryption setup`](#encryption-and-verification).
### Minimal config
Token-based:
```json5
{
channels: {
matrix: {
enabled: true,
homeserver: "https://matrix.example.org",
accessToken: "syt_xxx",
dm: { policy: "pairing" },
},
},
}
```
Password-based (token is cached after first login):
```json5
{
channels: {
matrix: {
enabled: true,
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
password: "replace-me", // pragma: allowlist secret
deviceName: "OpenClaw Gateway",
},
},
}
```
### Auto-join
`channels.matrix.autoJoin` defaults to `"off"`: the bot will not appear in new rooms or DMs from fresh invites until you join manually. OpenClaw cannot tell at invite time whether an invite is a DM or a group, so every invite goes through `autoJoin` first; `dm.policy` only applies later, after the bot has joined and the room is classified.
<Warning>
Set `autoJoin: "allowlist"` plus `autoJoinAllowlist` to restrict accepted invites, or `autoJoin: "always"` to accept every invite.
`autoJoinAllowlist` accepts only `!roomId:server`, `#alias:server`, or `*`. Plain room names are rejected; aliases resolve against the homeserver, not against state the invited room claims.
</Warning>
```json5
{
channels: {
matrix: {
autoJoin: "allowlist",
autoJoinAllowlist: ["!ops:example.org", "#support:example.org"],
groups: {
"!ops:example.org": { requireMention: true },
},
},
},
}
```
### Allowlist target formats
- DMs (`dm.allowFrom`, `groupAllowFrom`, `groups.<room>.users`): use `@user:server`. Display names are ignored by default (mutable); set `dangerouslyAllowNameMatching: true` only for explicit display-name compatibility.
- Room allowlist keys (`groups`, legacy alias `rooms`): use `!room:server` or `#alias:server`. Plain names are ignored unless `dangerouslyAllowNameMatching: true`.
- Invite allowlists (`autoJoinAllowlist`): use `!room:server`, `#alias:server`, or `*`. Plain names are always rejected.
### Account ID normalization
The wizard converts a friendly name into a normalized account ID (`Ops Bot` -> `ops-bot`). Punctuation is hex-escaped in scoped env-var names so accounts cannot collide: `-` (0x2D) becomes `_X2D_`, so `ops-prod` maps to env prefix `MATRIX_OPS_X2D_PROD_`.
### Cached credentials
Matrix caches credentials under `~/.openclaw/credentials/matrix/`: `credentials.json` for the default account, `credentials-<account>.json` for named accounts. When cached credentials exist, OpenClaw treats Matrix as configured even without an `accessToken` in the config file - this covers setup, `openclaw doctor`, and channel-status probes.
### Environment variables
Config-key-backed env vars, used when the equivalent config key is unset. The default account uses unprefixed names; named accounts insert the account token before the suffix (see [normalization](#account-id-normalization)).
| Default account | Named account (`<ID>` = account token) |
| --------------------- | -------------------------------------- |
| `MATRIX_HOMESERVER` | `MATRIX_<ID>_HOMESERVER` |
| `MATRIX_ACCESS_TOKEN` | `MATRIX_<ID>_ACCESS_TOKEN` |
| `MATRIX_USER_ID` | `MATRIX_<ID>_USER_ID` |
| `MATRIX_PASSWORD` | `MATRIX_<ID>_PASSWORD` |
| `MATRIX_DEVICE_ID` | `MATRIX_<ID>_DEVICE_ID` |
| `MATRIX_DEVICE_NAME` | `MATRIX_<ID>_DEVICE_NAME` |
For account `ops`, names become `MATRIX_OPS_HOMESERVER`, `MATRIX_OPS_ACCESS_TOKEN`, and so on. `MATRIX_HOMESERVER` (and any `*_HOMESERVER` scoped variant) cannot be set from a workspace `.env`; see [Workspace `.env` files](/gateway/security).
<Note>
The recovery key is not a config-backed env var: OpenClaw never reads it from the environment itself. CLI guidance text suggests piping it through a shell variable named `MATRIX_RECOVERY_KEY` for the default account, or `MATRIX_RECOVERY_KEY_<ID>` (plain uppercased account ID, no hex-escaping) for a named account - see [Verify this device with a recovery key](#verify-this-device-with-a-recovery-key).
</Note>
## Configuration example
A practical baseline with DM pairing, room allowlist, and E2EE:
```json5
{
channels: {
matrix: {
enabled: true,
homeserver: "https://matrix.example.org",
accessToken: "syt_xxx",
encryption: true,
dm: {
policy: "pairing",
sessionScope: "per-room",
threadReplies: "off",
},
groupPolicy: "allowlist",
groupAllowFrom: ["@admin:example.org"],
groups: {
"!roomid:example.org": { requireMention: true },
},
autoJoin: "allowlist",
autoJoinAllowlist: ["!roomid:example.org"],
threadReplies: "inbound",
replyToMode: "off",
streaming: "partial",
},
},
}
```
## Streaming previews
Matrix reply streaming is opt-in. `streaming` controls how OpenClaw delivers the in-flight assistant reply; `blockStreaming` controls whether each completed block is kept as its own Matrix message.
```json5
{
channels: {
matrix: {
streaming: "partial",
},
},
}
```
To keep live answer previews but hide interim tool/progress lines, use object form:
```json5
{
channels: {
matrix: {
streaming: {
mode: "partial",
preview: {
toolProgress: false,
},
},
},
},
}
```
Full object form accepts `{ mode, preview, progress }`:
```json5
{
channels: {
matrix: {
streaming: {
mode: "progress",
progress: {
label: "auto", // pick from configured or built-in labels (false to hide)
labels: ["Thinking", "Writing", "Searching"], // candidates for label: "auto"
maxLines: 8, // max rolling progress lines (default: 8)
maxLineChars: 120, // max chars per line before truncation (default: 120)
toolProgress: true, // show tool/progress activity (default: true)
},
},
},
},
}
```
- `progress.label`: custom label, `"auto"`/unset to pick a configured or built-in label, or `false` to hide it.
- `progress.labels`: candidates used only when `label` is `"auto"` or unset.
- `progress.maxLines`: max rolling progress lines kept in the draft; older lines are trimmed past this.
- `progress.maxLineChars`: max characters per compact progress line before truncation.
- `progress.toolProgress`: when `true` (default), live tool/progress activity appears in the draft.
| `streaming` | Behavior |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"off"` (default) | Wait for the full reply, send once. `true` <-> `"partial"`, `false` <-> `"off"`. |
| `"partial"` | Edit one normal text message in place as the model writes the current block. Stock clients may notify on the first preview, not the final edit. |
| `"quiet"` | Same as `"partial"` but the message is a non-notifying notice. Recipients are notified once a per-user push rule matches the finalized edit (see below). |
| `"progress"` | Sends individual compact progress lines using a progress draft. |
`blockStreaming` (default `false`) is independent of `streaming`:
| `streaming` | `blockStreaming: true` | `blockStreaming: false` (default) |
| ----------------------- | ------------------------------------------------------------------- | ---------------------------------------------------- |
| `"partial"` / `"quiet"` | Live draft for the current block, completed blocks kept as messages | Live draft for the current block, finalized in place |
| `"off"` | One notifying Matrix message per finished block | One notifying Matrix message for the full reply |
Notes:
- If a preview grows past Matrix's per-event size limit, OpenClaw stops preview streaming and falls back to final-only delivery.
- Media replies always send attachments normally; if a stale preview cannot be reused safely, OpenClaw redacts it before sending the final media reply.
- Tool-progress preview updates are on by default when preview streaming is active. Set `streaming.preview.toolProgress: false` to keep preview edits for answer text but leave tool progress on the normal delivery path.
- Preview edits cost extra Matrix API calls. Leave `streaming: "off"` for the most conservative rate-limit profile.
## Voice messages
Inbound Matrix voice notes are transcribed before the room mention gate, so a voice note saying the bot name can trigger the agent in a `requireMention: true` room, and the agent gets the transcript instead of only an audio attachment placeholder.
Matrix uses the shared audio media provider under `tools.media.audio`, such as OpenAI `gpt-4o-mini-transcribe`. See [Media tools overview](/tools/media-overview) for provider setup and limits.
- `m.audio` events and `m.file` events with an `audio/*` MIME type are eligible.
- In encrypted rooms, OpenClaw decrypts the attachment through the existing Matrix media path before transcription.
- The transcript is marked machine-generated and untrusted in the agent prompt.
- The attachment is marked as already transcribed so downstream media tools do not transcribe it again.
- Set `tools.media.audio.enabled: false` to disable audio transcription globally.
## Approval metadata
Matrix native approval prompts are normal `m.room.message` events with OpenClaw-specific content under the `com.openclaw.approval` key. Stock clients still render the text body; OpenClaw-aware clients can read the structured approval id, kind, state, decisions, and exec/plugin details.
When a prompt is too long for one Matrix event, OpenClaw chunks the visible text and attaches `com.openclaw.approval` to the first chunk only. Allow/deny reactions bind to that first event, so long prompts keep the same approval target as single-event prompts.
### Self-hosted push rules for quiet finalized previews
`streaming: "quiet"` only notifies recipients once a block or turn is finalized - a per-user push rule must match the finalized preview marker. See [Matrix push rules for quiet previews](/channels/matrix-push-rules) for the full recipe.
## Bot-to-bot rooms
By default, Matrix messages from other configured OpenClaw Matrix accounts are ignored. Use `allowBots` to intentionally allow inter-agent traffic:
```json5
{
channels: {
matrix: {
allowBots: "mentions", // true | "mentions"
groups: {
"!roomid:example.org": {
requireMention: true,
},
},
},
},
}
```
- `allowBots: true` accepts messages from other configured Matrix bot accounts in allowed rooms and DMs.
- `allowBots: "mentions"` accepts those messages only when they visibly mention this bot in rooms; DMs are still allowed regardless.
- `groups.<room>.allowBots` overrides the account-level setting for one room.
- Accepted configured-bot messages use shared [bot loop protection](/channels/bot-loop-protection). Configure `channels.defaults.botLoopProtection`, then override per-account with `channels.matrix.botLoopProtection` or per-room with `channels.matrix.groups.<room>.botLoopProtection`.
- OpenClaw still ignores messages from the same Matrix user ID to avoid self-reply loops.
- Matrix has no native bot flag; OpenClaw treats "bot-authored" as "sent by another configured Matrix account on this OpenClaw gateway".
Use strict room allowlists and mention requirements when enabling bot-to-bot traffic in shared rooms.
## Encryption and verification
In encrypted (E2EE) rooms, outbound image events use `thumbnail_file` so image previews are encrypted alongside the full attachment; unencrypted rooms use plain `thumbnail_url`. No configuration is needed - the plugin detects E2EE state automatically.
All `openclaw matrix` commands accept `--verbose` (full diagnostics), `--json` (machine-readable output), and `--account <id>` (multi-account setups). Output is concise by default.
### Enable encryption
```bash
openclaw matrix encryption setup
```
Bootstraps secret storage and cross-signing, creates a room-key backup if needed, then prints status and next steps. Useful flags:
- `--recovery-key <key>` apply a recovery key before bootstrapping (prefer the stdin form below)
- `--force-reset-cross-signing` discard the current cross-signing identity and create a new one (intentional use only)
For a new account, enable E2EE at creation time:
```bash
openclaw matrix account add \
--homeserver https://matrix.example.org \
--access-token syt_xxx \
--enable-e2ee
```
`--encryption` is an alias for `--enable-e2ee`. Manual config equivalent:
```json5
{
channels: {
matrix: {
enabled: true,
homeserver: "https://matrix.example.org",
accessToken: "syt_xxx",
encryption: true,
dm: { policy: "pairing" },
},
},
}
```
### Status and trust signals
```bash
openclaw matrix verify status
openclaw matrix verify status --include-recovery-key --json
```
`verify status` reports three independent trust signals (`--verbose` shows all of them):
- `Locally trusted`: trusted by this client only
- `Cross-signing verified`: the SDK reports verification via cross-signing
- `Signed by owner`: signed by your own self-signing key (diagnostic only)
`Verified by owner` is `yes` only when `Cross-signing verified` is `yes`; local trust or an owner signature alone is not enough.
`--allow-degraded-local-state` returns best-effort diagnostics without preparing the Matrix account first; useful for offline or partially-configured probes.
### Verify this device with a recovery key
Pipe the recovery key via stdin instead of passing it on the command line:
```bash
printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin
```
The command reports three states:
- `Recovery key accepted`: Matrix accepted the key for secret storage or device trust.
- `Backup usable`: room-key backup can be loaded with the trusted recovery material.
- `Device verified by owner`: this device has full Matrix cross-signing identity trust.
It exits non-zero when full identity trust is incomplete, even if the recovery key unlocked backup material. In that case, finish self-verification from another Matrix client:
```bash
openclaw matrix verify self
```
`verify self` waits for `Cross-signing verified: yes` before exiting successfully. Use `--timeout-ms <ms>` to tune the wait.
The literal-key form `openclaw matrix verify device "<recovery-key>"` also works, but the key ends up in shell history.
### Bootstrap or repair cross-signing
```bash
openclaw matrix verify bootstrap
```
The repair/setup command for encrypted accounts. In order, it:
- bootstraps secret storage, reusing an existing recovery key when possible
- bootstraps cross-signing and uploads missing public keys
- marks and cross-signs the current device
- creates a server-side room-key backup if one does not already exist
If the homeserver requires UIA to upload cross-signing keys, OpenClaw tries no-auth first, then `m.login.dummy`, then `m.login.password` (requires `channels.matrix.password`).
Useful flags:
- `--recovery-key-stdin` (pair with `printf '%s\n' "$MATRIX_RECOVERY_KEY" | ...`) or `--recovery-key <key>`
- `--force-reset-cross-signing` to discard the current cross-signing identity (intentional only; requires the active recovery key stored or supplied with `--recovery-key-stdin`)
### Room-key backup
```bash
openclaw matrix verify backup status
printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin
```
`backup status` shows whether a server-side backup exists and whether this device can decrypt it. `backup restore` imports backed-up room keys into the local crypto store; omit `--recovery-key-stdin` if the recovery key is already on disk.
To replace a broken backup with a fresh baseline (accepts losing unrecoverable old history; can also recreate secret storage if the current backup secret is unloadable):
```bash
openclaw matrix verify backup reset --yes
```
Add `--rotate-recovery-key` only when the previous recovery key should intentionally stop unlocking the fresh backup baseline.
### Listing, requesting, and responding to verifications
```bash
openclaw matrix verify list
```
Lists pending verification requests for the selected account.
```bash
openclaw matrix verify request --own-user
openclaw matrix verify request --user-id @ops:example.org --device-id ABCDEF
```
Sends a verification request from this account. `--own-user` requests self-verification (accept the prompt in another Matrix client of the same user); `--user-id`/`--device-id`/`--room-id` target someone else. `--own-user` cannot combine with the other targeting flags.
For lower-level lifecycle handling - typically while shadowing inbound requests from another client - these commands act on a specific request `<id>` (printed by `verify list` and `verify request`):
| Command | Purpose |
| ------------------------------------------ | ------------------------------------------------------------------- |
| `openclaw matrix verify accept <id>` | Accept an inbound request |
| `openclaw matrix verify start <id>` | Start the SAS flow |
| `openclaw matrix verify sas <id>` | Print the SAS emoji or decimals |
| `openclaw matrix verify confirm-sas <id>` | Confirm that the SAS matches what the other client shows |
| `openclaw matrix verify mismatch-sas <id>` | Reject the SAS when the emoji or decimals do not match |
| `openclaw matrix verify cancel <id>` | Cancel; takes optional `--reason <text>` and `--code <matrix-code>` |
`accept`, `start`, `sas`, `confirm-sas`, `mismatch-sas`, and `cancel` all accept `--user-id` and `--room-id` as DM follow-up hints when the verification is anchored to a specific direct-message room.
### Multi-account notes
Without `--account <id>`, Matrix CLI commands use the implicit default account. With multiple named accounts and no `channels.matrix.defaultAccount`, commands refuse to guess and ask you to choose. When E2EE is disabled or unavailable for a named account, errors point at that account's config key, for example `channels.matrix.accounts.assistant.encryption`.
<AccordionGroup>
<Accordion title="Startup behavior">
With `encryption: true`, `startupVerification` defaults to `"if-unverified"`. On startup an unverified device requests self-verification in another Matrix client, skipping duplicates and applying a cooldown (24 hours by default). Tune with `startupVerificationCooldownHours` or disable with `startupVerification: "off"`.
Startup also runs a conservative crypto bootstrap pass reusing the current secret storage and cross-signing identity. If bootstrap state is broken, OpenClaw attempts a guarded repair even without `channels.matrix.password`; if the homeserver requires password UIA, startup logs a warning and stays non-fatal. Already-owner-signed devices are preserved.
See [Matrix migration](/channels/matrix-migration) for the full upgrade flow.
</Accordion>
<Accordion title="Verification notices">
Matrix posts verification lifecycle notices into the strict DM verification room as `m.notice` messages: request, ready (with "Verify by emoji" guidance), start/completion, and SAS (emoji/decimal) details when available.
Incoming requests from another Matrix client are tracked and auto-accepted. For self-verification, OpenClaw starts the SAS flow automatically and confirms its own side once emoji verification is available - you still need to compare and confirm "They match" in your Matrix client.
Verification system notices are not forwarded to the agent chat pipeline.
</Accordion>
<Accordion title="Deleted or invalid Matrix device">
If `verify status` says the current device is no longer listed on the homeserver, create a new OpenClaw Matrix device. For password login:
```bash
openclaw matrix account add \
--account assistant \
--homeserver https://matrix.example.org \
--user-id '@assistant:example.org' \
--password '<password>' \
--device-name OpenClaw-Gateway
```
For token auth, create a fresh access token in your Matrix client or admin UI, then update OpenClaw:
```bash
openclaw matrix account add \
--account assistant \
--homeserver https://matrix.example.org \
--access-token '<token>'
```
Replace `assistant` with the account ID from the failed command, or omit `--account` for the default account.
</Accordion>
<Accordion title="Device hygiene">
Old OpenClaw-managed devices can accumulate. List and prune:
```bash
openclaw matrix devices list
openclaw matrix devices prune-stale
```
</Accordion>
<Accordion title="Crypto store">
Matrix E2EE uses the official `matrix-js-sdk` Rust crypto path with `fake-indexeddb` as the IndexedDB shim. Crypto state persists to `crypto-idb-snapshot.json` (restrictive file permissions).
Encrypted runtime state lives under `~/.openclaw/matrix/accounts/<account>/<homeserver>__<user>/<token-hash>/` and includes the sync store, crypto store, recovery key, IDB snapshot, thread bindings, and startup verification state. When the token changes but the account identity stays the same, OpenClaw reuses the best existing root so prior state remains visible.
A single older token-hash root can be a normal token-rotation continuity path. If OpenClaw logs `matrix: multiple populated token-hash storage roots detected`, inspect the account directory and archive stale sibling roots only after confirming the selected active root is healthy. Prefer moving stale roots into an `_archive/` directory over deleting them immediately.
</Accordion>
</AccordionGroup>
## Profile management
```bash
openclaw matrix profile set --name "OpenClaw Assistant"
openclaw matrix profile set --avatar-url https://cdn.example.org/avatar.png
```
Pass both options in one call. Matrix accepts `mxc://` avatar URLs directly; passing `http://`/`https://` uploads the file first and stores the resolved `mxc://` URL into `channels.matrix.avatarUrl` (or the per-account override).
## Threads
Matrix supports native threads for both automatic replies and message-tool sends. Two independent knobs control behavior:
### Session routing (`sessionScope`)
`dm.sessionScope` decides how Matrix DM rooms map to OpenClaw sessions:
- `"per-user"` (default): all DM rooms with the same routed peer share one session.
- `"per-room"`: each Matrix DM room gets its own session key, even for the same peer.
Explicit conversation bindings always win over `sessionScope`; bound rooms and threads keep their chosen target session.
### Reply threading (`threadReplies`)
`threadReplies` decides where the bot posts its reply:
- `"off"`: replies are top-level. Inbound threaded messages stay on the parent session.
- `"inbound"`: reply inside a thread only when the inbound message was already in that thread.
- `"always"`: reply inside a thread rooted at the triggering message; that conversation routes through a matching thread-scoped session from the first trigger onward.
`dm.threadReplies` overrides this for DMs only - for example, keep room threads isolated while keeping DMs flat.
### Thread inheritance and slash commands
- Inbound threaded messages include the thread root message as extra agent context.
- Message-tool sends auto-inherit the current Matrix thread when targeting the same room (or the same DM user target), unless an explicit `threadId` is provided.
- DM user-target reuse only kicks in when current session metadata proves the same DM peer on the same Matrix account; otherwise OpenClaw falls back to normal user-scoped routing.
- `/focus`, `/unfocus`, `/agents`, `/session idle`, `/session max-age`, and thread-bound `/acp spawn` all work in Matrix rooms and DMs.
- Top-level `/focus` creates a new Matrix thread and binds it to the target session when `threadBindings.spawnSessions` is enabled.
- Running `/focus` or `/acp spawn --thread here` inside an existing Matrix thread binds that thread in place.
When OpenClaw detects a Matrix DM room colliding with another DM room on the same shared session, it posts a one-time `m.notice` pointing to the `/focus` escape hatch and suggesting a `dm.sessionScope` change. The notice only appears when thread bindings are enabled.
## ACP conversation bindings
Matrix rooms, DMs, and existing Matrix threads can become durable ACP workspaces without changing the chat surface.
Fast operator flow:
- Run `/acp spawn codex --bind here` inside the Matrix DM, room, or existing thread to keep using.
- In a top-level DM or room, the current DM/room stays the chat surface and future messages route to the spawned ACP session.
- Inside an existing thread, `--bind here` binds that current thread in place.
- `/new` and `/reset` reset the same bound ACP session in place.
- `/acp close` closes the ACP session and removes the binding.
`--bind here` does not create a child Matrix thread. `threadBindings.spawnSessions` gates `/acp spawn --thread auto|here`, where OpenClaw needs to create or bind a child thread.
### Thread binding config
Matrix inherits global defaults from `session.threadBindings` and supports per-channel overrides:
- `threadBindings.enabled`
- `threadBindings.idleHours`
- `threadBindings.maxAgeHours`
- `threadBindings.spawnSessions`: gates both subagent and ACP thread spawns.
- `threadBindings.spawnSubagentSessions` / `threadBindings.spawnAcpSessions`: narrower overrides for subagent-only or ACP-only spawns.
- `threadBindings.defaultSpawnContext`
Matrix thread-bound session spawns default on. Set `threadBindings.spawnSessions: false` to block top-level `/focus` and `/acp spawn --thread auto|here` from creating/binding Matrix threads. Set `threadBindings.defaultSpawnContext: "isolated"` when native subagent thread spawns should not fork the parent transcript.
## Reactions
Matrix supports outbound reactions, inbound reaction notifications, and ack reactions.
Outbound reaction tooling is gated by `channels.matrix.actions.reactions`:
- `react` adds a reaction to a Matrix event.
- `reactions` lists the current reaction summary for a Matrix event.
- `emoji=""` removes the bot's own reactions on that event.
- `remove: true` removes only the specified emoji reaction from the bot.
**Resolution order** (first defined value wins):
| Setting | Order |
| ----------------------- | ----------------------------------------------------------------------------------- |
| `ackReaction` | per-account -> channel -> `messages.ackReaction` -> agent identity emoji fallback |
| `ackReactionScope` | per-account -> channel -> `messages.ackReactionScope` -> default `"group-mentions"` |
| `reactionNotifications` | per-account -> channel -> default `"own"` |
`reactionNotifications: "own"` forwards added `m.reaction` events when they target bot-authored Matrix messages; `"off"` disables reaction system events. Reaction removals are not synthesized into system events - Matrix surfaces those as redactions, not as standalone `m.reaction` removals.
## History context
- `channels.matrix.historyLimit` controls how many recent room messages are included as `InboundHistory` when a room message triggers the agent. Falls back to `messages.groupChat.historyLimit`; effective default `0` if both are unset (disabled).
- Matrix room history is room-only; DMs keep using normal session history.
- Room history is pending-only: OpenClaw buffers room messages that did not trigger a reply yet, then snapshots that window when a mention or other trigger arrives.
- The current trigger message is not included in `InboundHistory`; it stays in the main inbound body for that turn.
- Retries of the same Matrix event reuse the original history snapshot instead of drifting forward to newer room messages.
## Context visibility
Matrix supports the shared `contextVisibility` control for supplemental room context such as fetched reply text, thread roots, and pending history.
- `contextVisibility: "all"` is the default. Supplemental context is kept as received.
- `contextVisibility: "allowlist"` filters supplemental context to senders allowed by the active room/user allowlist checks.
- `contextVisibility: "allowlist_quote"` behaves like `allowlist`, but still keeps one explicit quoted reply.
This affects supplemental context visibility only, not whether the inbound message itself can trigger a reply. Trigger authorization still comes from `groupPolicy`, `groups`, `groupAllowFrom`, and DM policy settings.
## DM and room policy
```json5
{
channels: {
matrix: {
dm: {
policy: "allowlist",
allowFrom: ["@admin:example.org"],
threadReplies: "off",
},
groupPolicy: "allowlist",
groupAllowFrom: ["@admin:example.org"],
groups: {
"!roomid:example.org": { requireMention: true },
},
},
},
}
```
To silence DMs entirely while keeping rooms working, set `dm.enabled: false`:
```json5
{
channels: {
matrix: {
dm: { enabled: false },
groupPolicy: "allowlist",
groupAllowFrom: ["@admin:example.org"],
},
},
}
```
See [Groups](/channels/groups) for mention-gating and allowlist behavior.
Pairing example for Matrix DMs:
```bash
openclaw pairing list matrix
openclaw pairing approve matrix <CODE>
```
If an unapproved Matrix user keeps messaging before approval, OpenClaw reuses the same pending pairing code and may send a reminder reply after a short cooldown instead of minting a new code.
See [Pairing](/channels/pairing) for the shared DM pairing flow and storage layout.
## Direct room repair
If direct-message state drifts, OpenClaw can end up with stale `m.direct` mappings pointing at old solo rooms instead of the live DM. Inspect the current mapping for a peer:
```bash
openclaw matrix direct inspect --user-id @alice:example.org
```
Repair it:
```bash
openclaw matrix direct repair --user-id @alice:example.org
```
Both commands accept `--account <id>` for multi-account setups. The repair flow:
- prefers a strict 1:1 DM already mapped in `m.direct`
- falls back to any currently joined strict 1:1 DM with that user
- creates a fresh direct room and rewrites `m.direct` if no healthy DM exists
It does not delete old rooms automatically. It picks the healthy DM and updates the mapping so future Matrix sends, verification notices, and other direct-message flows target the right room.
## Exec approvals
Matrix can act as a native approval client. Configure under `channels.matrix.execApprovals` (or `channels.matrix.accounts.<account>.execApprovals` for a per-account override):
- `enabled`: deliver approvals through Matrix-native prompts. Unset or `"auto"` auto-enables once at least one approver can be resolved; set `false` to disable explicitly.
- `approvers`: Matrix user IDs (`@owner:example.org`) allowed to approve exec requests. Falls back to `channels.matrix.dm.allowFrom`.
- `target`: where prompts go. `"dm"` (default) sends to approver DMs; `"channel"` sends to the originating room or DM; `"both"` sends to both.
- `agentFilter` / `sessionFilter`: optional allowlists for which agents/sessions trigger Matrix delivery.
Authorization differs slightly between approval kinds:
- **Exec approvals** use `execApprovals.approvers`, falling back to `dm.allowFrom`.
- **Plugin approvals** authorize through `dm.allowFrom` only.
Both kinds share Matrix reaction shortcuts and message updates. Approvers see reaction shortcuts on the primary approval message:
- ✅ allow once
- ❌ deny
- ♾️ allow always (when the effective exec policy allows it)
Fallback slash commands: `/approve <id> allow-once`, `/approve <id> allow-always`, `/approve <id> deny`.
Only resolved approvers can approve or deny. Channel delivery for exec approvals includes the command text - only enable `channel` or `both` in trusted rooms.
Related: [Exec approvals](/tools/exec-approvals).
## Slash commands
Slash commands (`/new`, `/reset`, `/model`, `/focus`, `/unfocus`, `/agents`, `/session`, `/acp`, `/approve`, etc.) work directly in DMs. In rooms, OpenClaw also recognizes commands prefixed with the bot's own Matrix mention, so `@bot:server /new` triggers the command path without a custom mention regex - this keeps the bot responsive to the room-style `@mention /command` posts that Element and similar clients emit when a user tab-completes the bot before typing the command.
Authorization rules still apply: command senders must satisfy the same DM or room allowlist/owner policies as plain messages.
## Multi-account
```json5
{
channels: {
matrix: {
enabled: true,
defaultAccount: "assistant",
dm: { policy: "pairing" },
accounts: {
assistant: {
homeserver: "https://matrix.example.org",
accessToken: "syt_assistant_xxx",
encryption: true,
},
alerts: {
homeserver: "https://matrix.example.org",
accessToken: "syt_alerts_xxx",
dm: {
policy: "allowlist",
allowFrom: ["@ops:example.org"],
threadReplies: "off",
},
},
},
},
},
}
```
**Inheritance:**
- Top-level `channels.matrix` values act as defaults for named accounts unless an account overrides them.
- Scope an inherited room entry to a specific account with `groups.<room>.account`. Entries without `account` are shared across accounts; `account: "default"` still works when the default account is configured at the top level.
**Default account selection:**
- Set `defaultAccount` to pick the named account that implicit routing, probing, and CLI commands prefer.
- If you have multiple accounts and one is literally named `default`, OpenClaw uses it implicitly even when `defaultAccount` is unset.
- With multiple named accounts and no default selected, CLI commands refuse to guess - set `defaultAccount` or pass `--account <id>`.
- The top-level `channels.matrix.*` block is only treated as the implicit `default` account when its auth is complete (`homeserver` + `accessToken`, or `homeserver` + `userId` + `password`). Named accounts remain discoverable from `homeserver` + `userId` once cached credentials cover auth.
**Promotion:**
- When OpenClaw promotes a single-account config to multi-account during repair or setup, it preserves the existing named account if one exists or `defaultAccount` already points at one. Only Matrix auth/bootstrap keys move into the promoted account; shared delivery-policy keys stay at the top level.
See [Configuration reference](/gateway/config-channels#multi-account-all-channels) for the shared multi-account pattern.
## Private/LAN homeservers
By default, OpenClaw blocks private/internal Matrix homeservers for SSRF protection unless you opt in per account.
If your homeserver runs on localhost, a LAN/Tailscale IP, or an internal hostname, enable `network.dangerouslyAllowPrivateNetwork` for that account:
```json5
{
channels: {
matrix: {
homeserver: "http://matrix-synapse:8008",
network: {
dangerouslyAllowPrivateNetwork: true,
},
accessToken: "syt_internal_xxx",
},
},
}
```
CLI setup example:
```bash
openclaw matrix account add \
--account ops \
--homeserver http://matrix-synapse:8008 \
--allow-private-network \
--access-token syt_ops_xxx
```
This opt-in only allows trusted private/internal targets. Public cleartext homeservers such as `http://matrix.example.org:8008` remain blocked. Prefer `https://` whenever possible.
## Proxying Matrix traffic
If your Matrix deployment needs an explicit outbound HTTP(S) proxy, set `channels.matrix.proxy`:
```json5
{
channels: {
matrix: {
homeserver: "https://matrix.example.org",
accessToken: "syt_bot_xxx",
proxy: "http://127.0.0.1:7890",
},
},
}
```
Named accounts can override the top-level default with `channels.matrix.accounts.<id>.proxy`. OpenClaw uses the same proxy setting for runtime Matrix traffic and account status probes.
## Target resolution
Matrix accepts these target forms anywhere OpenClaw asks for a room or user target:
- Users: `@user:server`, `user:@user:server`, or `matrix:user:@user:server`
- Rooms: `!room:server`, `room:!room:server`, or `matrix:room:!room:server`
- Aliases: `#alias:server`, `channel:#alias:server`, or `matrix:channel:#alias:server`
Matrix room IDs are case-sensitive. Use the exact room ID casing from Matrix when configuring explicit delivery targets, cron jobs, bindings, or allowlists. OpenClaw keeps internal session keys canonical for storage, so those lowercase keys are not a reliable source for Matrix delivery IDs.
Live directory lookup uses the logged-in Matrix account:
- User lookups query the Matrix user directory on that homeserver.
- Room lookups accept explicit room IDs and aliases directly. Joined-room name lookup is best-effort and only applies to runtime room allowlists when `dangerouslyAllowNameMatching: true` is set.
- If a room name cannot be resolved to an ID or alias, it is ignored by runtime allowlist resolution.
## Configuration reference
Allowlist-style user fields (`groupAllowFrom`, `dm.allowFrom`, `groups.<room>.users`) accept full Matrix user IDs (safest). Non-ID entries are ignored by default. If `dangerouslyAllowNameMatching: true` is set, exact Matrix directory display-name matches are resolved at startup and whenever the allowlist changes while the monitor is running; unresolvable entries are ignored at runtime.
Room allowlist keys (`groups`, legacy `rooms`) should be room IDs or aliases. Plain room-name keys are ignored by default; `dangerouslyAllowNameMatching: true` restores best-effort lookup against joined room names.
### Account and connection
- `enabled`: enable or disable the channel.
- `name`: optional display label for the account.
- `defaultAccount`: preferred account ID when multiple Matrix accounts are configured.
- `accounts`: named per-account overrides. Top-level `channels.matrix` values are inherited as defaults.
- `homeserver`: homeserver URL, for example `https://matrix.example.org`.
- `network.dangerouslyAllowPrivateNetwork`: allow this account to connect to `localhost`, LAN/Tailscale IPs, or internal hostnames.
- `proxy`: optional HTTP(S) proxy URL for Matrix traffic. Per-account override supported.
- `userId`: full Matrix user ID (`@bot:example.org`).
- `accessToken`: access token for token-based auth. Plaintext and SecretRef values supported across env/file/exec providers ([Secrets Management](/gateway/secrets)).
- `password`: password for password-based login. Plaintext and SecretRef values supported.
- `deviceId`: explicit Matrix device ID.
- `deviceName`: device display name used at password-login time.
- `avatarUrl`: stored self-avatar URL for profile sync and `profile set` updates.
- `initialSyncLimit`: maximum number of events fetched during startup sync.
### Encryption
- `encryption`: enable E2EE. Default: `false`.
- `startupVerification`: `"if-unverified"` (default when E2EE is on) or `"off"`. Auto-requests self-verification on startup when this device is unverified.
- `startupVerificationCooldownHours`: cooldown before the next automatic startup request. Default: `24`.
### Access and policy
- `groupPolicy`: `"open"`, `"allowlist"`, or `"disabled"`. Default: `"allowlist"`.
- `groupAllowFrom`: allowlist of user IDs for room traffic.
- `mentionPatterns`: scoped regex patterns for room mentions. Object with `{ mode: "allow"|"deny", allowIn: [roomId, ...], denyIn: [roomId, ...] }`. Controls whether configured `agents.list[].groupChat.mentionPatterns` apply per-room.
- `dm.enabled`: when `false`, ignore all DMs. Default: `true`.
- `dm.policy`: `"pairing"` (default), `"allowlist"`, `"open"`, or `"disabled"`. Applies after the bot has joined and classified the room as a DM; it does not affect invite handling.
- `dm.allowFrom`: allowlist of user IDs for DM traffic.
- `dm.sessionScope`: `"per-user"` (default) or `"per-room"`.
- `dm.threadReplies`: DM-only override for reply threading (`"off"`, `"inbound"`, `"always"`).
- `allowBots`: accept messages from other configured Matrix bot accounts (`true` or `"mentions"`).
- `allowlistOnly`: when `true`, forces all active DM policies (except `"disabled"`) and `"open"` group policies to `"allowlist"`. Does not change `"disabled"` policies.
- `dangerouslyAllowNameMatching`: when `true`, allows Matrix display-name directory lookup for user allowlist entries and joined-room name lookup for room allowlist keys. Prefer full `@user:server` IDs and room IDs or aliases.
- `autoJoin`: `"always"`, `"allowlist"`, or `"off"`. Default: `"off"`. Applies to every Matrix invite, including DM-style invites.
- `autoJoinAllowlist`: rooms/aliases allowed when `autoJoin` is `"allowlist"`. Alias entries resolve against the homeserver, not against state claimed by the invited room.
- `contextVisibility`: supplemental context visibility (`"all"` default, `"allowlist"`, `"allowlist_quote"`).
### Reply behavior
- `replyToMode`: `"off"` (default), `"first"`, `"all"`, or `"batched"`.
- `threadReplies`: `"off"` (top-level default resolves to `"inbound"` unless explicitly set), `"inbound"`, or `"always"`.
- `threadBindings`: per-channel overrides for thread-bound session routing and lifecycle.
- `streaming`: `"off"` (default), `"partial"`, `"quiet"`, `"progress"`, or object form `{ mode, preview: { toolProgress }, progress: { label, labels, maxLines, maxLineChars, toolProgress } }`. `true` <-> `"partial"`, `false` <-> `"off"`.
- `blockStreaming`: when `true`, completed assistant blocks are kept as separate progress messages. Default: `false`.
- `markdown`: optional Markdown rendering config for outbound text.
- `responsePrefix`: optional string prepended to outbound replies.
- `textChunkLimit`: outbound chunk size in characters when `chunkMode: "length"`. Default: `4000`.
- `chunkMode`: `"length"` (default, splits by character count) or `"newline"` (splits at line boundaries).
- `historyLimit`: number of recent room messages included as `InboundHistory` when a room message triggers the agent. Falls back to `messages.groupChat.historyLimit`; effective default `0` (disabled).
- `mediaMaxMb`: media size cap in MB for outbound sends and inbound processing. Default: `20`.
### Reaction settings
- `ackReaction`: ack reaction override for this channel/account.
- `ackReactionScope`: scope override (`"group-mentions"` default, `"group-all"`, `"direct"`, `"all"`, `"none"`, `"off"`).
- `reactionNotifications`: inbound reaction notification mode (`"own"` default, `"off"`).
### Tooling and per-room overrides
- `actions`: per-action tool gating (`messages`, `reactions`, `pins`, `profile`, `memberInfo`, `channelInfo`, `verification`).
- `groups`: per-room policy map. Session identity uses the stable room ID after resolution. (`rooms` is a legacy alias.)
- `groups.<room>.account`: restrict one inherited room entry to a specific account.
- `groups.<room>.enabled`: per-room toggle. When `false`, the room is ignored as if it were not in the map.
- `groups.<room>.requireMention`: per-room override of the channel-level mention requirement.
- `groups.<room>.allowBots`: per-room override of the channel-level setting (`true` or `"mentions"`).
- `groups.<room>.botLoopProtection`: per-room override for bot-to-bot loop protection budget.
- `groups.<room>.users`: per-room sender allowlist.
- `groups.<room>.tools`: per-room tool allow/deny overrides.
- `groups.<room>.autoReply`: per-room mention-gating override. `true` disables mention requirements for that room; `false` forces them back on.
- `groups.<room>.skills`: per-room skill filter.
- `groups.<room>.systemPrompt`: per-room system prompt snippet.
### Exec approval settings
- `execApprovals.enabled`: deliver exec approvals through Matrix-native prompts.
- `execApprovals.approvers`: Matrix user IDs allowed to approve. Falls back to `dm.allowFrom`.
- `execApprovals.target`: `"dm"` (default), `"channel"`, or `"both"`.
- `execApprovals.agentFilter` / `execApprovals.sessionFilter`: optional agent/session allowlists for delivery.
## Related
- [Channels Overview](/channels) - all supported channels
- [Pairing](/channels/pairing) - DM authentication and pairing flow
- [Groups](/channels/groups) - group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) - session routing for messages
- [Security](/gateway/security) - access model and hardening

568
docs/channels/mattermost.md Normal file
View File

@@ -0,0 +1,568 @@
---
summary: "Mattermost bot setup and OpenClaw config"
read_when:
- Setting up Mattermost
- Debugging Mattermost routing
title: "Mattermost"
sidebarTitle: "Mattermost"
---
Status: downloadable plugin (bot token + WebSocket events). Channels, private channels, group DMs, and DMs are supported. Mattermost is a self-hostable team messaging platform ([mattermost.com](https://mattermost.com)).
## Install
<Tabs>
<Tab title="npm registry">
```bash
openclaw plugins install @openclaw/mattermost
```
</Tab>
<Tab title="Local checkout">
```bash
openclaw plugins install ./path/to/local/mattermost-plugin
```
</Tab>
</Tabs>
Details: [Plugins](/tools/plugin)
## Quick setup
<Steps>
<Step title="Ensure plugin is available">
Install `@openclaw/mattermost` with the command above, then restart the Gateway if it is already running.
</Step>
<Step title="Create a Mattermost bot">
Create a Mattermost bot account, copy the **bot token**, and add the bot to the teams and channels it should read.
</Step>
<Step title="Copy the base URL">
Copy the Mattermost **base URL** (e.g., `https://chat.example.com`). A trailing `/api/v4` is stripped automatically.
</Step>
<Step title="Configure OpenClaw and start the gateway">
Minimal config:
```json5
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
},
},
}
```
Non-interactive alternative:
```bash
openclaw channels add --channel mattermost --bot-token <token> --http-url https://chat.example.com
```
</Step>
</Steps>
<Note>
Self-hosted Mattermost on a private/LAN/tailnet address: outbound Mattermost API requests pass through an SSRF guard that blocks private and internal IPs by default. Opt in with `channels.mattermost.network.dangerouslyAllowPrivateNetwork: true` (per account: `channels.mattermost.accounts.<id>.network.dangerouslyAllowPrivateNetwork`).
</Note>
## Native slash commands
Native slash commands are opt-in. When enabled, OpenClaw registers `oc_*` slash commands on every team the bot is a member of and receives callback POSTs on the gateway HTTP server.
```json5
{
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",
},
},
},
}
```
Registered commands: `/oc_status`, `/oc_model`, `/oc_models`, `/oc_new`, `/oc_help`, `/oc_think`, `/oc_reasoning`, `/oc_verbose`, `/oc_queue`. With `nativeSkills: true`, skill commands are also registered as `/oc_<skill>`.
<AccordionGroup>
<Accordion title="Behavior notes">
- `native` and `nativeSkills` default to `"auto"`, which resolves to disabled for Mattermost. Set them to `true` explicitly.
- `callbackPath` defaults to `/api/channels/mattermost/command`.
- If `callbackUrl` is omitted, OpenClaw derives `http://<gateway.customBindHost or localhost>:<gateway.port, default 18789><callbackPath>`. Wildcard bind hosts (`0.0.0.0`, `::`) fall back to `localhost`.
- For multi-account setups, `commands` can be set at the top level or under `channels.mattermost.accounts.<id>.commands` (account values override top-level fields).
- Existing slash commands with the same trigger created by other integrations are left untouched (registration skips them); commands the bot created are updated or recreated when the callback URL drifts.
- Command callbacks are validated with the per-command tokens returned by Mattermost when OpenClaw registers `oc_*` commands.
- OpenClaw refreshes current Mattermost command registration before accepting each callback, so stale tokens from deleted or regenerated slash commands stop being accepted without a gateway restart.
- Callback validation fails closed if the Mattermost API cannot confirm the command is still current; failed validations are cached briefly, concurrent lookups are coalesced, and fresh lookup starts are rate-limited per command to bound replay pressure.
- Slash callbacks fail closed when registration failed, startup was partial, or the callback token does not match the resolved command's registered token (a token valid for one command cannot reach upstream validation for a different command).
- Accepted callbacks are acknowledged with an ephemeral "Processing..." reply; the real answer arrives as a normal message.
</Accordion>
<Accordion title="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://<gateway-host>/api/channels/mattermost/command`; a GET should return `405 Method Not Allowed` from OpenClaw, not `404`.
</Accordion>
<Accordion title="Mattermost egress allowlist">
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`
</Accordion>
</AccordionGroup>
## Environment variables (default account)
Set these on the gateway host if you prefer env vars:
- `MATTERMOST_BOT_TOKEN=...`
- `MATTERMOST_URL=https://chat.example.com`
<Note>
Env vars apply only to the **default** account (`default`). Other accounts must use config values.
`MATTERMOST_URL` cannot be set from a workspace `.env`; see [Workspace .env files](/gateway/security).
</Note>
## Chat modes
Mattermost responds to DMs automatically. Channel behavior is controlled by `chatmode`:
<Tabs>
<Tab title="oncall (default)">
Respond only when @mentioned in channels.
</Tab>
<Tab title="onmessage">
Respond to every channel message.
</Tab>
<Tab title="onchar">
Respond when a message starts with a trigger prefix.
</Tab>
</Tabs>
Config example:
```json5
{
channels: {
mattermost: {
chatmode: "onchar",
oncharPrefixes: [">", "!"], // default
},
},
}
```
Notes:
- `onchar` still responds to explicit @mentions.
- `channels.mattermost.requireMention` is still honored, but `chatmode` is preferred. Per-channel `groups.<channelId>.requireMention` settings win over both.
- After the bot sends a visible reply in a channel thread, later messages in that same thread are answered without a new @mention or `onchar` prefix, so multi-turn thread conversations keep flowing. Participation is remembered for 7 days after the bot last replied in that thread and persists across gateway restarts. Threads the bot has only observed are unaffected; start a new top-level message to require an explicit mention again.
## Threading and sessions
Use `channels.mattermost.replyToMode` to control whether channel and group replies stay in the main channel or start a thread under the triggering post.
- `off` (default): only reply in a thread when the inbound post is already in one.
- `first`: for top-level channel/group posts, start a thread under that post and route the conversation to a thread-scoped session.
- `all` and `batched`: same behavior as `first` for Mattermost today, because once Mattermost has a thread root, follow-up chunks and media continue in that same thread.
- Direct messages ignore this setting and stay non-threaded.
```json5
{
channels: {
mattermost: {
replyToMode: "all",
},
},
}
```
Thread-scoped sessions use the triggering post id as the thread root.
## Access control (DMs)
- Default: `channels.mattermost.dmPolicy = "pairing"` (unknown senders get a pairing code). Other values: `allowlist`, `open`, `disabled`.
- Approve via:
- `openclaw pairing list mattermost`
- `openclaw pairing approve mattermost <CODE>`
- Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]` (the config schema enforces the wildcard).
- `channels.mattermost.allowFrom` accepts user ids (recommended) and `accessGroup:<name>` entries. See [Access groups](/channels/access-groups).
## Channels (groups)
- Default: `channels.mattermost.groupPolicy = "allowlist"` (mention-gated).
- Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs recommended).
- `channels.mattermost.groupAllowFrom` accepts `accessGroup:<name>` entries. See [Access groups](/channels/access-groups).
- Per-channel mention overrides live under `channels.mattermost.groups.<channelId>.requireMention` or `channels.mattermost.groups["*"].requireMention` for a default.
- `@username` matching is mutable and only enabled when `channels.mattermost.dangerouslyAllowNameMatching: true`.
- Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated).
- Resolution order: `channels.mattermost.groupPolicy`, then `channels.defaults.groupPolicy`, then `"allowlist"`.
- Runtime note: if the `channels.mattermost` section is completely missing, runtime fails closed to `groupPolicy="allowlist"` for group checks (even if `channels.defaults.groupPolicy` is set) and logs a one-time warning.
Example:
```json5
{
channels: {
mattermost: {
groupPolicy: "open",
groups: {
"*": { requireMention: true },
"team-channel-id": { requireMention: false },
},
},
},
}
```
## Targets for outbound delivery
Use these target formats with `openclaw message send` or cron/webhooks:
| Target | Delivers to |
| ----------------------------------- | ------------------------------------------------------------- |
| `channel:<id>` | Channel by id |
| `channel:<name>` or `#channel-name` | Channel by name, searched across the teams the bot belongs to |
| `user:<id>` or `mattermost:<id>` | DM with that user |
| `@username` | DM (username resolved via the Mattermost API) |
Outbound sends support at most one attachment per message; split multiple files into separate sends.
<Warning>
Bare opaque IDs (like `64ifufp...`) are **ambiguous** in Mattermost (user ID vs channel ID).
OpenClaw resolves them **user-first**:
- If the ID exists as a user (`GET /api/v4/users/<id>` succeeds), OpenClaw sends a **DM** by resolving the direct channel via `/api/v4/channels/direct`.
- Otherwise the ID is treated as a **channel ID**.
If you need deterministic behavior, always use the explicit prefixes (`user:<id>` / `channel:<id>`).
</Warning>
## DM channel retry
When OpenClaw sends to a Mattermost DM target and needs to resolve the direct channel first, it retries transient direct-channel creation failures by default.
Use `channels.mattermost.dmChannelRetry` to tune that behavior globally for the Mattermost plugin, or `channels.mattermost.accounts.<id>.dmChannelRetry` for one account. Defaults:
```json5
{
channels: {
mattermost: {
dmChannelRetry: {
maxRetries: 3,
initialDelayMs: 1000,
maxDelayMs: 10000,
timeoutMs: 30000,
},
},
},
}
```
Notes:
- This applies only to DM channel creation (`/api/v4/channels/direct`), not every Mattermost API call.
- Retries use exponential backoff with jitter and apply to transient failures such as rate limits, 5xx responses, and network or timeout errors.
- 4xx client errors other than `429` are treated as permanent and are not retried.
## Preview streaming
Mattermost streams thinking, tool activity, and partial reply text into a single **draft preview post** that finalizes in place when the final answer is safe to send. The preview updates on the same post id instead of spamming the channel with per-chunk messages. Media/error finals cancel pending preview edits and use normal delivery instead of flushing a throwaway preview post.
Preview streaming is **on by default** in `partial` mode. Configure via `channels.mattermost.streaming` (a mode string, boolean, or an object like `{ mode: "progress" }`):
```json5
{
channels: {
mattermost: {
streaming: "partial", // off | partial | block | progress
},
},
}
```
<AccordionGroup>
<Accordion title="Streaming modes">
- `partial` (default): one preview post that is edited as the reply grows, then finalized with the complete answer.
- `block` uses append-style draft chunks inside the preview post.
- `progress` shows a status preview while generating and only posts the final answer at completion.
- `off` disables preview streaming.
</Accordion>
<Accordion title="Streaming behavior notes">
- If the stream cannot be finalized in place (for example the post was deleted mid-stream), OpenClaw falls back to sending a fresh final post so the reply is never lost.
- Thinking-only payloads are suppressed from channel posts, including text that arrives as a `> Thinking` blockquote. Set `/reasoning on` to see thinking in other surfaces; the Mattermost final post keeps the answer only.
- See [Streaming](/concepts/streaming#preview-streaming-modes) for the channel-mapping matrix.
</Accordion>
</AccordionGroup>
## 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, subject to the same DM/group policy checks as messages.
Examples:
```text
message action=react channel=mattermost target=channel:<channelId> messageId=<postId> emoji=thumbsup
message action=react channel=mattermost target=channel:<channelId> messageId=<postId> emoji=thumbsup remove=true
```
Config:
- `channels.mattermost.actions.reactions`: enable/disable reaction actions (default true).
- Per-account override: `channels.mattermost.accounts.<id>.actions.reactions`.
## Interactive buttons (message tool)
Send messages with clickable buttons. When a user clicks a button, the agent receives the selection and can respond.
Buttons come from the semantic `presentation` payload (in normal agent replies and in `message action=send`). OpenClaw renders value buttons as Mattermost interactive buttons, keeps URL buttons visible in the message text, and downgrades select menus to readable text.
```text
message action=send channel=mattermost target=channel:<channelId> presentation={"blocks":[{"type":"buttons","buttons":[{"label":"Yes","value":"yes"},{"label":"No","value":"no"}]}]}
```
Presentation button fields:
<ParamField path="label" type="string" required>
Display label (alias: `text`).
</ParamField>
<ParamField path="value" type="string">
Value sent back on click, used as the action ID (aliases: `callback_data`, `callbackData`). Required for a clickable button unless `url` is set.
</ParamField>
<ParamField path="url" type="string">
Link button; rendered as `label: url` text in the message body instead of an interactive button.
</ParamField>
<ParamField path="style" type='"primary" | "secondary" | "success" | "danger"'>
Button style. Mattermost applies default styling to values it does not support.
</ParamField>
To advertise button support in the agent system prompt, add `inlineButtons` to the channel capabilities:
```json5
{
channels: {
mattermost: {
capabilities: ["inlineButtons"],
},
},
}
```
When a user clicks a button:
<Steps>
<Step title="Access check">
The clicker must pass the same DM/group policy checks as a message sender; unauthorized clicks get an ephemeral notice and are ignored.
</Step>
<Step title="Buttons replaced with confirmation">
All buttons are replaced with a confirmation line (e.g., "✓ **Yes** selected by @user").
</Step>
<Step title="Agent receives the selection">
The agent receives the selection as an inbound message (plus a system event) and responds.
</Step>
</Steps>
<AccordionGroup>
<Accordion title="Implementation notes">
- Button callbacks use HMAC-SHA256 verification (automatic, no config needed).
- The whole attachment block is replaced on click, so all buttons are removed together - partial removal is not possible.
- Action IDs containing hyphens or underscores are sanitized automatically (Mattermost routing limitation).
- Clicks whose `action_id` does not match an action on the original post are rejected with `403` ("Unknown action").
</Accordion>
<Accordion title="Config and reachability">
- `channels.mattermost.capabilities`: array of capability strings. Add `"inlineButtons"` to enable the buttons tool description in the agent system prompt.
- `channels.mattermost.interactions.callbackBaseUrl`: optional external base URL for button callbacks (for example `https://gateway.example.com`). Use this when Mattermost cannot reach the gateway at its bind host directly.
- In multi-account setups, you can also set the same field under `channels.mattermost.accounts.<id>.interactions.callbackBaseUrl`.
- If `interactions.callbackBaseUrl` is omitted, OpenClaw derives the callback URL from `gateway.customBindHost` + `gateway.port` (default 18789), then falls back to `http://localhost:<port>`. The callback path is `/mattermost/interactions/<accountId>`.
- Reachability rule: the button callback URL must be reachable from the Mattermost server. `localhost` only works when Mattermost and OpenClaw run on the same host/network namespace.
- `channels.mattermost.interactions.allowedSourceIps`: source-IP allowlist for button callbacks. Without it, only loopback sources (`127.0.0.1`, `::1`) are accepted, so a remote Mattermost server must be allowlisted here or its clicks are rejected with `403`. Behind a reverse proxy, also set `gateway.trustedProxies` so the real client IP is derived from forwarded headers.
- If your callback target is private/tailnet/internal, add its host/domain to Mattermost `ServiceSettings.AllowedUntrustedInternalConnections`.
</Accordion>
</AccordionGroup>
### Direct API integration (external scripts)
External scripts and webhooks can post buttons directly via the Mattermost REST API instead of going through the agent's `message` tool. Use `buildButtonAttachments()` from the plugin when possible; if posting raw JSON, follow these rules:
**Payload structure:**
```json5
{
channel_id: "<channelId>",
message: "Choose an option:",
props: {
attachments: [
{
actions: [
{
id: "mybutton01", // alphanumeric only - see below
type: "button", // required, or clicks are silently ignored
name: "Approve", // display label
style: "primary", // optional: "default", "primary", "danger"
integration: {
url: "https://gateway.example.com/mattermost/interactions/default",
context: {
action_id: "mybutton01", // must match button id
action: "approve",
// ... any custom fields ...
_token: "<hmac>", // see HMAC section below
},
},
},
],
},
],
},
}
```
<Warning>
**Critical rules**
1. Attachments go in `props.attachments`, not top-level `attachments` (silently ignored).
2. Every action needs `type: "button"` - without it, clicks are swallowed silently.
3. Every action needs an `id` field - Mattermost ignores actions without IDs.
4. Action `id` must be **alphanumeric only** (`[a-zA-Z0-9]`). Hyphens and underscores break Mattermost's server-side action routing (returns 404). Strip them before use.
5. `context.action_id` must match the button's `id`; the gateway rejects clicks whose `action_id` does not exist on the post.
6. `context.action_id` is required - the interaction handler returns 400 without it.
7. The callback source IP must be allowed (see `interactions.allowedSourceIps` above).
</Warning>
**HMAC token generation**
The gateway verifies button clicks with HMAC-SHA256. External scripts must generate tokens that match the gateway's verification logic:
<Steps>
<Step title="Derive the secret from the bot token">
`HMAC-SHA256(key="openclaw-mattermost-interactions", data=botToken)`, hex-encoded.
</Step>
<Step title="Build the context object">
Build the context object with all fields **except** `_token`.
</Step>
<Step title="Serialize with sorted keys">
Serialize with **recursively sorted keys** and **no spaces** (the gateway canonicalizes nested objects too and produces compact JSON).
</Step>
<Step title="Sign the payload">
`HMAC-SHA256(key=secret, data=serializedContext)`
</Step>
<Step title="Add the token">
Add the resulting hex digest as `_token` in the context.
</Step>
</Steps>
Python example:
```python
import hmac, hashlib, json
secret = hmac.new(
b"openclaw-mattermost-interactions",
bot_token.encode(), hashlib.sha256
).hexdigest()
ctx = {"action_id": "mybutton01", "action": "approve"}
payload = json.dumps(ctx, sort_keys=True, separators=(",", ":"))
token = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
context = {**ctx, "_token": token}
```
<AccordionGroup>
<Accordion title="Common HMAC pitfalls">
- Python's `json.dumps` adds spaces by default (`{"key": "val"}`). Use `separators=(",", ":")` to match JavaScript's compact output (`{"key":"val"}`).
- Always sign **all** context fields (minus `_token`). The gateway strips `_token` then signs everything remaining. Signing a subset causes silent verification failure.
- Use `sort_keys=True` - the gateway sorts keys before signing, and Mattermost may reorder context fields when storing the payload.
- Derive the secret from the bot token (deterministic), not random bytes. The secret must be the same across the process that creates buttons and the gateway that verifies.
</Accordion>
</AccordionGroup>
## Directory adapter
The Mattermost plugin includes a directory adapter that resolves channel and user names via the Mattermost API. This enables `#channel-name` and `@username` targets in `openclaw message send` and cron/webhook deliveries.
No configuration is needed - the adapter uses the bot token from the account config.
## Multi-account
Mattermost supports multiple accounts under `channels.mattermost.accounts`:
```json5
{
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" },
},
},
},
}
```
Account values override top-level fields; `channels.mattermost.defaultAccount` picks which account is used when none is specified.
## Troubleshooting
<AccordionGroup>
<Accordion title="No replies in channels">
Ensure the bot is in the channel and mention it (oncall), use a trigger prefix (onchar), or set `chatmode: "onmessage"`.
</Accordion>
<Accordion title="Auth or multi-account errors">
- Check the bot token, base URL, and whether the account is enabled.
- Multi-account issues: env vars only apply to the `default` account.
- Private/LAN Mattermost hosts need `network.dangerouslyAllowPrivateNetwork: true` (the SSRF guard blocks private IPs by default).
</Accordion>
<Accordion title="Native slash commands fail">
- `Unauthorized: invalid command token.`: OpenClaw did not accept the callback token. Typical causes:
- slash command registration failed or only partially completed at startup
- the callback is hitting the wrong gateway/account
- Mattermost still has old commands pointing at a previous callback target
- the gateway restarted without reactivating slash commands
- If native slash commands stop working, check logs for `mattermost: failed to register slash commands` or `mattermost: native slash commands enabled but no commands could be registered`.
- If `callbackUrl` is omitted and logs warn that the callback resolved to a loopback URL like `http://localhost:18789/...`, that URL is probably only reachable when Mattermost runs on the same host/network namespace as OpenClaw. Set an explicit externally reachable `commands.callbackUrl` instead.
</Accordion>
<Accordion title="Buttons issues">
- Buttons appear as white boxes or not at all: the button data is malformed. Each presentation button needs a `label` and a `value` (buttons missing either are dropped).
- Buttons render but clicks do nothing: verify the gateway is reachable from the Mattermost server, the Mattermost server IP is included in `channels.mattermost.interactions.allowedSourceIps` (only loopback is accepted without it), and `ServiceSettings.AllowedUntrustedInternalConnections` includes the callback host for private targets.
- Buttons return 404 on click: the button `id` likely contains hyphens or underscores. Mattermost's action router breaks on non-alphanumeric IDs. Use `[a-zA-Z0-9]` only.
- Gateway logs `rejected callback source`: the click came from an IP outside `interactions.allowedSourceIps`. Allowlist the Mattermost server or your ingress, and set `gateway.trustedProxies` behind a reverse proxy.
- Gateway logs `invalid _token`: HMAC mismatch. Check that you sign all context fields (not a subset), use sorted keys, and use compact JSON (no spaces). See the HMAC section above.
- Gateway logs `missing _token in context`: the `_token` field is not in the button's context. Ensure it is included when building the integration payload.
- Gateway rejects the click with `Unknown action`: `context.action_id` does not match any action `id` on the post. Set both to the same sanitized value.
- Agent does not offer buttons: add `capabilities: ["inlineButtons"]` to the Mattermost channel config.
</Accordion>
</AccordionGroup>
## Related
- [Channel Routing](/channels/channel-routing) - session routing for messages
- [Channels Overview](/channels) - all supported channels
- [Groups](/channels/groups) - group chat behavior and mention gating
- [Pairing](/channels/pairing) - DM authentication and pairing flow
- [Security](/gateway/security) - access model and hardening

1066
docs/channels/msteams.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
---
summary: "Nextcloud Talk support status, capabilities, and configuration"
read_when:
- Working on Nextcloud Talk channel features
title: "Nextcloud Talk"
---
Nextcloud Talk is a downloadable channel plugin (`@openclaw/nextcloud-talk`) that connects OpenClaw to a self-hosted Nextcloud instance through a Talk webhook bot. Direct messages, rooms, reactions, and markdown messages are supported; media goes out as URLs.
## Install
```bash
openclaw plugins install @openclaw/nextcloud-talk
```
Use the bare package spec to follow the current official release tag. Pin an exact version only when you need a reproducible install.
From a local checkout (dev workflows):
```bash
openclaw plugins install ./path/to/local/nextcloud-talk-plugin
```
Restart the gateway after installing. Details: [Plugins](/tools/plugin)
## Quick setup (beginner)
1. Install the plugin (above).
2. On your Nextcloud server, create a bot:
```bash
./occ talk:bot:install "OpenClaw" "<shared-secret>" "<webhook-url>" --feature webhook --feature response --feature reaction
```
Keep `--feature response`: without it, outbound replies fail with 401. Repair an existing bot with `./occ talk:bot:state --feature webhook --feature response --feature reaction <botId> 1`.
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)
CLI setup (`--url`/`--token` are aliases for the explicit fields; `nc-talk` and `nc` work as channel aliases):
```bash
openclaw channels add --channel nextcloud-talk \
--url https://cloud.example.com \
--token "<shared-secret>"
```
Equivalent explicit fields:
```bash
openclaw channels add --channel nextcloud-talk \
--base-url https://cloud.example.com \
--secret "<shared-secret>"
```
File-backed secret:
```bash
openclaw channels add --channel nextcloud-talk \
--base-url https://cloud.example.com \
--secret-file /path/to/nextcloud-talk-secret
```
5. Restart the gateway (or finish setup).
Minimal config:
```json5
{
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.
- The webhook URL must be reachable from the Nextcloud server; set `webhookPublicUrl` when the gateway sits behind a proxy. Webhook requests are HMAC-SHA256 signed with the bot secret; invalid signatures are rejected and rate limited.
- Media uploads are not supported by the bot API; outbound media is appended as an `Attachment: <url>` line.
- The webhook payload does not distinguish DMs from rooms; set `apiUser` + `apiPassword` to enable room-type lookups (cached about 5 minutes). Without them, every conversation is treated as a room.
- Outbound requests go through the SSRF guard. For a Nextcloud host on a trusted private/internal network, opt in with `channels.nextcloud-talk.network.dangerouslyAllowPrivateNetwork: true`.
- With `apiUser`/`apiPassword` and `webhookPublicUrl` set, `openclaw channels status` probes the bot and warns when the `response` feature is missing.
## 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 <CODE>`
- Public DMs: `channels.nextcloud-talk.dmPolicy="open"` plus `channels.nextcloud-talk.allowFrom=["*"]`.
- `allowFrom` matches Nextcloud user IDs only (lowercased); display names are ignored.
## Rooms (groups)
- Default: `channels.nextcloud-talk.groupPolicy = "allowlist"` (mention-gated).
- Allowlist rooms with `channels.nextcloud-talk.rooms`, keyed by room token; `"*"` sets a wildcard default:
```json5
{
channels: {
"nextcloud-talk": {
rooms: {
"room-token": { requireMention: true },
},
},
},
}
```
- Per-room keys: `requireMention` (default true), `enabled` (false disables the room), `allowFrom` (per-room sender allowlist), `tools` (allow/deny tool overrides), `skills` (limit loaded skills), `systemPrompt`.
- 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 (string or secret reference).
- `channels.nextcloud-talk.botSecretFile`: regular-file secret path. Symlinks are rejected.
- `channels.nextcloud-talk.apiUser`: API user for room lookups (DM detection) and the status probe.
- `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` (default: pairing). `open` requires `allowFrom=["*"]`.
- `channels.nextcloud-talk.allowFrom`: DM allowlist (user IDs).
- `channels.nextcloud-talk.groupPolicy`: `allowlist | open | disabled` (default: allowlist).
- `channels.nextcloud-talk.groupAllowFrom`: room sender allowlist (user IDs); falls back to `allowFrom` when unset.
- `channels.nextcloud-talk.rooms`: per-room settings and allowlist (see above).
- Static sender access groups can be referenced from `allowFrom` and `groupAllowFrom` with `accessGroup:<name>`.
- `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 keyed by user ID (`historyLimit`).
- `channels.nextcloud-talk.textChunkLimit`: outbound text chunk size in chars (default: 4000).
- `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.responsePrefix`: outbound reply prefix.
- `channels.nextcloud-talk.markdown.tables`: markdown table rendering mode (`off | bullets | code | block`).
- `channels.nextcloud-talk.mediaMaxMb`: inbound media cap (MB).
- `channels.nextcloud-talk.network.dangerouslyAllowPrivateNetwork`: allow private/internal Nextcloud hosts past the SSRF guard.
- `channels.nextcloud-talk.accounts.<id>`: per-account overrides (same keys); `defaultAccount` picks the default. Env vars `NEXTCLOUD_TALK_BOT_SECRET` / `NEXTCLOUD_TALK_API_PASSWORD` apply to the default account only.
## Related
- [Channels Overview](/channels) — all supported channels
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) — session routing for messages
- [Security](/gateway/security) — access model and hardening

241
docs/channels/nostr.md Normal file
View File

@@ -0,0 +1,241 @@
---
summary: "Nostr DM channel via NIP-04 encrypted messages"
read_when:
- You want OpenClaw to receive DMs via Nostr
- You're setting up decentralized messaging
title: "Nostr"
---
Nostr is a downloadable channel plugin (`@openclaw/nostr`) that lets OpenClaw receive and answer NIP-04 encrypted direct messages over Nostr relays. One account per gateway; DMs only.
## Install
```bash
openclaw plugins install @openclaw/nostr
```
Use the bare package spec to follow the current official release tag. Pin an exact version only when you need a reproducible install.
From a local checkout (dev workflows):
```bash
openclaw plugins install --link <path-to-local-nostr-plugin>
```
Restart the gateway after installing or enabling plugins. Onboarding (`openclaw onboard`) and `openclaw channels add` surface Nostr from the shared channel catalog once the plugin is installed.
### Non-interactive setup
```bash
openclaw channels add --channel nostr --private-key "$NOSTR_PRIVATE_KEY"
openclaw channels add --channel nostr --private-key "$NOSTR_PRIVATE_KEY" --relay-urls "wss://relay.damus.io,wss://relay.primal.net"
```
Use `--use-env` to keep `NOSTR_PRIVATE_KEY` in the environment instead of storing the key in config (default account only).
## Quick setup
1. Generate a Nostr keypair (if needed):
```bash
# Using nak
nak key generate
```
2. Add to config:
```json5
{
channels: {
nostr: {
privateKey: "${NOSTR_PRIVATE_KEY}",
},
},
}
```
3. Export the key:
```bash
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; secret refs allowed |
| `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:
```json5
{
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.
Enforcement notes:
- Inbound event signatures are verified before sender policy and NIP-04 decryption, so forged events are rejected early.
- Pairing replies are sent without decrypting or processing the original DM body.
- Inbound DMs are rate-limited (globally and per sender) and oversized payloads are dropped before decrypt.
### Allowlist example
```json5
{
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`.
```json5
{
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
# Start strfry
docker run -p 7777:7777 ghcr.io/hoytech/strfry
```
```json5
{
channels: {
nostr: {
privateKey: "${NOSTR_PRIVATE_KEY}",
relays: ["ws://localhost:7777"],
},
},
}
```
### Manual test
1. Note the bot pubkey from gateway logs or `openclaw channels status` (hex; convert to npub in your client if needed).
2. Open a Nostr client (Amethyst, Damus, 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.
- Signatures are verified before sender policy, and sender policy is enforced before decrypt, so forged events are rejected early and unknown senders cannot force full crypto work.
## Limitations (MVP)
- Direct messages only (no group chats).
- No media attachments.
- NIP-04 only (NIP-17 gift-wrap planned).
## Related
- [Channels Overview](/channels) — all supported channels
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) — session routing for messages
- [Security](/gateway/security) — access model and hardening

237
docs/channels/pairing.md Normal file
View File

@@ -0,0 +1,237 @@
---
summary: "Pairing overview: approve who can DM you + which nodes can join"
read_when:
- Setting up DM access control
- Pairing a new iOS/Android node
- Reviewing OpenClaw security posture
title: "Pairing"
---
"Pairing" is OpenClaw's explicit access 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)
`dmPolicy: "open"` is public only when the effective DM allowlist includes `"*"`.
Setup and validation require that wildcard for public-open configs. If existing
state contains `open` with concrete `allowFrom` entries, runtime still admits
only those senders, and pairing-store approvals do not widen `open` access.
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 account**; additional requests are ignored until one expires or is approved.
### Approve a sender
```bash
openclaw pairing list telegram
openclaw pairing approve telegram <CODE>
```
Add `--notify` to the approve command to tell the requester on the same channel. Multi-account channels take `--account <id>`.
If no command owner is configured yet, approving a DM pairing code also bootstraps
`commands.ownerAllowFrom` to the approved sender, such as `telegram:123456789`.
That gives first-time setups an explicit owner for privileged commands and exec
approval prompts. After an owner exists, later pairing approvals only grant DM
access; they do not add more owners.
Supported channels (any installed channel plugin that declares pairing; external plugins such as `openclaw-weixin` can add more): `discord`, `feishu`, `googlechat`, `imessage`, `irc`, `line`, `matrix`, `mattermost`, `msteams`, `nextcloud-talk`, `nostr`, `signal`, `slack`, `sms`, `synology-chat`, `telegram`, `twitch`, `whatsapp`, `zalo`, `zalouser`.
### Reusable sender groups
Use top-level `accessGroups` when the same trusted sender set should apply to
multiple message channels or to both DM and group allowlists.
Static groups use `type: "message.senders"` and are referenced with
`accessGroup:<name>` from channel allowlists:
```json5
{
accessGroups: {
operators: {
type: "message.senders",
members: {
discord: ["discord:123456789012345678"],
telegram: ["987654321"],
whatsapp: ["+15551234567"],
},
},
},
channels: {
telegram: { dmPolicy: "allowlist", allowFrom: ["accessGroup:operators"] },
whatsapp: { groupPolicy: "allowlist", groupAllowFrom: ["accessGroup:operators"] },
},
}
```
Access groups are documented in detail here: [Access groups](/channels/access-groups)
### Where the state lives
Stored under `~/.openclaw/credentials/`:
- Pending requests: `<channel>-pairing.json`
- Approved allowlist store: `<channel>-<accountId>-allowFrom.json` (approvals for the
default account use `<channel>-default-allowFrom.json`)
Account scoping behavior:
- Non-default accounts read/write only their scoped allowlist file.
- The default account also keeps honoring a legacy unscoped `<channel>-allowFrom.json`
file from older installs; entries from both files are merged on read.
Treat these as sensitive (they gate access to your assistant).
<Note>
The pairing allowlist store is for DM access. Group authorization is separate.
Approving a DM pairing code does not automatically allow that sender to run group
commands or control the bot in groups. First-owner bootstrap is separate config
state in `commands.ownerAllowFrom`, and group chat delivery still follows the
channel's group allowlists (for example `groupAllowFrom`, `groups`, or per-group
or per-topic overrides depending on the channel).
</Note>
## 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 from the Control UI (recommended)
Use an already connected Control UI session with `operator.admin` access:
1. Open the Control UI and select **Nodes**.
2. In **Devices**, click **Pair mobile device**.
3. On your phone, open the OpenClaw app → **Settings****Gateway**.
4. Scan the QR code or paste the setup code, then connect.
Official OpenClaw iOS and Android apps are approved automatically when their
setup-code metadata matches. If **Devices** shows a pending request (for
example, for a non-official client or mismatched metadata), review its role and
scopes before approving it.
The button is disabled when the current Control UI session does not have
administrator access. Use the CLI approval flow below from the Gateway host in
that case.
### Pair via Telegram
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. Scan the QR code (`/pair qr`) or paste the setup code and connect.
5. The official mobile app connects automatically. If `/pair pending` shows a
request, review its role and scopes before approving it.
The setup code is a base64-encoded JSON payload that contains:
- `url`: the Gateway WebSocket URL (`ws://...` or `wss://...`)
- `bootstrapToken`: a single-use bootstrap token for the initial pairing handshake (expires after 10 minutes; `expiresAtMs` is included in the payload)
Run `/pair cleanup` to invalidate unused setup codes once pairing finishes.
That bootstrap token carries the built-in pairing bootstrap profile:
- the built-in setup profile allows the fresh QR/setup-code baseline only:
`node` plus a bounded `operator` handoff
- the handed-off `node` token stays `scopes: []`
- the handed-off `operator` token is limited to `operator.approvals`,
`operator.read`, `operator.talk.secrets`, and `operator.write`
- `operator.admin` is not granted by QR/setup-code bootstrap; it requires a
separate approved operator pairing or token flow
- later token rotation/revocation remains bounded by both the device's approved
role contract and the caller session's operator scopes
Treat the setup code like a password while it is valid.
For Tailscale, public, or other remote mobile pairing, use Tailscale Serve/Funnel
or another `wss://` Gateway URL. Plaintext `ws://` setup codes are accepted only
for loopback, private LAN addresses, `.local` Bonjour hosts, and the Android
emulator host. Tailnet CGNAT addresses, `.ts.net` names, and public hosts still
fail closed before QR/setup-code issuance.
### Approve a node device
```bash
openclaw devices list
openclaw devices approve <requestId>
openclaw devices reject <requestId>
```
When an explicit approval is denied because the approving paired-device session
was opened with pairing-only scope, the CLI retries the same request with
`operator.admin`. This lets an existing admin-capable paired device recover a new
Control UI/browser pairing without editing `devices/paired.json` by hand. The
Gateway still validates the retried connection; tokens that cannot authenticate
with `operator.admin` remain blocked.
If the same device retries with different auth details (for example different
role/scopes/public key), the previous pending request is superseded and a new
`requestId` is created.
<Note>
An already paired device does not get broader access silently. If it reconnects asking for more scopes or a broader role, OpenClaw keeps the existing approval as-is and creates a fresh pending upgrade request. Use `openclaw devices list` to compare the currently approved access with the newly requested access before you approve.
</Note>
### Optional trusted-CIDR node auto-approve
Device pairing remains manual by default. For tightly controlled node networks,
you can opt in to first-time node auto-approval with explicit CIDRs or exact IPs:
```json5
{
gateway: {
nodes: {
pairing: {
autoApproveCidrs: ["192.168.1.0/24"],
},
},
},
}
```
This only applies to fresh `role: node` pairing requests with no requested
scopes. Operator, browser, Control UI, and WebChat clients still require manual
approval. Role, scope, metadata, and public-key changes still require manual
approval.
### Node pairing state storage
Stored under `~/.openclaw/devices/`:
- `pending.json` (short-lived; pending requests expire after 5 minutes)
- `paired.json` (paired devices + tokens)
### Notes
- The legacy `node.pair.*` API (CLI: `openclaw nodes pending|approve|reject|remove|rename`) is a
separate gateway-owned pairing store. WS nodes still require device pairing.
- The pairing record is the durable source of truth for approved roles. Active
device tokens stay bounded to that approved role set; a stray token entry
outside the approved roles does not create new access.
## 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)
- iMessage: [iMessage](/channels/imessage)
- Discord: [Discord](/channels/discord)
- Slack: [Slack](/channels/slack)

View File

@@ -0,0 +1,97 @@
---
summary: "Synthetic Slack-class channel plugin for deterministic OpenClaw QA scenarios"
title: "QA channel"
read_when:
- You are wiring the synthetic QA transport into a local or CI test run
- You need the bundled qa-channel config surface
- You are iterating on end-to-end QA automation
---
`qa-channel` is a repo-local synthetic message transport for automated OpenClaw QA (`extensions/qa-channel`, private package, excluded from packaged installs). It is not a production channel - it exists to exercise the same channel plugin boundary used by real transports while keeping state deterministic and fully inspectable.
## What it does
- Slack-class target grammar:
- `dm:<user>`
- `channel:<room>`
- `group:<room>`
- `thread:<room>/<thread>`
- Shared `channel:` and `group:` conversations are surfaced to agents as group/channel room turns, so they exercise the same visible-reply and message-tool routing policy used by Discord, Slack, Telegram, and similar transports.
- HTTP-backed synthetic bus for inbound message injection, outbound transcript capture, thread creation, reactions, edits, deletes, and search/read actions.
- Host-side self-check runner that writes a Markdown report to `.artifacts/qa-e2e/`.
## Config
```json
{
"channels": {
"qa-channel": {
"baseUrl": "http://127.0.0.1:43123",
"botUserId": "openclaw",
"botDisplayName": "OpenClaw QA",
"allowFrom": ["*"],
"pollTimeoutMs": 1000
}
}
}
```
Account keys:
- `enabled` - master toggle for this account.
- `name` - optional display label.
- `baseUrl` - synthetic bus URL. The account counts as configured once this is set.
- `botUserId` - synthetic bot user id used in target grammar (default: `openclaw`).
- `botDisplayName` - display name for outbound messages (default: `OpenClaw QA`).
- `pollTimeoutMs` - long-poll wait window. Integer between 100 and 30000 (default: 1000).
- `allowFrom` - sender allowlist (user ids or `"*"`; default: `["*"]`). DMs are
always `open` policy; allowlisted group policy also uses these synthetic
sender ids.
- `groupPolicy` - shared-room policy: `"open"` (default), `"allowlist"`, or
`"disabled"`.
- `groupAllowFrom` - optional shared-room sender allowlist. When omitted under
`"allowlist"`, QA Channel falls back to `allowFrom`.
- `groups.<room>.requireMention` - require a bot mention before replying in a
specific group/channel room (default: false). `groups."*"` sets the default;
per-room `tools` / `toolsBySender` set tool policy overrides.
- `defaultTo` - fallback target when none is supplied.
- `actions.messages` / `actions.reactions` / `actions.search` / `actions.threads` - per-action tool gating.
Multi-account keys at the top level:
- `accounts` - record of named per-account overrides keyed by account id.
- `defaultAccount` - preferred account id when multiple are configured.
## Runners
Host-side self-check (writes a Markdown report under `.artifacts/qa-e2e/`):
```bash
pnpm qa:e2e
```
This routes through `qa-lab`, starts the in-repo QA bus, boots the `qa-channel` runtime slice, and runs a deterministic self-check.
Full repo-backed scenario suite:
```bash
pnpm openclaw qa suite
```
Runs scenarios in parallel against the QA gateway lane. See [QA overview](/concepts/qa-e2e-automation) for scenarios, profiles, and provider modes.
Docker-backed QA site (gateway + QA Lab debugger UI in one stack):
```bash
pnpm qa:lab:up
```
Builds the QA site, starts the Docker-backed gateway + QA Lab stack, and prints the QA Lab URL. From there you can pick scenarios, choose the model lane, launch individual runs, and watch results live. The QA Lab debugger is separate from the shipped Control UI bundle.
## Related
- [QA overview](/concepts/qa-e2e-automation) - overall stack, transport adapters, scenario authoring
- [Matrix QA](/concepts/qa-matrix) - example live-transport runner that drives a real channel
- [Pairing](/channels/pairing)
- [Groups](/channels/groups)
- [Channels overview](/channels)

357
docs/channels/qqbot.md Normal file
View File

@@ -0,0 +1,357 @@
---
summary: "QQ Bot setup, config, and usage"
read_when:
- You want to connect OpenClaw to QQ
- You need QQ Bot credential setup
- You want QQ Bot group or private chat support
title: QQ bot
---
QQ Bot connects to OpenClaw via the official QQ Bot API (WebSocket gateway).
C2C private chat and group `@`-mentions are the primary chat types, with rich
media (images, voice, video, files). Guild channel messages are supported for
text and remote-URL images only; voice, video, file uploads, and local/Base64
images are not available in guild channels. Reactions and threads are not
supported anywhere.
Status: official downloadable plugin.
## Install
```bash
openclaw plugins install @openclaw/qqbot
```
## Setup
1. Go to the [QQ Open Platform](https://q.qq.com/) and scan the QR code with your
phone QQ to register / log in.
2. Click **Create Bot** to create a new QQ bot.
3. Find **AppID** and **AppSecret** on the bot's settings page and copy them.
<Note>
AppSecret is not stored in plaintext. If you leave the page without saving it, you'll have to regenerate a new one.
</Note>
4. Add the channel:
```bash
openclaw channels add --channel qqbot --token "AppID:AppSecret"
```
5. Restart the Gateway.
Interactive setup:
```bash
openclaw channels add
```
The wizard also offers QR-code binding as an alternative to typing AppID/AppSecret
manually: scan the code with the phone app tied to the target QQ Bot to complete
binding. OpenClaw persists the returned credentials under the account's config
scope.
## Configure
Minimal config:
```json5
{
channels: {
qqbot: {
enabled: true,
appId: "YOUR_APP_ID",
clientSecret: "YOUR_APP_SECRET",
},
},
}
```
Default-account env vars (top-level account only):
- `QQBOT_APP_ID`
- `QQBOT_CLIENT_SECRET`
File-backed AppSecret:
```json5
{
channels: {
qqbot: {
enabled: true,
appId: "YOUR_APP_ID",
clientSecretFile: "/path/to/qqbot-secret.txt",
},
},
}
```
Env SecretRef AppSecret:
```json5
{
channels: {
qqbot: {
enabled: true,
appId: "YOUR_APP_ID",
clientSecret: { source: "env", provider: "default", id: "QQBOT_CLIENT_SECRET" },
},
},
}
```
Notes:
- `openclaw channels add --channel qqbot --token-file ...` sets the AppSecret
only; `appId` must already be set in config or `QQBOT_APP_ID`.
- `clientSecret` accepts a plaintext string, a file path (`clientSecretFile`),
or a structured SecretRef object.
- Legacy `secretref:...` / `secretref-env:...` marker strings are rejected for
`clientSecret`; use a structured SecretRef object instead.
### Access policy
- `allowFrom` / `groupAllowFrom` gate who can chat with the bot in C2C /
group contexts. `dmPolicy` / `groupPolicy` (`open` | `allowlist` | `disabled`)
control the enforcement mode. `dmPolicy` defaults to `allowlist` once
`allowFrom` has a concrete (non-wildcard) entry, otherwise `open`.
`groupPolicy` defaults to `allowlist` once either `groupAllowFrom` or
`allowFrom` has a concrete entry, otherwise `open`.
- "Auth: allowlist" slash commands require an explicit non-wildcard entry in
`allowFrom` (or `groupAllowFrom` for group invocations) regardless of
`dmPolicy` / `groupPolicy` — see [Slash commands](#slash-commands).
### Multi-account setup
Run multiple QQ bots under a single OpenClaw instance:
```json5
{
channels: {
qqbot: {
enabled: true,
appId: "111111111",
clientSecret: "secret-of-bot-1",
accounts: {
bot2: {
enabled: true,
appId: "222222222",
clientSecret: "secret-of-bot-2",
},
},
},
},
}
```
Each account owns an isolated WebSocket connection, API client, and token
cache, keyed by `appId`. Log lines are tagged with the owning account id so
diagnostics stay separable when you run several bots under one Gateway.
Add a second bot via CLI:
```bash
openclaw channels add --channel qqbot --account bot2 --token "222222222:secret-of-bot-2"
```
### Group chats
Group support uses QQ group OpenIDs, not display names. Add the bot to a
group, then mention it or configure the group to run without a mention.
```json5
{
channels: {
qqbot: {
groupPolicy: "allowlist",
groupAllowFrom: ["member_openid"],
groups: {
"*": {
requireMention: true,
commandLevel: "all",
historyLimit: 50,
tools: { deny: ["exec", "read", "write"] },
},
GROUP_OPENID: {
name: "Release room",
requireMention: false,
ignoreOtherMentions: true,
commandLevel: "safety",
historyLimit: 20,
prompt: "Keep replies short and operational.",
},
},
},
},
}
```
`groups["*"]` sets defaults for every group; a concrete `groups.GROUP_OPENID`
entry overrides those defaults for one group. Group settings:
| Field | Default | Description |
| --------------------- | ---------------- | -------------------------------------------------------------------------------------------------- |
| `requireMention` | `true` | Require an `@`-mention before the bot replies. |
| `commandLevel` | `all` | Which built-in slash commands can run in the group (see below). |
| `ignoreOtherMentions` | `false` | Drop messages that mention someone else but not the bot. |
| `historyLimit` | `50` | Recent non-mention messages kept as context for the next mentioned turn. `0` disables history. |
| `tools` | — | Allow/deny tools for the whole group. |
| `toolsBySender` | — | Per-sender tool overrides; see [Groups](/channels/groups#groupchannel-tool-restrictions-optional). |
| `name` | openid prefix | Friendly label used in logs and group context. |
| `prompt` | built-in default | Per-group behavior prompt appended to the agent context. |
`commandLevel` accepts:
| Level | Behavior |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `all` | Existing built-in commands stay available. Some stay hidden from menus but authorized users can still run them in the group. |
| `safety` | `/help`, `/btw`, `/stop` stay visible in the group; sensitive commands (`/config`, `/tools`, `/bash`, etc.) must be run in private chat. |
| `strict` | Only group-session controls needed for strict operation are allowed. `/stop` still works so an authorized sender can interrupt an active run. |
Old QQBot `toolPolicy` entries are retired. Run `openclaw doctor --fix` to migrate them to `tools`.
Activation modes are `mention` and `always`. `requireMention: true` maps to
`mention`; `requireMention: false` maps to `always`. A session-level activation
override, when present, wins over config.
The inbound queue is per peer. Group peers get a larger queue cap (50 vs. 20
for direct peers), evict bot-authored messages before human ones when full,
and merge bursts of normal group messages into one attributed turn. Slash
commands run one by one, independent of any merge batch.
### Voice (STT / TTS)
STT and TTS support two-level configuration with priority fallback:
| Setting | Plugin-specific | Framework fallback |
| ------- | -------------------------------------------------------- | ----------------------------- |
| STT | `channels.qqbot.stt` | `tools.media.audio.models[0]` |
| TTS | `channels.qqbot.tts`, `channels.qqbot.accounts.<id>.tts` | `messages.tts` |
```json5
{
channels: {
qqbot: {
stt: {
provider: "your-provider",
model: "your-stt-model",
},
tts: {
provider: "your-provider",
model: "your-tts-model",
voice: "your-voice",
},
accounts: {
"qq-main": {
tts: {
providers: {
openai: { voice: "shimmer" },
},
},
},
},
},
},
}
```
Set `enabled: false` on either to disable. Account-level TTS overrides use the
same shape as `messages.tts` and deep-merge over channel/global TTS config.
Inbound QQ voice attachments are exposed to agents as audio media metadata
while keeping raw voice files out of generic `MediaPaths`. `[[audio_as_voice]]`
in a plain-text reply synthesizes TTS and sends a native QQ voice message when
TTS is configured.
Outbound audio upload/transcode behavior can also be tuned with
`channels.qqbot.audioFormatPolicy`:
- `sttDirectFormats`
- `uploadDirectFormats`
- `transcodeEnabled`
## Target formats
| Format | Description |
| -------------------------- | ------------------ |
| `qqbot:c2c:OPENID` | Private chat (C2C) |
| `qqbot:group:GROUP_OPENID` | Group chat |
| `qqbot:channel:CHANNEL_ID` | Guild channel |
<Note>
Each bot has its own set of user OpenIDs. An OpenID received by Bot A **cannot** be used to send messages via Bot B.
</Note>
## Slash commands
Built-in commands intercepted before the AI queue:
| Command | Auth | Scope | Description |
| -------------------- | --------- | ------------ | ------------------------------------------------------------------------------ |
| `/bot-ping` | — | any | Latency test |
| `/bot-help` | — | any | List all commands |
| `/bot-me` | — | private only | Show the sender's QQ user ID (openid) for `allowFrom` / `groupAllowFrom` setup |
| `/bot-version` | — | private only | Show the OpenClaw framework version and plugin version |
| `/bot-upgrade` | — | private only | Show the QQBot upgrade guide link |
| `/bot-approve` | allowlist | private only | Manage command-execution approval config (on / off / always / reset / status) |
| `/bot-logs` | allowlist | private only | Export recent gateway logs as a file |
| `/bot-clear-storage` | allowlist | private only | Delete cached downloads under the QQBot media directory |
| `/bot-streaming` | allowlist | private only | Toggle C2C streaming replies |
| `/bot-group-allways` | allowlist | private only | Toggle the default group activation mode (mention-required vs. always-on) |
Append `?` to any command for usage help (for example `/bot-upgrade ?`).
"Auth: allowlist" commands additionally require the sender's openid in an
explicit non-wildcard `allowFrom` list (`groupAllowFrom` takes precedence for
group-issued commands, falling back to `allowFrom`). A wildcard
`allowFrom: ["*"]` permits chat but not these commands. Running one of them
outside private chat, or without authorization, returns a hint rather than
silently dropping the message.
`/bot-me`, `/bot-version`, and `/bot-upgrade` are private-chat-only but do not
require the allowlist — any C2C sender can run them.
When QQ Bot exec approvals use the default same-chat fallback, native approval
button clicks follow the same explicit non-wildcard command allowlist. To
grant approval-only access without broader command access, configure
`channels.qqbot.execApprovals.approvers`. Native exec approvals are enabled by
default.
## Media and storage
- Inbound, outbound, and gateway-bridge media share one payload root under
`~/.openclaw/media/qqbot` (honoring `OPENCLAW_HOME` when set), so uploads,
downloads, and transcode caches stay under one guarded directory.
- Rich media delivery for C2C and group targets goes through one `sendMedia`
path. Local files and in-memory buffers of 5&nbsp;MiB or more use QQ's
chunked upload endpoints; smaller payloads and remote-URL/Base64 sources use
the one-shot upload API.
- If a hot upgrade interrupts the Gateway before it finishes writing
`openclaw.json`, the plugin restores the last-known `appId` / `clientSecret`
for that account from an internal snapshot on the next start (never
overwriting an intentional config change), so re-scanning the QR code is not
required.
## Troubleshooting
- **Gateway does not start / no inbound messages:** verify `appId` and
`clientSecret` are correct and the bot is enabled on the QQ Open Platform.
A missing credential surfaces as "QQBot not configured (missing appId or
clientSecret)".
- **Setup with `--token-file` still shows unconfigured:** `--token-file` only
sets the AppSecret. `appId` must still be set in config or `QQBOT_APP_ID`.
- **Bursty group replies collide:** the inbound queue evicts bot-authored
messages ahead of human ones when a peer's queue fills up, and merges
bursts of normal (non-command) group messages into one attributed turn, so
a flood of bot chatter should not starve human messages.
- **Proactive messages not arriving:** QQ may block bot-initiated messages if
the user has not interacted recently.
- **Voice not transcribed:** ensure STT is configured and the provider is
reachable.
## Related
- [Pairing](/channels/pairing)
- [Groups](/channels/groups)
- [Channel troubleshooting](/channels/troubleshooting)

151
docs/channels/raft.md Normal file
View File

@@ -0,0 +1,151 @@
---
summary: "Raft External Agent support through the Raft CLI wake bridge"
read_when:
- You want to connect OpenClaw to a Raft workspace
- You are configuring a Raft External Agent
- You are debugging Raft wake delivery
title: "Raft"
sidebarTitle: "Raft"
---
Raft connects an OpenClaw agent to a Raft External Agent through the local
Raft CLI. Raft sends authenticated wake hints to the Gateway; the agent then
uses the Raft CLI to check and send messages. Direct chat only (no groups).
## Install
Raft is an official external plugin. Install it on the Gateway host:
```bash
openclaw plugins install @openclaw/raft
openclaw gateway restart
```
Details: [Plugins](/tools/plugin)
## Prerequisites
- A Raft workspace with an External Agent.
- The Raft CLI installed on the same host as the OpenClaw Gateway, on the
service's `PATH`.
- A Raft CLI profile that is already signed in and associated with that
External Agent.
The plugin does not store Raft credentials; the Raft CLI keeps that
authentication in its own profile.
## Configure
Set the profile in config:
```json5
{
channels: {
raft: {
enabled: true,
profile: "openclaw",
},
},
}
```
For the default account, you can instead set `RAFT_PROFILE` in the Gateway
environment:
```bash
RAFT_PROFILE=openclaw
```
Use a named account when one Gateway connects to more than one Raft External Agent:
```json5
{
channels: {
raft: {
accounts: {
support: {
profile: "support-agent",
},
engineering: {
profile: "engineering-agent",
},
},
},
},
}
```
Interactive setup records the same profile:
```bash
openclaw channels add --channel raft
```
## How it works
When the Gateway starts, the plugin:
1. Opens a loopback-only HTTP wake endpoint on an ephemeral port.
2. Starts `raft --profile <profile> agent bridge` with that endpoint and a
per-process token.
3. Accepts only authenticated, content-free wake hints with a replay identity
from the local bridge.
4. Requires one of `eventId`, `attemptId`, `messageId`, `delivery_id`,
`wake_id`, or `id` on every wake payload.
5. Deduplicates retried wake deliveries by bridge event id for 24 hours,
including across Gateway restarts.
6. Returns a stable runtime session for the current bridge and an empty
activity-drain batch for the Raft CLI protocol.
7. Starts one serialized OpenClaw agent turn per accepted wake.
The bridge owns Raft delivery retries and reconnects. The OpenClaw turn
receives only a wake notice, not a copied Raft message body. It uses the CLI
to read pending messages and to send its response:
```bash
raft --profile openclaw message check
raft --profile openclaw message send
```
<Note>
Raft is not a push-message transport. OpenClaw does not automatically send the model's final text back through the bridge, so the agent must use the Raft CLI after processing a wake.
</Note>
## Verify
Check that OpenClaw can find the CLI and has a configured profile:
```bash
openclaw channels status --probe
openclaw plugins inspect raft --runtime --json
```
Then send a message to the Raft External Agent. The Gateway log should show
the Raft bridge starting, followed by an inbound wake. The agent should use
the configured Raft profile to check its pending messages.
## Troubleshooting
<AccordionGroup>
<Accordion title="Raft CLI is missing">
Install the Raft CLI on the Gateway host and make `raft` available on the
service's `PATH`. Verify it with `raft --help`, then restart the Gateway.
</Accordion>
<Accordion title="The bridge exits immediately">
Verify the configured profile is signed in and belongs to the intended
Raft External Agent. Run `raft --profile <profile> agent bridge` directly
to see the CLI diagnostic.
</Accordion>
<Accordion title="A wake arrives but no Raft response is sent">
This is expected when the agent does not invoke the Raft CLI. The wake
bridge does not carry message bodies or automatic final replies. Check the
agent's tool policy and ensure it can run `raft --profile <profile>
message check` and `message send`.
</Accordion>
</AccordionGroup>
## References
- [Raft](https://raft.build/)
- [Raft documentation](https://docs.raft.build/welcome/)
- [Hermes Raft integration](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/raft)

457
docs/channels/signal.md Normal file
View File

@@ -0,0 +1,457 @@
---
summary: "Signal support via signal-cli (native daemon or bbernhard container), setup paths, and number model"
read_when:
- Setting up Signal support
- Debugging Signal send/receive
title: "Signal"
---
Signal is a downloadable channel plugin (`@openclaw/signal`). The gateway talks to `signal-cli` over HTTP: either the native daemon (JSON-RPC + SSE) or the [bbernhard/signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) container (REST + WebSocket). OpenClaw does not embed libsignal.
## The number model (read this first)
- The gateway connects to a **Signal device**: the `signal-cli` account.
- Running the bot on **your personal Signal account** makes it ignore your own messages (loop protection).
- For "I text the bot and it replies," use a **separate bot number**.
## Install
```bash
openclaw plugins install @openclaw/signal
```
Bare plugin specs try ClawHub first, then npm fallback. Force a source with `openclaw plugins install clawhub:@openclaw/signal` or `npm:@openclaw/signal`. `plugins install` registers and enables the plugin; no separate `enable` step is needed. See [Plugins](/tools/plugin) for general install rules.
## Quick setup
<Steps>
<Step title="Pick a number">
Use a **separate Signal number** for the bot (recommended).
</Step>
<Step title="Install the plugin">
```bash
openclaw plugins install @openclaw/signal
```
</Step>
<Step title="Run the guided setup">
```bash
openclaw channels add
```
The wizard detects whether `signal-cli` is on `PATH` and, when missing, offers to install it: downloads the official native GraalVM build on Linux x86-64, or installs via Homebrew on macOS and other architectures. It then prompts for the bot number and `signal-cli` path.
</Step>
<Step title="Link or register the account">
- **QR link (fastest):** `signal-cli link -n "OpenClaw"`, then scan with Signal. See [Path A](#setup-path-a-link-existing-signal-account-qr).
- **SMS registration:** dedicated number with captcha + SMS verification. See [Path B](#setup-path-b-register-dedicated-bot-number-sms-linux).
</Step>
<Step title="Verify and pair">
```bash
openclaw gateway call channels.status --params '{"probe":true}'
```
Send a first DM and approve pairing: `openclaw pairing approve signal <CODE>`.
</Step>
</Steps>
Minimal config:
```json5
{
channels: {
signal: {
enabled: true,
account: "+15551234567",
cliPath: "signal-cli",
dmPolicy: "pairing",
allowFrom: ["+15557654321"],
},
},
}
```
| Field | Description |
| ------------ | ------------------------------------------------- |
| `account` | Bot phone number in E.164 format (`+15551234567`) |
| `cliPath` | Path to `signal-cli` (`signal-cli` if on `PATH`) |
| `configPath` | signal-cli config dir passed as `--config` |
| `dmPolicy` | DM access policy (`pairing` recommended) |
| `allowFrom` | Phone numbers or `uuid:<id>` values allowed to DM |
Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [Multi-account channels](/gateway/config-channels#multi-account-all-channels) for the shared pattern.
## What it is
- Deterministic routing: replies always go back to Signal.
- DMs share the agent's main session; groups are isolated (`agent:<agentId>:signal:group:<groupId>`).
- By default, Signal may write config updates triggered by `/config set|unset` (requires `commands.config: true`). Disable with `channels.signal.configWrites: false`.
## Setup path A: link existing Signal account (QR)
1. Install `signal-cli` (JVM or native build), or let `openclaw channels add` install it for you.
2. Link a bot account: `signal-cli link -n "OpenClaw"`, then scan the QR in Signal.
3. Configure Signal and start the gateway.
## Setup path B: register dedicated bot number (SMS, Linux)
Use this for a dedicated bot number instead of linking an existing Signal app account. The flow below is tested on Ubuntu 24.
1. Get a number that can receive SMS (or voice verification for landlines). A dedicated bot number avoids account/session conflicts.
2. Install `signal-cli` on the gateway host:
```bash
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 a JRE first. Keep `signal-cli` updated; upstream notes old releases can break as Signal server APIs change.
3. Register and verify the number:
```bash
signal-cli -a +<BOT_PHONE_NUMBER> register
```
If captcha is required (browser access is needed to complete this step):
1. Open `https://signalcaptchas.org/registration/generate.html`.
2. Complete the captcha, copy the `signalcaptcha://...` link target from "Open Signal".
3. Run from the same external IP as the browser session when possible (captcha tokens expire quickly).
4. Register and verify immediately:
```bash
signal-cli -a +<BOT_PHONE_NUMBER> register --captcha '<SIGNALCAPTCHA_URL>'
signal-cli -a +<BOT_PHONE_NUMBER> verify <VERIFICATION_CODE>
```
4. Configure OpenClaw, restart the gateway, verify the channel:
```bash
# If you run the gateway as a user systemd service:
systemctl --user restart openclaw-gateway.service
# Then verify:
openclaw doctor
openclaw channels status --probe
```
5. Pair your DM sender:
- Send any message to the bot number.
- Approve on the server: `openclaw pairing approve signal <PAIRING_CODE>`.
- Save the bot number as a contact on your phone to avoid "Unknown contact".
<Warning>
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 to keep your existing phone app setup.
</Warning>
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)
To manage `signal-cli` yourself (slow JVM cold starts, container init, shared CPUs), run the daemon separately and point OpenClaw at it:
```json5
{
channels: {
signal: {
httpUrl: "http://127.0.0.1:8080",
autoStart: false,
},
},
}
```
This skips auto-spawn and OpenClaw's startup wait. For slow auto-spawned starts, set `channels.signal.startupTimeoutMs`.
## Container mode (bbernhard/signal-cli-rest-api)
Instead of running `signal-cli` natively, use the [bbernhard/signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) Docker container, which wraps `signal-cli` behind a REST + WebSocket interface.
Requirements:
- The container **must** run with `MODE=json-rpc` for real-time message receiving.
- Register or link your Signal account inside the container before connecting OpenClaw.
Example `docker-compose.yml` service:
```yaml
signal-cli:
image: bbernhard/signal-cli-rest-api:latest
environment:
MODE: json-rpc
ports:
- "8080:8080"
volumes:
- signal-cli-data:/home/.local/share/signal-cli
```
OpenClaw config:
```json5
{
channels: {
signal: {
enabled: true,
account: "+15551234567",
httpUrl: "http://signal-cli:8080",
autoStart: false,
apiMode: "container", // or "auto" to detect automatically
},
},
}
```
`apiMode` controls which protocol OpenClaw uses:
| Value | Behavior |
| ------------- | ------------------------------------------------------------------------------------ |
| `"auto"` | (Default) Probes both transports; streaming validates container WebSocket receive |
| `"native"` | Force native signal-cli (JSON-RPC at `/api/v1/rpc`, SSE at `/api/v1/events`) |
| `"container"` | Force bbernhard container (REST at `/v2/send`, WebSocket at `/v1/receive/{account}`) |
When `apiMode` is `"auto"`, OpenClaw caches the detected mode for 30 seconds per daemon URL to avoid repeated probes (native wins when both transports are healthy). Container receive is only selected for streaming after `/v1/receive/{account}` upgrades to WebSocket, which requires `MODE=json-rpc`.
Container mode supports the same Signal operations as native mode where the container exposes matching APIs: sends, receives, attachments, typing indicators, read/viewed receipts, reactions, groups, and styled text. OpenClaw translates native Signal RPC calls into the container's REST payloads, including `group.{base64(internal_id)}` group IDs and `text_mode: "styled"` for formatted text.
Operational notes:
- Use `autoStart: false` with container mode; OpenClaw should not spawn a native daemon when `apiMode: "container"` is selected.
- Use `MODE=json-rpc` for receiving. `MODE=normal` can make `/v1/about` look healthy, but `/v1/receive/{account}` will not WebSocket-upgrade, so OpenClaw will not select container receive streaming in `auto` mode.
- Set `apiMode: "container"` when `httpUrl` points at the bbernhard REST API, `"native"` when it points at native `signal-cli` JSON-RPC/SSE, and `"auto"` when the deployment may vary.
- Container attachment downloads honor the same media byte limits as native mode. Oversized responses are rejected before being fully buffered when the server sends `Content-Length`, and while streaming otherwise.
## Access control (DMs + groups)
DMs:
- Default: `channels.signal.dmPolicy = "pairing"`.
- Unknown senders get a pairing code; messages are ignored until approved (codes expire after 1 hour).
- Approve via `openclaw pairing list signal` and `openclaw pairing approve signal <CODE>`.
- Pairing is the default token exchange for Signal DMs. Details: [Pairing](/channels/pairing)
- UUID-only senders (from `sourceUuid`) are stored as `uuid:<id>` in `channels.signal.allowFrom`.
Groups:
- `channels.signal.groupPolicy = open | allowlist | disabled`.
- `channels.signal.groupAllowFrom` controls which groups or senders can trigger group replies when `allowlist` is set; entries can be Signal group IDs (raw, `group:<id>`, or `signal:group:<id>`), sender phone numbers, `uuid:<id>` values, or `*`.
- `channels.signal.groups["<group-id>" | "*"]` can override group behavior with `requireMention`, `tools`, and `toolsBySender`.
- Use `channels.signal.accounts.<id>.groups` for per-account overrides in multi-account setups.
- Allowlisting a group through `groupAllowFrom` does not disable mention gating by itself. A specifically configured `channels.signal.groups["<group-id>"]` entry processes every group message unless `requireMention: true` is explicitly 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)
- Native mode: `signal-cli` runs as a daemon; the gateway reads events via SSE.
- Container mode: the gateway sends via REST API and receives via WebSocket.
- 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 are supported (base64 fetched from `signal-cli`).
- Voice-note attachments use the `signal-cli` filename as a MIME fallback when `contentType` is missing, so audio transcription can still classify AAC voice memos.
- 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.
## Lifecycle status reactions
Set `messages.statusReactions.enabled: true` to let Signal show the shared queued/thinking/tool/compaction/done/error reaction lifecycle on inbound turns. Signal uses the inbound message timestamp as the reaction target; group reactions are sent with the Signal group ID plus the original sender as the target author.
Status reactions also require an ack reaction and a matching `messages.ackReactionScope` (`direct`, `group-all`, `group-mentions`, or `all`). Set `channels.signal.reactionLevel: "off"` to disable Signal status reactions.
`messages.removeAckAfterReply: true` clears the final status reaction after the configured hold time. Otherwise Signal restores the initial ack reaction after the final done/error state.
## Reactions (message tool)
Use `message action=react` with `channel=signal`.
- Targets: sender E.164 or UUID (use `uuid:<id>` from pairing output; a bare UUID also works).
- `messageId` is the Signal timestamp for the message you're reacting to.
- Group reactions require `targetAuthor` or `targetAuthorUuid`.
```text
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:<groupId> targetAuthor=uuid:<sender-uuid> messageId=1737630212345 emoji=✅
```
Config:
- `channels.signal.actions.reactions`: enable/disable reaction actions (default true).
- `channels.signal.reactionLevel`: `off | ack | minimal | extensive` (default `minimal`).
- `off`/`ack` disables agent reactions (message tool `react` errors).
- `minimal`/`extensive` enables agent reactions and sets the guidance level.
- Per-account overrides: `channels.signal.accounts.<id>.actions.reactions`, `channels.signal.accounts.<id>.reactionLevel`.
## Approval reactions
Signal exec and plugin approval prompts use the top-level `approvals.exec` and `approvals.plugin` routing blocks. Signal has no `channels.signal.execApprovals` block.
- `👍` approves once.
- `👎` denies.
- Use `/approve <id> allow-always` when a request offers persistent approval.
Approval reaction resolution requires explicit Signal approvers from `channels.signal.allowFrom`, `channels.signal.defaultTo`, or the matching account-level fields. Direct same-chat exec approval prompts can still suppress the duplicate local `/approve` fallback without explicit approvers; no-approver group approvals keep the local fallback visible.
## Delivery targets (CLI/cron)
- DMs: `signal:+15551234567` (or plain E.164).
- UUID DMs: `uuid:<id>` (or bare UUID).
- Groups: `signal:group:<groupId>`.
- Usernames: `username:<name>` (if supported by your Signal account).
## Aliases
Configure aliases for stable names on recurring Signal targets. Aliases are OpenClaw-side config only; they do not create or edit Signal contacts.
```json5
{
channels: {
signal: {
aliases: {
me: "+15557654321",
jane: "uuid:123e4567-e89b-12d3-a456-426614174000",
ops: "group:<groupId>",
},
defaultTo: "signal:me",
},
},
}
```
Use aliases anywhere Signal delivery targets are accepted:
```bash
openclaw message send --channel signal --target signal:ops --message "Deployment is complete"
```
Per-account aliases inherit the top-level aliases and can add or override names:
```json5
{
channels: {
signal: {
aliases: {
me: "+15557654321",
},
accounts: {
work: {
aliases: {
ops: "group:<workGroupId>",
},
},
},
},
},
}
```
`openclaw directory peers list --channel signal` and `openclaw directory groups list --channel signal` list configured aliases. The Signal directory is config-backed; it does not live-query Signal contacts or mutate the Signal account.
## Troubleshooting
Run this ladder first:
```bash
openclaw status
openclaw gateway status
openclaw logs --follow
openclaw doctor
openclaw channels status --probe
```
Then confirm DM pairing state if needed:
```bash
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
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.apiMode`: `auto | native | container` (default: auto). See [Container mode](#container-mode-bbernhardsignal-cli-rest-api).
- `channels.signal.account`: E.164 for the bot account.
- `channels.signal.cliPath`: path to `signal-cli`.
- `channels.signal.configPath`: optional `signal-cli --config` directory.
- `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 (min 1000, cap 120000; default 30000).
- `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:<id>`). `open` requires `"*"`. Signal has no usernames; use phone/UUID IDs.
- `channels.signal.aliases`: OpenClaw-side aliases for DM or group delivery targets.
- `channels.signal.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
- `channels.signal.groupAllowFrom`: group allowlist; accepts Signal group IDs (raw, `group:<id>`, or `signal:group:<id>`), sender E.164 numbers, or `uuid:<id>` values.
- `channels.signal.groups`: per-group overrides keyed by Signal group ID (or `"*"`). Supported fields: `requireMention`, `tools`, `toolsBySender`.
- `channels.signal.accounts.<id>.groups`: per-account version of `channels.signal.groups` for multi-account setups.
- `channels.signal.accounts.<id>.aliases`: per-account aliases, merged with top-level aliases.
- `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["<phone_or_uuid>"].historyLimit`.
- `channels.signal.textChunkLimit`: outbound chunk size in characters (default 4000).
- `channels.signal.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
- `channels.signal.mediaMaxMb`: inbound/outbound media cap in MB (default 8).
- `channels.signal.reactionLevel`: `off | ack | minimal | extensive` (default `minimal`). See [Reactions](#reactions-message-tool).
- `channels.signal.reactionNotifications`: `off | own | all | allowlist` (default `own`) - when the agent is notified of incoming reactions from others.
- `channels.signal.reactionAllowlist`: senders whose reactions notify the agent when `reactionNotifications: "allowlist"`.
- `channels.signal.blockStreaming`, `channels.signal.blockStreamingCoalesce`: block-mode streaming controls shared across channels. See [Streaming](/concepts/streaming).
Related global options:
- `agents.list[].groupChat.mentionPatterns` (Signal does not support native mentions).
- `messages.groupChat.mentionPatterns` (global fallback).
- `messages.responsePrefix`.
## Related
- [Channels Overview](/channels) - all supported channels
- [Pairing](/channels/pairing) - DM authentication and pairing flow
- [Groups](/channels/groups) - group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) - session routing for messages
- [Security](/gateway/security) - access model and hardening

1654
docs/channels/slack.md Normal file

File diff suppressed because it is too large Load Diff

424
docs/channels/sms.md Normal file
View File

@@ -0,0 +1,424 @@
---
summary: "Twilio SMS channel setup, access controls, and webhook configuration"
read_when:
- You want to connect OpenClaw to SMS through Twilio
- You need SMS webhook or allowlist setup
title: "SMS"
---
OpenClaw receives and sends SMS through a Twilio phone number or Messaging Service. The Gateway registers an inbound webhook route (default `/webhooks/sms`), validates Twilio request signatures by default, and sends replies back through Twilio's Messages API.
Status: official plugin, installed separately. Text only: no MMS/media, direct messages only.
<CardGroup cols={3}>
<Card title="Pairing" icon="link" href="/channels/pairing">
Default DM policy for SMS is pairing.
</Card>
<Card title="Gateway security" icon="shield" href="/gateway/security">
Review webhook exposure and sender access controls.
</Card>
<Card title="Channel troubleshooting" icon="wrench" href="/channels/troubleshooting">
Cross-channel diagnostics and repair playbooks.
</Card>
</CardGroup>
## Before you begin
You need:
- The official SMS plugin installed with `openclaw plugins install @openclaw/sms`.
- A Twilio account with an SMS-capable phone number, or a Twilio Messaging Service.
- The Twilio Account SID and Auth Token.
- A public HTTPS URL that reaches your OpenClaw Gateway.
- A sender policy choice: `pairing` (default) for private use, `allowlist` for preapproved phone numbers, or `open` only for intentionally public SMS access.
One Twilio number can serve both SMS and [Voice Call](/plugins/voice-call) if it has both capabilities. The SMS webhook and Voice webhook are configured separately in Twilio and use separate Gateway paths; this page only covers the SMS webhook.
## Quick Setup
<Steps>
<Step title="Install the plugin">
```bash
openclaw plugins install @openclaw/sms
```
</Step>
<Step title="Create or choose a Twilio sender">
In Twilio, open **Phone Numbers > Manage > Active numbers** and choose an SMS-capable number. Save:
- Account SID, for example `ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`
- Auth Token
- Sender phone number, for example `+15551234567`
If you use a Messaging Service instead of a fixed sender number, save the Messaging Service SID, for example `MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`.
</Step>
<Step title="Configure the SMS channel">
Save this as `sms.patch.json5` and change the placeholders:
```json5
{
channels: {
sms: {
enabled: true,
accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authToken: "twilio-auth-token",
fromNumber: "+15551234567",
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
dmPolicy: "pairing",
},
},
}
```
Apply it:
```bash
openclaw config patch --file ./sms.patch.json5 --dry-run
openclaw config patch --file ./sms.patch.json5
```
</Step>
<Step title="Point Twilio at the Gateway webhook">
In the Twilio phone number settings, open **Messaging** and set **A message comes in** to:
```text
https://gateway.example.com/webhooks/sms
```
Use HTTP `POST`. The default local path is `/webhooks/sms`; change `channels.sms.webhookPath` if you need a different route.
</Step>
<Step title="Expose the exact SMS webhook path">
Your public URL must route the SMS path to the Gateway process (default port `18789`). If you use Tailscale Funnel for local testing, expose `/webhooks/sms` explicitly:
```bash
tailscale funnel --bg --set-path /webhooks/sms http://127.0.0.1:<gateway-port>/webhooks/sms
tailscale funnel status
```
Voice Call and SMS use separate webhook paths. If the same Twilio number handles both, keep both routes configured in Twilio and in your tunnel.
</Step>
<Step title="Start the Gateway and approve first sender">
```bash
openclaw gateway
```
Send a text message to the Twilio number. The first message creates a pairing request. Approve it:
```bash
openclaw pairing list sms
openclaw pairing approve sms <CODE>
```
Pairing codes expire after 1 hour.
</Step>
</Steps>
## Configuration Examples
All keys live under `channels.sms` (and per account under `channels.sms.accounts.<id>`):
| Key | Default | Purpose |
| --------------------------------------- | --------------- | ------------------------------------------------------------------- |
| `enabled` | `true` | Enable or disable the channel/account. |
| `accountSid` | — | Twilio Account SID (`AC...`). |
| `authToken` | — | Twilio Auth Token; plaintext string or SecretRef. |
| `fromNumber` | — | E.164 sender number. |
| `messagingServiceSid` | — | Messaging Service SID (`MG...`) used when no `fromNumber` resolves. |
| `defaultTo` | — | Default destination when a send flow omits an explicit target. |
| `webhookPath` | `/webhooks/sms` | Gateway HTTP path for inbound Twilio webhooks. |
| `publicWebhookUrl` | — | Public URL configured in Twilio; required for signature validation. |
| `dangerouslyDisableSignatureValidation` | `false` | Skip `X-Twilio-Signature` checks; local tunnel testing only. |
| `dmPolicy` | `"pairing"` | `pairing`, `allowlist`, `open`, or `disabled`. |
| `allowFrom` | `[]` | Allowed sender numbers in E.164, or `"*"` with `dmPolicy: "open"`. |
| `textChunkLimit` | `1500` | Maximum characters per outbound SMS chunk. |
| `accounts`, `defaultAccount` | — | Multi-account map and default account id. |
### Config file
Use config-file setup when you want the channel definition to travel with the Gateway config:
```json5
{
channels: {
sms: {
enabled: true,
accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authToken: "twilio-auth-token",
fromNumber: "+15551234567",
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
dmPolicy: "pairing",
},
},
}
```
### Environment variables
Environment variables apply to the default account only; config values take precedence over env values.
| Variable | Maps to |
| ----------------------------------------------- | -------------------------------------------------- |
| `TWILIO_ACCOUNT_SID` | `accountSid` |
| `TWILIO_AUTH_TOKEN` | `authToken` |
| `TWILIO_PHONE_NUMBER` (alias `TWILIO_SMS_FROM`) | `fromNumber` |
| `TWILIO_MESSAGING_SERVICE_SID` | `messagingServiceSid` |
| `SMS_PUBLIC_WEBHOOK_URL` | `publicWebhookUrl` |
| `SMS_WEBHOOK_PATH` | `webhookPath` |
| `SMS_ALLOWED_USERS` | `allowFrom` (comma-separated) |
| `SMS_TEXT_CHUNK_LIMIT` | `textChunkLimit` |
| `SMS_DANGEROUSLY_DISABLE_SIGNATURE_VALIDATION` | `dangerouslyDisableSignatureValidation` (`"true"`) |
```bash
export TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export TWILIO_AUTH_TOKEN="<twilio-auth-token>"
export TWILIO_PHONE_NUMBER="+15551234567"
export SMS_PUBLIC_WEBHOOK_URL="https://gateway.example.com/webhooks/sms"
```
Then enable the channel in config:
```json5
{
channels: {
sms: {
enabled: true,
dmPolicy: "pairing",
},
},
}
```
### SecretRef auth token
`authToken` can be a SecretRef (`source: "env" | "file" | "exec"`). Use this when the Gateway should resolve the Twilio Auth Token from the OpenClaw secrets runtime instead of storing plaintext config:
```json5
{
channels: {
sms: {
enabled: true,
accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authToken: { source: "env", provider: "default", id: "TWILIO_AUTH_TOKEN" },
fromNumber: "+15551234567",
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
dmPolicy: "pairing",
},
},
}
```
The referenced environment variable or secret provider must be visible to the Gateway runtime. Restart managed Gateway processes after changing host environment variables.
### Messaging Service sender
Use `messagingServiceSid` instead of `fromNumber` when Twilio should choose the sender through a Messaging Service:
```json5
{
channels: {
sms: {
enabled: true,
accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authToken: "twilio-auth-token",
messagingServiceSid: "MGxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
dmPolicy: "pairing",
},
},
}
```
If both `fromNumber` and `messagingServiceSid` are present after config and env resolution, `fromNumber` is used.
### Default outbound target
Set `defaultTo` when automation or agent-initiated delivery should have a default destination if a send flow omits an explicit target:
```json5
{
channels: {
sms: {
enabled: true,
accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authToken: "twilio-auth-token",
fromNumber: "+15551234567",
defaultTo: "+15557654321",
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
},
},
}
```
## Access control
`channels.sms.dmPolicy` controls direct SMS access:
- `pairing` (default): unknown senders get a pairing code; approve with `openclaw pairing approve sms <CODE>`.
- `allowlist`: only senders in `allowFrom` are processed. An empty `allowFrom` rejects every sender (the Gateway logs a startup warning).
- `open`: config validation requires `allowFrom` to include `"*"`. Without the wildcard, only listed numbers can chat.
- `disabled`: all inbound DMs are dropped.
`allowFrom` entries should be E.164 phone numbers such as `+15551234567`. `sms:` and `twilio-sms:` prefixes are accepted and normalized. For a private assistant, prefer `dmPolicy: "allowlist"` with explicit phone numbers:
```json5
{
channels: {
sms: {
enabled: true,
accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authToken: "twilio-auth-token",
fromNumber: "+15551234567",
publicWebhookUrl: "https://gateway.example.com/webhooks/sms",
dmPolicy: "allowlist",
allowFrom: ["+15557654321"],
},
},
}
```
## Sending SMS
With the SMS channel selected, targets accept bare E.164 numbers or the `sms:` prefix:
```bash
openclaw message send --channel sms --target sms:+15551234567 --message "hello"
```
When channel selection is implicit, the `twilio-sms:` prefix selects this channel without taking over the `sms:` service prefix, which iMessage uses to pick carrier SMS delivery for its own targets:
```bash
openclaw message send --target twilio-sms:+15551234567 --message "hello"
```
The CLI requires an explicit `--target`. `defaultTo` is for automation and agent-initiated delivery paths where the target can be resolved from channel config.
Agent replies from inbound SMS conversations automatically go back to the sender through the configured Twilio sender.
SMS output is plain text. OpenClaw strips markdown, flattens fenced code blocks, rewrites links as `label (url)`, and splits long replies into chunks of at most `textChunkLimit` characters (default 1500) before sending them through Twilio.
## Verify Setup
After the Gateway starts:
1. Confirm the Gateway log shows the SMS webhook route.
2. Run a Twilio-side probe (checks the configured Twilio webhook URL/method and recent inbound errors):
```bash
openclaw channels capabilities --channel sms
openclaw channels status --channel sms --probe --json
```
3. Send an SMS to the Twilio number from your phone.
4. Run `openclaw pairing list sms`.
5. Approve the pairing code with `openclaw pairing approve sms <CODE>`.
6. Send another SMS and confirm the agent replies.
For outbound-only testing, use:
```bash
openclaw message send --channel sms --target sms:+15557654321 --message "OpenClaw SMS test"
```
### End-to-end test from macOS iMessage/SMS
On a Mac that can send carrier SMS through Messages, you can use `imsg` to drive the sender side without touching your phone:
```bash
imsg send --to "+15551234567" --service sms --text "OpenClaw SMS E2E $(date -u +%Y%m%dT%H%M%SZ)" --json
openclaw pairing list sms
openclaw pairing approve sms <CODE>
imsg send --to "+15551234567" --service sms --text "reply exactly SMS pong" --json
```
The first message should create a pairing request. The second message should receive the agent reply through Twilio.
## Webhook security
By default, OpenClaw validates `X-Twilio-Signature` using `publicWebhookUrl` and `authToken`. Keep `publicWebhookUrl` byte-for-byte aligned with the URL configured in Twilio, including scheme, host, path, and query string.
The webhook route also enforces, independent of signature validation:
- `POST` only.
- Rate limit of 30 requests per minute per source IP (HTTP 429 above that).
- The payload `AccountSid` must match the configured `accountSid` (HTTP 403 otherwise).
- Replayed `MessageSid` values are deduplicated for 10 minutes.
- Request bodies over 32 KB are rejected.
For local tunnel testing only, you can set:
```json5
{
channels: {
sms: {
dangerouslyDisableSignatureValidation: true,
},
},
}
```
Do not use disabled signature validation on a public Gateway.
## Multi-account config
Use `accounts` when you operate more than one Twilio number:
```json5
{
channels: {
sms: {
accounts: {
support: {
enabled: true,
accountSid: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
authToken: "twilio-auth-token",
fromNumber: "+15551234567",
publicWebhookUrl: "https://gateway.example.com/webhooks/sms/support",
webhookPath: "/webhooks/sms/support",
dmPolicy: "allowlist",
allowFrom: ["+15557654321"],
},
},
},
},
}
```
Each account must use a distinct `webhookPath`; the Gateway refuses to register a webhook route whose path is already owned by another account. `TWILIO_*`/`SMS_*` environment fallbacks apply only to the default account; set `defaultAccount` to change which account that is.
## Troubleshooting
### Twilio returns 403 or OpenClaw rejects the webhook
Check that `publicWebhookUrl` exactly matches the URL configured in Twilio, including scheme, host, path, and query string. Twilio signs the public URL string, so proxy rewrites and alternate hostnames can break signature validation.
A 403 with `Invalid account` means the inbound payload's `AccountSid` does not match the configured `accountSid`; check that the webhook points at the account that owns the number.
### No pairing request appears
Check the Twilio number's **Messaging** webhook URL and method. It must point to the SMS webhook URL and use `POST`. Also confirm the Gateway is reachable from the public internet or through your tunnel.
If the Twilio message log shows error `11200`, Twilio accepted the inbound SMS but could not reach your webhook. Check:
- Twilio **Messaging > A message comes in** points at `publicWebhookUrl`.
- The method is `POST`.
- The tunnel or reverse proxy exposes the exact `webhookPath`; for Tailscale Funnel, run `tailscale funnel status` and confirm `/webhooks/sms` is listed.
- `publicWebhookUrl` uses the same scheme, host, path, and query string Twilio sends, so signature validation can reproduce the signed URL.
`openclaw channels status --channel sms --probe` surfaces both mismatched Twilio webhook settings and recent `11200` errors.
### Outbound sends fail
Confirm `accountSid`, `authToken`, and either `fromNumber` or `messagingServiceSid` are resolved. If you use a trial Twilio account, the destination number may need to be verified in Twilio before outbound SMS will send.
### Messages arrive but the agent does not answer
Check `dmPolicy` and `allowFrom`. With the default `pairing` policy, the sender must be approved before normal agent turns are processed.

View File

@@ -0,0 +1,178 @@
---
summary: "Synology Chat webhook setup and OpenClaw config"
read_when:
- Setting up Synology Chat with OpenClaw
- Debugging Synology Chat webhook routing
title: "Synology Chat"
---
Synology Chat connects to OpenClaw through a webhook pair: a Synology Chat outgoing webhook posts inbound direct messages to the Gateway, and replies go back through a Synology Chat incoming webhook.
Status: official plugin, installed separately. Direct messages only; text and URL-based file sends are supported.
## Install
```bash
openclaw plugins install @openclaw/synology-chat
```
Local checkout (when running from a git repo):
```bash
openclaw plugins install ./path/to/local/synology-chat-plugin
```
Details: [Plugins](/tools/plugin)
## Quick setup
1. Install the plugin (above).
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. Finish setup in OpenClaw. Synology Chat appears in the same channel setup list in both flows:
- Guided: `openclaw onboard` or `openclaw channels add`
- Direct: `openclaw channels add --channel synology-chat --token <token> --url <incoming-webhook-url>`
5. Restart the Gateway and send a DM to the Synology Chat bot.
Webhook auth details:
- OpenClaw accepts the outgoing webhook token from `body.token`, then
`?token=...`, then headers.
- Accepted header forms:
- `x-synology-token`
- `x-webhook-token`
- `x-openclaw-token`
- `Authorization: Bearer <token>`
- Empty or missing tokens fail closed.
- Payloads may be `application/x-www-form-urlencoded` or `application/json`; `token`, `user_id`, and `text` are required.
Minimal config:
```json5
{
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.
`SYNOLOGY_CHAT_INCOMING_URL` and `SYNOLOGY_NAS_HOST` cannot be set from a workspace `.env`; see [Workspace `.env` files](/gateway/security#workspace-env-files).
## DM policy and access control
- Supported `dmPolicy` values: `allowlist` (default), `open`, and `disabled`. Synology Chat has no pairing flow; approve senders by adding their numeric Synology user IDs to `allowedUserIds`.
- `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.
- `dmPolicy: "open"` allows public DMs only when `allowedUserIds` includes `"*"`; with restrictive entries, only matching users can chat. `open` with an empty `allowedUserIds` list also refuses to start the route.
- `dmPolicy: "disabled"` blocks DMs.
- Reply recipient binding stays on stable numeric `user_id` by default. `channels.synology-chat.dangerouslyAllowNameMatching: true` is break-glass compatibility mode that re-enables mutable username/nickname lookup for reply delivery.
## Outbound delivery
Use numeric Synology Chat user IDs as targets. The `synology-chat:`, `synology_chat:`, and `synology:` prefixes are accepted.
Examples:
```bash
openclaw message send --channel synology-chat --target 123456 --message "Hello from OpenClaw"
openclaw message send --channel synology-chat --target synology-chat:123456 --message "Hello again"
openclaw message send --channel synology-chat --target synology:123456 --message "Short prefix"
```
Outbound text is chunked at 2000 characters. Media sends are supported by URL-based file delivery: the NAS downloads and attaches the file (max 32 MB). Outbound file URLs must use `http` or `https`, and private or otherwise blocked network targets are rejected before OpenClaw forwards the URL to the NAS webhook.
## 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.
Direct-message sessions are isolated per account and user, so the same numeric `user_id`
on two different Synology accounts does not share transcript state.
Give each enabled account a distinct `webhookPath`. OpenClaw rejects duplicate exact paths
and refuses to start named accounts that only inherit a shared webhook path in multi-account setups.
If you intentionally need legacy inheritance for a named account, set
`dangerouslyAllowInheritedWebhookPath: true` on that account or at `channels.synology-chat`,
but duplicate exact paths are still rejected fail-closed. Prefer explicit per-account paths.
```json5
{
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 (`rateLimitPerMinute`, default 30).
- Invalid token checks use constant-time secret comparison and fail closed; repeated invalid-token attempts temporarily lock out the source IP.
- Inbound message text is sanitized against known prompt-injection patterns and truncated at 4000 characters.
- Prefer `dmPolicy: "allowlist"` for production.
- Keep `dangerouslyAllowNameMatching` off unless you explicitly need legacy username-based reply delivery.
- Keep `dangerouslyAllowInheritedWebhookPath` off unless you explicitly accept shared-path routing risk in a multi-account setup.
## Troubleshooting
- `Missing required fields (token, user_id, text)`:
- the outgoing webhook payload is missing one of the required fields
- if Synology sends the token in headers, make sure the gateway/proxy preserves those headers
- `Invalid token`:
- the outgoing webhook secret does not match `channels.synology-chat.token`
- the request is hitting the wrong account/webhook path
- a reverse proxy stripped the token header before the request reached OpenClaw
- `Rate limit exceeded`:
- too many invalid token attempts from the same source can temporarily lock that source out
- authenticated senders also have a separate per-user message rate limit
- `Allowlist is empty. Configure allowedUserIds or use dmPolicy=open with allowedUserIds=["*"].`:
- `dmPolicy="allowlist"` is enabled but no users are configured
- `User not authorized`:
- the sender's numeric `user_id` is not in `allowedUserIds`
## Related
- [Channels Overview](/channels) — all supported channels
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) — session routing for messages
- [Security](/gateway/security) — access model and hardening

934
docs/channels/telegram.md Normal file
View File

@@ -0,0 +1,934 @@
---
summary: "Telegram bot support status, capabilities, and configuration"
read_when:
- Working on Telegram features or webhooks
title: "Telegram"
---
Production-ready for bot DMs and groups via grammY. Long polling is the default transport; webhook mode is optional.
<CardGroup cols={3}>
<Card title="Pairing" icon="link" href="/channels/pairing">
Default DM policy for Telegram is pairing.
</Card>
<Card title="Channel troubleshooting" icon="wrench" href="/channels/troubleshooting">
Cross-channel diagnostics and repair playbooks.
</Card>
<Card title="Gateway configuration" icon="settings" href="/gateway/configuration">
Full channel config patterns and examples.
</Card>
</CardGroup>
## Quick setup
<Steps>
<Step title="Create the bot token in BotFather">
Open Telegram, chat with **@BotFather** (confirm the handle is exactly `@BotFather`), run `/newbot`, follow the prompts, and save the token.
</Step>
<Step title="Configure token and DM policy">
```json5
{
channels: {
telegram: {
enabled: true,
botToken: "123:abc",
dmPolicy: "pairing",
groups: { "*": { requireMention: true } },
},
},
}
```
Env fallback: `TELEGRAM_BOT_TOKEN` (default account only; named accounts must use `botToken` or `tokenFile`).
Telegram does **not** use `openclaw channels login telegram`; set the token in config/env, then start the gateway.
</Step>
<Step title="Start gateway and approve first DM">
```bash
openclaw gateway
openclaw pairing list telegram
openclaw pairing approve telegram <CODE>
```
Pairing codes expire after 1 hour.
</Step>
<Step title="Add the bot to a group">
Add the bot to your group, then get the two IDs group access needs:
- your Telegram user ID, for `allowFrom` / `groupAllowFrom`
- the Telegram group chat ID, as the key under `channels.telegram.groups`
Get the group chat ID from `openclaw logs --follow`, a forwarded-ID bot, or Bot API `getUpdates`. After the group is allowed, `/whoami@<bot_username>` confirms the user and group IDs.
Negative supergroup IDs starting with `-100` are group chat IDs. They go under `channels.telegram.groups`, not `groupAllowFrom`.
</Step>
</Steps>
<Note>
Token resolution is account-aware: `tokenFile` beats `botToken` beats env, and config always wins over `TELEGRAM_BOT_TOKEN` (which only resolves for the default account). After a successful startup, OpenClaw caches the bot identity for up to 24 hours so restarts skip an extra `getMe` call; changing or removing the token clears that cache.
</Note>
## Telegram side settings
<AccordionGroup>
<Accordion title="Privacy mode and group visibility">
Telegram bots default to **Privacy Mode**, which limits which group messages they receive.
To see all group messages, either:
- disable privacy mode via `/setprivacy`, or
- make the bot a group admin.
After toggling privacy mode, remove and re-add the bot in each group so Telegram applies the change.
</Accordion>
<Accordion title="Group permissions">
Admin status is controlled in Telegram group settings. Admin bots receive all group messages, useful for always-on group behavior.
</Accordion>
<Accordion title="Helpful BotFather toggles">
- `/setjoingroups` — allow/deny group adds
- `/setprivacy` — group visibility behavior
</Accordion>
</AccordionGroup>
## Access control and activation
### Group bot identity
In groups and forum topics, an explicit mention of the configured bot handle (for example `@my_bot`) addresses the selected OpenClaw agent, even when the agent persona name differs from the Telegram username. Group silence policy still applies to unrelated traffic, but the bot handle itself is never "someone else."
<Tabs>
<Tab title="DM policy">
`channels.telegram.dmPolicy` controls direct message access:
- `pairing` (default)
- `allowlist` (requires at least one sender ID in `allowFrom`)
- `open` (requires `allowFrom` to include `"*"`)
- `disabled`
`dmPolicy: "open"` with `allowFrom: ["*"]` lets any Telegram account that finds or guesses the bot username command the bot. Use it only for intentionally public bots with tightly restricted tools; one-owner bots should use `allowlist` with numeric user IDs.
`channels.telegram.allowFrom` accepts numeric Telegram user IDs. `telegram:` / `tg:` prefixes are accepted and normalized.
In multi-account configs, a restrictive top-level `channels.telegram.allowFrom` is a safety boundary: an account-level `allowFrom: ["*"]` does not make that account public unless the merged effective allowlist still contains an explicit wildcard.
`dmPolicy: "allowlist"` with empty `allowFrom` blocks all DMs and is rejected by config validation.
Setup asks for numeric user IDs only. If your config has `@username` allowlist entries from an older setup, run `openclaw doctor --fix` to resolve them to numeric IDs (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` for allowlist flows (for example when `dmPolicy: "allowlist"` has no explicit IDs yet).
For one-owner bots, prefer `dmPolicy: "allowlist"` with explicit numeric `allowFrom` IDs over depending on previous pairing approvals.
Common confusion: DM pairing approval does not mean "this sender is authorized everywhere." Pairing grants DM access only. If no command owner exists yet, the first approved pairing also sets `commands.ownerAllowFrom`, giving owner-only commands and exec approvals an explicit operator account. Group sender authorization still comes from explicit config allowlists.
To be authorized for both DMs and group commands with one identity: put your numeric Telegram user ID in `channels.telegram.allowFrom`, and for owner-only commands make sure `commands.ownerAllowFrom` contains `telegram:<your user id>`.
### Finding your Telegram user ID
Safer (no third-party bot): DM your bot, run `openclaw logs --follow`, read `from.id`.
Official Bot API method:
```bash
curl "https://api.telegram.org/bot<bot_token>/getUpdates"
```
Third-party (less private): `@userinfobot` or `@getidsbot`.
</Tab>
<Tab title="Group policy and allowlists">
Two controls apply together:
1. **Which groups are allowed** (`channels.telegram.groups`)
- no `groups` config, `groupPolicy: "open"`: any group passes group-ID checks
- no `groups` config, `groupPolicy: "allowlist"` (default): all groups blocked until you add `groups` entries (or `"*"`)
- `groups` configured: acts as an allowlist (explicit IDs or `"*"`)
2. **Which senders are allowed in groups** (`channels.telegram.groupPolicy`)
- `open` / `allowlist` (default) / `disabled`
`groupAllowFrom` filters group senders; if unset, Telegram falls back to `allowFrom` (not the pairing store — group sender auth never inherits DM pairing-store approvals, a security boundary since `2026.2.25`).
`groupAllowFrom` entries should be numeric Telegram user IDs (`telegram:` / `tg:` prefixes are normalized); non-numeric entries are ignored. Do not put group or supergroup chat IDs here — negative chat IDs belong under `channels.telegram.groups`.
Practical pattern for one-owner bots: set your user ID in `channels.telegram.allowFrom`, leave `groupAllowFrom` unset, and allow the target groups under `channels.telegram.groups`.
If `channels.telegram` is entirely missing from config, runtime defaults to fail-closed `groupPolicy="allowlist"` unless `channels.defaults.groupPolicy` is explicitly set.
Owner-only group setup:
```json5
{
channels: {
telegram: {
enabled: true,
dmPolicy: "pairing",
allowFrom: ["<YOUR_TELEGRAM_USER_ID>"],
groupPolicy: "allowlist",
groups: {
"<GROUP_CHAT_ID>": {
requireMention: true,
},
},
},
},
}
```
Test from the group with `@<bot_username> ping`. Plain group messages do not trigger the bot while `requireMention: true`.
Allow any member in one specific group:
```json5
{
channels: {
telegram: {
groups: {
"-1001234567890": {
groupPolicy: "open",
requireMention: false,
},
},
},
},
}
```
Allow only specific users inside one specific group:
```json5
{
channels: {
telegram: {
groups: {
"-1001234567890": {
requireMention: true,
allowFrom: ["8734062810", "745123456"],
},
},
},
},
}
```
<Warning>
Common mistake: `groupAllowFrom` is not a group allowlist.
- Negative Telegram group/supergroup chat IDs (`-1001234567890`) go under `channels.telegram.groups`.
- Telegram user IDs (`8734062810`) go under `groupAllowFrom` to limit which people inside an allowed group can trigger the bot.
- Use `groupAllowFrom: ["*"]` only to let any member of an allowed group talk to the bot.
</Warning>
</Tab>
<Tab title="Mention behavior">
Group replies require mention by default. A mention can come from:
- a native `@botusername` mention, or
- a mention pattern in `agents.list[].groupChat.mentionPatterns` or `messages.groupChat.mentionPatterns`
Session-level toggles (state only, not persisted): `/activation always`, `/activation mention`. Use config for persistence:
```json5
{
channels: {
telegram: {
groups: {
"*": { requireMention: false },
},
},
},
}
```
Group history context is always on and bounded by `historyLimit`. Set `channels.telegram.historyLimit: 0` to disable the group history window. `openclaw doctor --fix` removes the retired `includeGroupHistoryContext` key.
Getting the group chat ID: forward a group message to `@userinfobot` / `@getidsbot`, read `chat.id` from `openclaw logs --follow`, inspect Bot API `getUpdates`, or (once the group is allowed) run `/whoami@<bot_username>`.
</Tab>
</Tabs>
## Runtime behavior
- Telegram runs inside 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, media placeholders, and persisted reply-chain context for replies the gateway has observed.
- Group sessions are isolated by group ID. Forum topics append `:topic:<threadId>`.
- DM messages can carry `message_thread_id`; OpenClaw preserves it for replies. DM topic sessions split only when Telegram `getMe` reports `has_topics_enabled: true` for the bot; otherwise DMs stay on the flat session.
- Long polling uses the grammY runner with per-chat/per-thread sequencing. Runner sink concurrency uses `agents.defaults.maxConcurrent`.
- Multi-account startup bounds concurrent `getMe` probes so large bot fleets do not fan out every account probe at once.
- Each gateway process guards long polling so only one active poller can use a bot token at a time. Persistent `getUpdates` 409 conflicts point to another OpenClaw gateway, script, or external poller using the same token.
- The polling watchdog restarts after 120 seconds without completed `getUpdates` liveness by default. Raise `channels.telegram.pollingStallThresholdMs` (30000-600000, per-account overrides supported) only if your deployment sees false polling-stall restarts during long-running work.
- Telegram Bot API has no read-receipt support (`sendReadReceipts` does not apply).
<Note>
`channels.telegram.dm.threadReplies` and `channels.telegram.direct.<chatId>.threadReplies` were removed. Run `openclaw doctor --fix` after upgrading if your config still has those keys. DM topic routing now follows Telegram `getMe.has_topics_enabled` (controlled by BotFather threaded mode): topics-enabled bots use thread-scoped DM sessions when Telegram sends `message_thread_id`; other DMs stay on the flat session.
</Note>
## Feature reference
<AccordionGroup>
<Accordion title="Live stream preview (message edits)">
OpenClaw streams partial replies in real time in direct chats, groups, and topics: send a preview message, then `editMessageText` repeatedly, finalizing in place.
- `channels.telegram.streaming` is `off | partial | block | progress` (default: `partial`)
- short initial answer previews are debounced, then materialized after a bounded delay if the run is still active
- `progress` keeps one editable status draft for tool progress, shows the stable status label when answer activity arrives before tool progress, clears it at completion, and sends the final answer as a normal message
- `streaming.preview.toolProgress` controls whether tool/progress updates reuse the same edited preview message (default: `true` when preview streaming is active)
- `streaming.preview.commandText` controls command/exec detail inside those lines: `raw` (default) or `status` (tool label only)
- `streaming.progress.commentary` (default: `false`) opts into assistant commentary/preamble text in the temporary progress draft
- legacy `channels.telegram.streamMode`, boolean `streaming` values, and retired native draft preview keys are detected; run `openclaw doctor --fix` to migrate them
Tool-progress lines are the short status updates shown while tools run (command execution, file reads, planning updates, patch summaries, Codex preamble/commentary in app-server mode). Telegram keeps these on by default (matches released behavior from `v2026.4.22`+).
Keep answer-preview edits but hide tool-progress lines:
```json
{
"channels": {
"telegram": {
"streaming": {
"mode": "partial",
"preview": { "toolProgress": false }
}
}
}
}
```
Keep tool-progress visible but hide command/exec text:
```json
{
"channels": {
"telegram": {
"streaming": {
"mode": "partial",
"preview": { "commandText": "status" }
}
}
}
}
```
`progress` mode shows tool progress without editing the final answer into that message. Put the command-text policy under `streaming.progress`:
```json
{
"channels": {
"telegram": {
"streaming": {
"mode": "progress",
"progress": {
"toolProgress": true,
"commandText": "status"
}
}
}
}
}
```
`streaming.mode: "off"` disables preview edits and suppresses generic tool/progress chatter instead of sending it as standalone status messages; approval prompts, media, and errors still route through normal final delivery. `streaming.preview.toolProgress: false` keeps only answer-preview edits.
<Note>
Selected quote replies are the exception. When `replyToMode` is `first`, `all`, or `batched` and the inbound message has selected quote text, OpenClaw sends the final answer through Telegram's native quote-reply path instead of editing the answer preview, so `streaming.preview.toolProgress` cannot show status lines that turn. Current-message replies without selected quote text still stream. Set `replyToMode: "off"` when tool-progress visibility matters more than native quote replies, or `streaming.preview.toolProgress: false` to accept that trade-off.
</Note>
For text-only replies: short previews get the final edit in place; long finals that split into multiple messages reuse the preview as the first chunk, then send only the remainder; progress-mode finals clear the status draft and use normal final delivery; if the final edit fails before completion is confirmed, OpenClaw falls back to normal final delivery and cleans up the stale preview. For complex replies (media payloads), OpenClaw always falls back to normal final delivery and cleans up the preview.
Preview streaming and block streaming are mutually exclusive — when block streaming is explicitly enabled, OpenClaw skips the preview stream to avoid double-streaming.
Reasoning: `/reasoning stream` streams reasoning into the live preview while generating, then deletes the reasoning preview after final delivery (use `/reasoning on` to keep it visible). The final answer is sent without reasoning text.
</Accordion>
<Accordion title="Rich message formatting">
Outbound text uses standard Telegram HTML messages by default, readable across current clients: bold, italic, links, code, spoilers, quotes — not Bot API 10.1 rich-only blocks (native tables, details, rich media, formulas).
Opt into Bot API 10.1 rich messages:
```json5
{
channels: {
telegram: {
richMessages: true,
},
},
}
```
When enabled: the agent is told rich messages are available for this bot/account; Markdown text renders through OpenClaw's Markdown IR as Telegram rich HTML; explicit rich HTML payloads preserve supported Bot API 10.1 tags (headings, tables, details, rich media, formulas); media captions still use Telegram HTML captions (rich messages do not replace captions, and captions cap at 1024 characters).
This keeps model text away from Telegram's rich-Markdown sigils, so currency like `$400-600K` is not parsed as math. Long rich text splits automatically across Telegram's limits. Tables over the 20-column limit fall back to a code block.
Default: off, for client compatibility — some current Desktop, Web, Android, and third-party clients render accepted rich messages as unsupported. Keep this off unless every client used with the bot can render them. `/status` shows whether the current session has rich messages on or off.
Link previews are on by default. `channels.telegram.linkPreview: false` disables automatic entity detection for rich text.
</Accordion>
<Accordion title="Native commands and custom commands">
Telegram's command menu is registered at startup with `setMyCommands`. `commands.native: "auto"` enables native commands for Telegram.
Add custom command menu entries:
```json5
{
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.
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 the Telegram menu. If native commands are disabled, built-ins are removed; custom/plugin commands may still register if configured.
Common setup failures:
- `setMyCommands failed` with `BOT_COMMANDS_TOO_MUCH` after a trim retry means the menu still overflows; reduce plugin/skill/custom commands or disable `channels.telegram.commands.native`.
- `deleteWebhook`, `deleteMyCommands`, or `setMyCommands` failing with `404: Not Found` while direct Bot API curl commands work usually means `channels.telegram.apiRoot` was set to the full `/bot<TOKEN>` endpoint. `apiRoot` must be the Bot API root only; `openclaw doctor --fix` removes an accidental trailing `/bot<TOKEN>`.
- `getMe returned 401` means Telegram rejected the configured bot token. Update `botToken`, `tokenFile`, or `TELEGRAM_BOT_TOKEN` (default account) with the current BotFather token; OpenClaw stops before polling so this is not reported as a webhook cleanup failure.
- `setMyCommands failed` with network/fetch errors usually means outbound DNS/HTTPS to `api.telegram.org` is blocked.
### Device pairing commands (`device-pair` plugin)
When installed:
1. `/pair` generates a setup code
2. paste the code in the iOS app
3. `/pair pending` lists pending requests (including role/scopes)
4. approve: `/pair approve <requestId>`, `/pair approve` (only pending request), or `/pair approve latest`
If a device retries with changed auth details (role, scopes, public key), the previous pending request is superseded with a new `requestId`; re-run `/pair pending` before approving.
More detail: [Pairing](/channels/pairing#pair-via-telegram).
</Accordion>
<Accordion title="Inline buttons">
Configure inline keyboard scope:
```json5
{
channels: {
telegram: {
capabilities: {
inlineButtons: "allowlist",
},
},
},
}
```
Per-account override:
```json5
{
channels: {
telegram: {
accounts: {
main: {
capabilities: {
inlineButtons: "allowlist",
},
},
},
},
},
}
```
Scopes: `off`, `dm`, `group`, `all`, `allowlist` (default). Legacy `capabilities: ["inlineButtons"]` maps to `"all"`.
Message action example:
```json5
{
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" }],
],
}
```
Mini App button example:
```json5
{
action: "send",
channel: "telegram",
to: "123456789",
message: "Open app:",
presentation: {
blocks: [
{
type: "buttons",
buttons: [{ label: "Launch", web_app: { url: "https://example.com/app" } }],
},
],
},
}
```
`web_app` buttons only work in private chats between a user and the bot.
Callback clicks not claimed by a registered plugin interactive handler are passed to the agent as text: `callback_data: <value>`.
</Accordion>
<Accordion title="Telegram message actions for agents and automation">
Actions:
- `sendMessage` (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`)
- `react` (`chatId`, `messageId`, `emoji`)
- `deleteMessage` (`chatId`, `messageId`)
- `editMessage` (`chatId`, `messageId`, `content` or `caption`, optional `presentation` inline buttons; button-only edits update reply markup)
- `createForumTopic` (`chatId`, `name`, optional `iconColor`, `iconCustomEmojiId`)
Ergonomic aliases: `send`, `react`, `delete`, `edit`, `sticker`, `sticker-search`, `topic-create`.
Gating: `channels.telegram.actions.sendMessage`, `deleteMessage`, `reactions`, `sticker` (default: disabled). `edit`, `createForumTopic`, and `editForumTopic` are enabled by default with no dedicated toggle.
Runtime sends use the active config/secrets snapshot from startup/reload, so action paths do not re-resolve `SecretRef` values per send.
Reaction removal semantics: [/tools/reactions](/tools/reactions).
</Accordion>
<Accordion title="Reply threading tags">
Explicit reply threading tags in generated output:
- `[[reply_to_current]]` — replies to the triggering message
- `[[reply_to:<id>]]` — replies to a specific message ID
`channels.telegram.replyToMode`: `off` (default), `first`, `all`.
When reply threading is enabled and the original text/caption is available, OpenClaw adds a native quote excerpt automatically. Telegram caps native quote text at 1024 UTF-16 code units; longer messages are quoted from the start and fall back to a plain reply if Telegram rejects the quote.
`off` disables implicit reply threading only; explicit `[[reply_to_*]]` tags are still honored.
</Accordion>
<Accordion title="Forum topics and thread behavior">
Forum supergroups: topic session keys append `:topic:<threadId>`; replies and typing target the topic thread; topic config path is `channels.telegram.groups.<chatId>.topics.<threadId>`.
General topic (`threadId=1`) is a special case: message sends omit `message_thread_id` (Telegram rejects `sendMessage(...thread_id=1)` with "thread not found"), but typing actions still include `message_thread_id` (empirically required for the typing indicator to appear).
Topic entries inherit group settings unless overridden (`requireMention`, `allowFrom`, `skills`, `systemPrompt`, `enabled`, `groupPolicy`). `agentId` is topic-only and does not inherit from group defaults. `topics."*"` sets defaults for every topic in that group; exact topic IDs still win over `"*"`.
**Per-topic agent routing**: each topic can route to a different agent via `agentId` in the topic config, giving it its own workspace, memory, and session:
```json5
{
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, for example `agent:zu:telegram:group:-1001234567890:topic:3`.
**Persistent ACP topic binding**: forum topics can pin ACP harness sessions through top-level typed bindings (`bindings[]` with `type: "acp"`, `match.channel: "telegram"`, `peer.kind: "group"`, and a topic-qualified id like `-1001234567890:topic:42`). Currently scoped to forum topics in groups/supergroups. See [ACP Agents](/tools/acp-agents).
**Thread-bound ACP spawn from chat**: `/acp spawn <agent> --thread here|auto` binds the current topic to a new ACP session; follow-ups route there directly, and OpenClaw pins the spawn confirmation in-topic. Requires `channels.telegram.threadBindings.spawnSessions` (default: `true`).
Template context exposes `MessageThreadId` and `IsForum`. DM chats with `message_thread_id` keep reply metadata but only use thread-aware session keys when Telegram `getMe` reports `has_topics_enabled: true`.
The retired `dm.threadReplies` and `direct.*.threadReplies` overrides are gone; BotFather threaded mode is the single source of truth. Run `openclaw doctor --fix` to remove stale config keys.
</Accordion>
<Accordion title="Audio, video, and stickers">
### Audio messages
Telegram distinguishes voice notes from audio files. Default: audio-file behavior; tag `[[audio_as_voice]]` in the agent reply to force a voice-note send. Inbound voice-note transcripts are framed as machine-generated, untrusted text in agent context, but mention detection still uses the raw transcript so mention-gated voice messages keep working.
```json5
{
action: "send",
channel: "telegram",
to: "123456789",
media: "https://example.com/voice.ogg",
asVoice: true,
}
```
### Video messages
Telegram distinguishes video files from video notes. Video notes do not support captions; provided message text sends separately.
```json5
{
action: "send",
channel: "telegram",
to: "123456789",
media: "https://example.com/video.mp4",
asVideoNote: true,
}
```
### Stickers
Inbound: static WEBP is downloaded and processed (placeholder `<media:sticker>`); animated TGS and video WEBM are skipped.
Sticker context fields: `Sticker.emoji`, `Sticker.setName`, `Sticker.fileId`, `Sticker.fileUniqueId`, `Sticker.cachedDescription`. Descriptions are cached in OpenClaw SQLite plugin state to reduce repeated vision calls.
Enable sticker actions:
```json5
{
channels: {
telegram: {
actions: {
sticker: true,
},
},
},
}
```
Send:
```json5
{
action: "sticker",
channel: "telegram",
to: "123456789",
fileId: "CAACAgIAAxkBAAI...",
}
```
Search cached stickers:
```json5
{
action: "sticker-search",
channel: "telegram",
query: "cat waving",
limit: 5,
}
```
</Accordion>
<Accordion title="Reaction notifications">
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`.
- `channels.telegram.reactionNotifications`: `off | own | all` (default: `own`)
- `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` (default: `minimal`)
`own` means user reactions to bot-sent messages only (best-effort via a 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 the group chat session; forum groups route to the general-topic session (`:topic:1`), not the exact originating topic.
`allowed_updates` for polling/webhook include `message_reaction` automatically.
</Accordion>
<Accordion title="Ack reactions">
`ackReaction` sends an acknowledgement emoji while OpenClaw processes an inbound message. `messages.ackReactionScope` decides *when* it is sent.
**Emoji resolution order:**
- `channels.telegram.accounts.<accountId>.ackReaction`
- `channels.telegram.ackReaction`
- `messages.ackReaction`
- agent identity emoji fallback (`agents.list[].identity.emoji`, else "👀")
Telegram expects a unicode emoji (for example "👀"); use `""` to disable the reaction for a channel or account.
**Scope (`messages.ackReactionScope`, default `"group-mentions"`; no Telegram-account or Telegram-channel override today):**
`all` (DMs + groups), `direct` (DMs only), `group-all` (every group message, no DMs), `group-mentions` (groups when the bot is mentioned; **no DMs** — default), `off` / `none` (disabled).
<Note>
The default scope (`group-mentions`) does not fire ack reactions in DMs. Set `messages.ackReactionScope` to `direct` or `all` for that. This value is read at Telegram provider startup, so a gateway restart is needed for the change to take effect.
</Note>
</Accordion>
<Accordion title="Config writes from Telegram events and commands">
Channel config writes are enabled by default (`configWrites !== false`). Telegram-triggered writes include group migration events (`migrate_to_chat_id`, updates `channels.telegram.groups`) and `/config set` / `/config unset` (requires command enablement).
Disable:
```json5
{
channels: {
telegram: {
configWrites: false,
},
},
}
```
</Accordion>
<Accordion title="Long polling vs webhook">
Default is long polling. For webhook mode, set `channels.telegram.webhookUrl` and `channels.telegram.webhookSecret`; optional `webhookPath` (default `/telegram-webhook`), `webhookHost` (default `127.0.0.1`), `webhookPort` (default `8787`), `webhookCertPath` (self-signed cert PEM for direct-IP or no-domain setups).
In long-polling mode, OpenClaw persists its restart watermark only after an update dispatches successfully; a failed handler leaves that update retryable in the same process instead of marking it completed.
The local listener binds to `127.0.0.1:8787` by default. For public ingress, put a reverse proxy in front of the local port, or set `webhookHost: "0.0.0.0"` intentionally.
Webhook mode validates request guards, the Telegram secret token, and the JSON body before returning `200`. OpenClaw then processes the update asynchronously through the same per-chat/per-topic bot lanes used by long polling, so slow agent turns do not hold Telegram's delivery ACK.
</Accordion>
<Accordion title="Limits, retry, and CLI targets">
- `channels.telegram.textChunkLimit` default 4000; `chunkMode="newline"` prefers paragraph boundaries (blank lines) before length splitting.
- `channels.telegram.mediaMaxMb` (default 100) caps inbound and outbound media size.
- `channels.telegram.mediaGroupFlushMs` (default 500, range 10-60000) controls how long albums/media groups are buffered before OpenClaw dispatches them as one inbound message. Increase it if album parts arrive late; decrease it to reduce album reply latency.
- `channels.telegram.timeoutSeconds` overrides the API client timeout (grammY default applies if unset). Bot clients clamp configured values below the 60-second outbound text/typing request guard so grammY does not abort visible reply delivery before OpenClaw's transport guard and fallback can run. Long polling still uses a 45-second `getUpdates` request guard so idle polls are not abandoned indefinitely.
- `channels.telegram.pollingStallThresholdMs` defaults to 120000; tune between 30000 and 600000 only for false-positive polling-stall restarts.
- group context history uses `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` (default 50); `0` disables.
- reply/quote/forward supplemental context normalizes into one selected conversation context window when the gateway has observed the parent messages; the observed-message cache lives in OpenClaw SQLite plugin state, and `openclaw doctor --fix` imports legacy sidecars. Telegram only includes one shallow `reply_to_message` per update, so chains older than the cache are limited to that payload.
- Telegram allowlists primarily gate who can trigger the agent, not a full supplemental-context redaction boundary.
- DM history: `channels.telegram.dmHistoryLimit`, `channels.telegram.dms["<user_id>"].historyLimit`.
- `channels.telegram.retry` applies to Telegram send helpers (CLI/tools/actions) for recoverable outbound API errors. Inbound final-reply delivery uses a bounded safe-send retry for pre-connect failures, but does not retry ambiguous post-send network envelopes that could duplicate visible messages.
CLI and message-tool send targets accept a numeric chat ID, username, or forum topic target:
```bash
openclaw message send --channel telegram --target 123456789 --message "hi"
openclaw message send --channel telegram --target @name --message "hi"
openclaw message send --channel telegram --target -1001234567890:topic:42 --message "hi topic"
```
Polls use `openclaw message poll` and support forum topics:
```bash
openclaw message poll --channel telegram --target 123456789 \
--poll-question "Ship it?" --poll-option "Yes" --poll-option "No"
openclaw message poll --channel telegram --target -1001234567890:topic:42 \
--poll-question "Pick a time" --poll-option "10am" --poll-option "2pm" \
--poll-duration-seconds 300 --poll-public
```
Telegram-only poll flags: `--poll-duration-seconds` (5-600), `--poll-anonymous`, `--poll-public`, `--thread-id` (or a `:topic:` target). `--poll-option` repeats 2-12 times (Telegram's option cap).
Telegram send also supports `--presentation` with `buttons` blocks for inline keyboards (when `channels.telegram.capabilities.inlineButtons` allows it), `--pin` or `--delivery '{"pin":true}'` to request pinned delivery when the bot can pin in that chat, and `--force-document` to send outbound images, GIFs, and videos as documents instead of compressed/animated/video uploads.
Action gating: `channels.telegram.actions.sendMessage=false` disables all outbound messages including polls; `channels.telegram.actions.poll=false` disables poll creation while leaving regular sends enabled.
</Accordion>
<Accordion title="Exec approvals in Telegram">
Telegram supports exec approvals in approver DMs and can optionally post prompts in the originating chat or topic. Approvers must be numeric Telegram user IDs.
- `channels.telegram.execApprovals.enabled` (`"auto"` enables when at least one approver is resolvable)
- `channels.telegram.execApprovals.approvers` (falls back to numeric owner IDs from `commands.ownerAllowFrom`)
- `channels.telegram.execApprovals.target`: `dm` (default) | `channel` | `both`
- `agentFilter`, `sessionFilter`
`channels.telegram.allowFrom`, `groupAllowFrom`, and `defaultTo` control who can talk to the bot and where it sends normal replies — they do not make someone an exec approver. The first approved DM pairing bootstraps `commands.ownerAllowFrom` when no command owner exists yet, so one-owner setups work without duplicating IDs under `execApprovals.approvers`.
Channel delivery shows the command text in the chat; only enable `channel` or `both` in trusted groups/topics. When the prompt lands in a forum topic, OpenClaw preserves the topic for the approval prompt and follow-up. Exec approvals expire after 30 minutes by default.
Inline approval buttons also require `channels.telegram.capabilities.inlineButtons` to allow the target surface (`dm`, `group`, or `all`). Approval IDs prefixed with `plugin:` resolve through plugin approvals; others resolve through exec approvals first.
See [Exec approvals](/tools/exec-approvals).
</Accordion>
</AccordionGroup>
## Error reply controls
When the agent hits a delivery or provider error, the error policy controls whether error messages reach the Telegram chat:
| Key | Values | Default | Description |
| ----------------------------------- | -------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `channels.telegram.errorPolicy` | `always`, `once`, `silent` | `always` | `always` sends every error message to the chat. `once` sends each unique error message once per cooldown window (suppresses repeated identical errors). `silent` never sends error messages to the chat. |
| `channels.telegram.errorCooldownMs` | number (ms) | `14400000` (4h) | Cooldown window for the `once` policy. After an error is sent, the same message is suppressed until this interval elapses. Prevents error spam during outages. |
Per-account, per-group, and per-topic overrides are supported (same inheritance as other Telegram config keys).
```json5
{
channels: {
telegram: {
errorPolicy: "always",
errorCooldownMs: 120000,
groups: {
"-1001234567890": {
errorPolicy: "silent", // suppress errors in this group
},
},
},
},
}
```
## Troubleshooting
<AccordionGroup>
<Accordion title="Bot does not respond to non mention group messages">
- If `requireMention=false`, Telegram privacy mode must allow full visibility: BotFather `/setprivacy` -> Disable, then remove + re-add the bot to the group.
- `openclaw channels status` warns when config expects unmentioned group messages.
- `openclaw channels status --probe` checks explicit numeric group IDs; wildcard `"*"` cannot be membership-probed.
- Quick session test: `/activation always`.
</Accordion>
<Accordion title="Bot not seeing group messages at all">
- When `channels.telegram.groups` exists, the group must be listed (or include `"*"`).
- Verify bot membership in the group.
- Review `openclaw logs --follow` for skip reasons.
</Accordion>
<Accordion title="Commands work partially or not at all">
- Authorize your sender identity (pairing and/or numeric `allowFrom`); command authorization still applies even when group policy is `open`.
- `setMyCommands failed` with `BOT_COMMANDS_TOO_MUCH` means the native menu has too many entries; reduce plugin/skill/custom commands or disable native menus.
- `deleteMyCommands` / `setMyCommands` startup calls and `sendChatAction` typing calls are bounded and retry once through Telegram's transport fallback on request timeout. Persistent network/fetch errors usually mean DNS/HTTPS to `api.telegram.org` is unreachable.
</Accordion>
<Accordion title="Startup reports unauthorized token">
- `getMe returned 401` is a Telegram auth failure for the configured bot token. Re-copy or regenerate the token in BotFather, then update `channels.telegram.botToken`, `tokenFile`, `accounts.<id>.botToken`, or `TELEGRAM_BOT_TOKEN` (default account).
- `deleteWebhook 401 Unauthorized` during startup is also an auth failure; treating it as "no webhook exists" would only defer the same bad-token failure to a later API call.
</Accordion>
<Accordion title="Polling or network instability">
- Node 22+ with a custom fetch/proxy can trigger immediate abort behavior if `AbortSignal` types mismatch.
- Some hosts resolve `api.telegram.org` to IPv6 first; broken IPv6 egress causes intermittent API failures.
- Logs with `TypeError: fetch failed` or `Network request for 'getUpdates' failed!` are retried as recoverable network errors.
- During polling startup, OpenClaw reuses the successful startup `getMe` probe for grammY so the runner does not need a second `getMe` before the first `getUpdates`.
- If `deleteWebhook` fails with a transient network error during polling startup, OpenClaw continues into long polling instead of making another pre-poll control-plane call. A still-active webhook then surfaces as a `getUpdates` conflict; OpenClaw rebuilds the transport and retries webhook cleanup.
- If Telegram sockets recycle on a short fixed cadence, check for a low `channels.telegram.timeoutSeconds` — bot clients clamp configured values below the outbound and `getUpdates` request guards, but older releases could abort every poll or reply when this was set below those guards.
- `Polling stall detected` in logs means OpenClaw restarts polling and rebuilds the transport after 120 seconds without completed long-poll liveness by default.
- `openclaw channels status --probe` and `openclaw doctor` warn when a running polling account has not completed `getUpdates` after startup grace, a running webhook account has not completed `setWebhook` after startup grace, or the last successful polling transport activity is stale.
- Raise `channels.telegram.pollingStallThresholdMs` only when long-running `getUpdates` calls are healthy but your host still reports false polling-stall restarts. Persistent stalls usually point to proxy, DNS, IPv6, or TLS egress issues to `api.telegram.org`.
- Telegram honors process proxy env for Bot API transport: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and lowercase variants. `NO_PROXY` / `no_proxy` can still bypass `api.telegram.org`.
- If `OPENCLAW_PROXY_URL` is set for a service environment and no standard proxy env is present, Telegram uses that URL for Bot API transport too.
- On VPS hosts with unstable direct egress/TLS, route Telegram API calls through a proxy:
```yaml
channels:
telegram:
proxy: socks5://<user>:<password>@proxy-host:1080
```
- Node 22+ defaults to `autoSelectFamily=true` (except WSL2). Telegram DNS result order honors `OPENCLAW_TELEGRAM_DNS_RESULT_ORDER`, then `channels.telegram.network.dnsResultOrder`, then the process default (for example `NODE_OPTIONS=--dns-result-order=ipv4first`), falling back to `ipv4first` on Node 22+ if none applies.
- On WSL2, or when IPv4-only behavior works better, force family selection:
```yaml
channels:
telegram:
network:
autoSelectFamily: false
```
- RFC 2544 benchmark-range answers (`198.18.0.0/15`) are already allowed for Telegram media downloads by default. If a trusted fake-IP or transparent proxy rewrites `api.telegram.org` to some other private/internal/special-use address during media downloads, opt in to the Telegram-only bypass:
```yaml
channels:
telegram:
network:
dangerouslyAllowPrivateNetwork: true
```
- The same opt-in is available per account at `channels.telegram.accounts.<accountId>.network.dangerouslyAllowPrivateNetwork`.
- If your proxy resolves Telegram media hosts into `198.18.x.x`, leave the dangerous flag off first — that range is already allowed by default.
<Warning>
`channels.telegram.network.dangerouslyAllowPrivateNetwork` weakens Telegram media SSRF protections. Use it only for trusted operator-controlled proxy environments (Clash, Mihomo, Surge fake-IP routing) that synthesize private or special-use answers outside the RFC 2544 benchmark range. Leave it off for normal public internet Telegram access.
</Warning>
- Temporary environment overrides: `OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY=1`, `OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY=1`, `OPENCLAW_TELEGRAM_DNS_RESULT_ORDER=ipv4first`.
- Validate DNS answers:
```bash
dig +short api.telegram.org A
dig +short api.telegram.org AAAA
```
</Accordion>
</AccordionGroup>
More help: [Channel troubleshooting](/channels/troubleshooting).
## Configuration reference
Primary reference: [Configuration reference - Telegram](/gateway/config-channels#telegram).
<Accordion title="High-signal Telegram fields">
- startup/auth: `enabled`, `botToken`, `tokenFile` (must be a regular file; symlinks are rejected), `accounts.*`
- access control: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`, `groups.*.topics.*`, top-level `bindings[]` (`type: "acp"`)
- topic defaults: `groups.<chatId>.topics."*"` applies to unmatched forum topics; exact topic IDs override it
- exec approvals: `execApprovals`, `accounts.*.execApprovals`
- command/menu: `commands.native`, `commands.nativeSkills`, `customCommands`
- threading/replies: `replyToMode`, `threadBindings`
- streaming: `streaming` (modes `off | partial | block | progress`), `streaming.preview.toolProgress`
- formatting/delivery: `textChunkLimit`, `chunkMode`, `richMessages`, `markdown.tables` (`off | bullets | code | block`), `linkPreview`, `responsePrefix`
- media/network: `mediaMaxMb`, `mediaGroupFlushMs`, `timeoutSeconds`, `pollingStallThresholdMs`, `retry`, `network.autoSelectFamily`, `network.dangerouslyAllowPrivateNetwork`, `proxy`
- custom API root: `apiRoot` (Bot API root only; do not include `/bot<TOKEN>`), `trustedLocalFileRoots` (self-hosted Bot API absolute `file_path` roots)
- webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost`, `webhookPort`, `webhookCertPath`
- actions/capabilities: `capabilities.inlineButtons`, `actions.sendMessage|editMessage|deleteMessage|reactions|sticker|createForumTopic|editForumTopic`
- reactions: `reactionNotifications`, `reactionLevel`
- errors: `errorPolicy`, `errorCooldownMs`, `silentErrorReplies`
- writes/history: `configWrites`, `historyLimit`, `dmHistoryLimit`, `dms.*.historyLimit`
</Accordion>
<Note>
Multi-account precedence: with two or more account IDs configured, set `channels.telegram.defaultAccount` (or include `channels.telegram.accounts.default`) to make default routing explicit. Otherwise OpenClaw falls back to the first normalized account ID and `openclaw doctor` warns. Named accounts inherit `channels.telegram.allowFrom` / `groupAllowFrom`, but not `accounts.default.*` values.
</Note>
## Related
<CardGroup cols={2}>
<Card title="Pairing" icon="link" href="/channels/pairing">
Pair a Telegram user to the gateway.
</Card>
<Card title="Groups" icon="users" href="/channels/groups">
Group and topic allowlist behavior.
</Card>
<Card title="Channel routing" icon="route" href="/channels/channel-routing">
Route inbound messages to agents.
</Card>
<Card title="Security" icon="shield" href="/gateway/security">
Threat model and hardening.
</Card>
<Card title="Multi-agent routing" icon="sitemap" href="/concepts/multi-agent">
Map groups and topics to agents.
</Card>
<Card title="Troubleshooting" icon="wrench" href="/channels/troubleshooting">
Cross-channel diagnostics.
</Card>
</CardGroup>

318
docs/channels/tlon.md Normal file
View File

@@ -0,0 +1,318 @@
---
summary: "Tlon/Urbit support status, capabilities, and configuration"
read_when:
- Working on Tlon/Urbit channel features
title: "Tlon"
---
Tlon is a decentralized messenger built on Urbit. OpenClaw connects to your Urbit ship and
responds to DMs and group chat messages. Group replies require an @ mention by default, with
authorization rules and an owner-approval flow layered on top.
Status: bundled plugin. DMs, group mentions, threads, rich text, image upload/download, and an
owner approval system are supported. Reactions and polls are not.
## Bundled plugin
Tlon ships bundled in current OpenClaw releases; packaged builds do not need a separate install.
On an older build or custom install that excludes it, install from npm:
```bash
openclaw plugins install @openclaw/tlon
```
Use the bare package name to track the current release tag. Pin a version (`@openclaw/tlon@x.y.z`)
only for reproducible installs.
From a local checkout:
```bash
openclaw plugins install ./path/to/local/tlon-plugin
```
Details: [Plugins](/tools/plugin)
## Setup
```bash
openclaw channels add --channel tlon --ship ~sampel-palnet --url https://your-ship-host --code lidlut-tabwed-pillex-ridrup
```
Or edit config directly:
```json5
{
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 authorized
},
},
}
```
Restart the gateway after editing config directly. Then DM the bot or @ mention it in a group
channel.
## Private/LAN ships
OpenClaw blocks private/internal hostnames and IP ranges for SSRF protection by default. If your
ship runs on a private network (localhost, LAN IP, internal hostname), opt in explicitly:
```json5
{
channels: {
tlon: {
url: "http://localhost:8080",
network: {
dangerouslyAllowPrivateNetwork: true,
},
},
},
}
```
Applies to targets like `http://localhost:8080`, `http://192.168.x.x:8080`, and
`http://my-ship.local:8080`. Only enable this for a ship URL you trust; it disables SSRF
protection for that account's HTTP requests.
<Note>
`channels.tlon.allowPrivateNetwork` (flat key) is retired. `openclaw doctor --fix` moves it to
`channels.tlon.network.dangerouslyAllowPrivateNetwork` automatically.
</Note>
## Group channels
Pin channels manually, or turn on auto-discovery:
```json5
{
channels: {
tlon: {
groupChannels: ["chat/~host-ship/general", "chat/~host-ship/support"],
autoDiscoverChannels: true,
},
},
}
```
`autoDiscoverChannels` defaults to `false` when unset in config; the setup wizard defaults the
prompt to yes and writes `true` explicitly. With it on, OpenClaw scries joined groups on startup,
watches new channels as group invites are accepted, and rechecks every 2 minutes.
## Access control
DM allowlist (empty = no DMs allowed unless the sender is `ownerShip`):
```json5
{
channels: {
tlon: {
dmAllowlist: ["~zod", "~nec"],
},
},
}
```
Group authorization defaults to `restricted` per channel. Set `defaultAuthorizedShips` for a
baseline, and override per channel nest:
```json5
{
channels: {
tlon: {
defaultAuthorizedShips: ["~zod"],
authorization: {
channelRules: {
"chat/~host-ship/general": {
mode: "restricted",
allowedShips: ["~zod", "~nec"],
},
"chat/~host-ship/announcements": {
mode: "open",
},
},
},
},
},
}
```
Once the bot has replied inside a thread, it keeps responding to later messages in that thread
without requiring another mention.
## Owner and approval system
```json5
{
channels: {
tlon: {
ownerShip: "~your-main-ship",
},
},
}
```
The owner ship is authorized everywhere: DM invites are always auto-accepted, group invites are
always auto-accepted, and channel messages always pass authorization. The owner does not need to
be in `dmAllowlist`, `defaultAuthorizedShips`, or `groupInviteAllowlist`.
When `ownerShip` is set, unauthorized requests do not just get dropped — they queue a pending
approval and DM the owner:
- DM requests from ships not on `dmAllowlist`
- Mentions in channels where the sender fails authorization
- Group invites from ships not on `groupInviteAllowlist` (when auto-accept is off, or on but the
inviter is not allowlisted)
The owner replies in DM to act on a request:
| Owner reply | Effect |
| ---------------------------- | ---------------------------------------------------- |
| `approve` / `deny` / `block` | Acts on the most recent pending approval |
| `approve <id>` / `deny <id>` | Acts on a specific approval by id |
| `block` | Also blocks the ship natively so it cannot reconnect |
| `unblock ~ship` | Reverses a native block |
| `blocked` | Lists currently blocked ships |
| `pending` | Lists pending approval requests |
Without `ownerShip` configured, unauthorized DMs and channel mentions are just dropped and logged;
there is no approval prompt.
## Auto-accept settings
Auto-accept DM invites from ships already on `dmAllowlist` (the owner is always auto-accepted
regardless of this flag):
```json5
{
channels: {
tlon: {
autoAcceptDmInvites: true,
},
},
}
```
Auto-accept group invites from an allowlist (fails closed: with `autoAcceptGroupInvites: true` and
an empty `groupInviteAllowlist`, no non-owner invite is accepted):
```json5
{
channels: {
tlon: {
autoAcceptGroupInvites: true,
groupInviteAllowlist: ["~zod"],
},
},
}
```
## Hot-reload via Urbit settings store
Most of the settings above (`dmAllowlist`, `groupInviteAllowlist`, `groupChannels`,
`defaultAuthorizedShips`, `autoDiscoverChannels`, `autoAcceptDmInvites`,
`autoAcceptGroupInvites`, `ownerShip`, `showModelSignature`) are mirrored into the ship's
`%settings` agent (desk `moltbot`, bucket `tlon`) on first run and then read live from there,
so changes made via a Landscape client or the bundled skill's settings commands apply without a
gateway restart. `channelRules` and pending approvals are also persisted there as JSON. File
config stays the source of truth for values never written to the settings store.
## Delivery targets (CLI/cron)
Use 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 plugin bundles [`@tloncorp/tlon-skill`](https://github.com/tloncorp/tlon-skill), a CLI for
direct Urbit operations, available automatically once the plugin is installed:
- **Activity**: mentions, replies, unreads
- **Channels**: list, create, rename
- **Contacts**: list/get/update profiles
- **Groups**: create, join, invite/request flows, roles
- **Hooks**: manage channel hooks
- **Messages**: history, search
- **DMs**: send, react, accept/decline
- **Posts**: react, delete
- **Notebook**: post to diary channels
- **Settings**: hot-reload plugin config via the settings store above
## Capabilities
| Feature | Status |
| --------------- | --------------------------------------------- |
| Direct messages | Supported |
| Groups/channels | Supported (mention-gated by default) |
| Threads | Supported (keeps replying once it has joined) |
| Rich text | Markdown converted to Tlon's native format |
| Images | Downloaded inbound, uploaded outbound |
| Reactions | Only via the [bundled skill](#bundled-skill) |
| Polls | Not supported |
| Native commands | Owner-only by default |
## Troubleshooting
```bash
openclaw status
openclaw gateway status
openclaw logs --follow
openclaw doctor
```
Common failures:
- **DMs ignored**: sender not in `dmAllowlist` and no `ownerShip` configured for the approval flow.
- **Group messages ignored**: channel not discovered/pinned, or sender fails authorization with no
`ownerShip` to queue an approval.
- **Connection errors**: check the ship URL is reachable; set
`network.dangerouslyAllowPrivateNetwork` for local ships.
- **Auth errors**: login codes rotate — copy the current code from your ship.
## Configuration reference
Full configuration: [Configuration](/gateway/configuration)
| Key | Meaning |
| ------------------------------------------------------ | -------------------------------------------------------------- |
| `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.network.dangerouslyAllowPrivateNetwork` | Allow localhost/LAN ship URLs (SSRF opt-in). |
| `channels.tlon.ownerShip` | Owner ship: always authorized, receives approval requests. |
| `channels.tlon.dmAllowlist` | Ships allowed to DM (empty = none besides owner). |
| `channels.tlon.autoAcceptDmInvites` | Auto-accept DMs from ships in `dmAllowlist`. |
| `channels.tlon.autoAcceptGroupInvites` | Auto-accept group invites from `groupInviteAllowlist`. |
| `channels.tlon.groupInviteAllowlist` | Ships whose group invites are auto-accepted. |
| `channels.tlon.autoDiscoverChannels` | Auto-discover joined group channels (default: `false`). |
| `channels.tlon.groupChannels` | Manually pinned channel nests. |
| `channels.tlon.defaultAuthorizedShips` | Ships authorized for all channels (used when no rule matches). |
| `channels.tlon.authorization.channelRules` | Per-channel-nest auth mode + allowlist. |
| `channels.tlon.showModelSignature` | Append `_[Generated by <model>]_` to replies. |
| `channels.tlon.responsePrefix` | Static prefix prepended to outbound replies. |
| `channels.tlon.accounts.<id>` | Additional named accounts (multi-ship setups). |
## Notes
- Group replies need an @ mention (e.g. `~your-bot-ship`) unless the bot already joined that thread.
- Thread replies land in-thread; the bot also gets the last 10 messages of thread context prepended
for the agent.
- Rich text (bold, italic, code, headers, lists) converts to Tlon's native format.
- Sending an inbound message that asks for a channel summary (for example "summarize this
channel") triggers a built-in history summarization instead of the normal reply flow.
## Related
- [Channels Overview](/channels) — all supported channels
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) — session routing for messages
- [Security](/gateway/security) — access model and hardening

View File

@@ -0,0 +1,159 @@
---
summary: "Fast channel level troubleshooting with per channel failure signatures and fixes"
read_when:
- Channel transport says connected but replies fail
- You need channel specific checks before deep provider docs
title: "Channel troubleshooting"
---
Use this page when a channel connects but behavior is wrong.
## Command ladder
Run these in order first:
```bash
openclaw status
openclaw gateway status
openclaw logs --follow
openclaw doctor
openclaw channels status --probe
```
Healthy baseline:
- `Runtime: running`
- `Connectivity probe: ok`
- `Capability: read-only`, `write-capable`, or `admin-capable`
- Channel probe shows transport connected and, where supported, `works` or `audit ok`
## After an update
Use this when Telegram, iMessage, BlueBubbles-era configs, or another plugin channel disappears
after updating.
```bash
openclaw status --all
openclaw doctor --fix
openclaw gateway restart
openclaw status --all
```
Look for `plugin load failed: dependency tree corrupted; run openclaw doctor --fix` in `openclaw
status --all`. That means the channel is configured, but plugin setup/load hit a corrupted
dependency tree instead of registering the channel. `openclaw doctor --fix` clears stale
plugin-runtime dependency symlinks and stale auth shadows, then `openclaw gateway restart` reloads
clean state.
## 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. |
| QR login times out with 408 | Check gateway `HTTPS_PROXY` / `HTTP_PROXY` env | Set a reachable proxy; use `NO_PROXY` only for bypasses. |
| Random disconnect/relogin loops | `openclaw channels status --probe` + logs | Recent reconnects are flagged even when currently connected; watch logs, restart the gateway, then relink if flapping continues. |
| `status=408 Request Time-out` loop | Probe, logs, doctor, then gateway status | Fix host connectivity/timing first; back up auth and re-link the account if the loop persists. |
| Replies arrive seconds/minutes late | `openclaw doctor --fix` | Doctor stops verified stale local TUI clients when they are degrading the Gateway event loop. |
Full troubleshooting: [WhatsApp troubleshooting](/channels/whatsapp#troubleshooting)
## 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`. |
| Startup reports `getMe returned 401` | Check configured token source | Re-copy or regenerate the BotFather token and update `botToken`, `tokenFile`, or default-account `TELEGRAM_BOT_TOKEN`. |
| Polling stalls or reconnects slowly | `openclaw logs --follow` for polling diagnostics | Upgrade; if restarts are false positives, tune `pollingStallThresholdMs`. Persistent stalls still point to proxy/DNS/IPv6. |
| `setMyCommands` rejected at startup | Inspect logs for `BOT_COMMANDS_TOO_MUCH` | Reduce plugin/skill/custom Telegram commands or disable native menus. |
| Upgraded and allowlist blocks you | `openclaw security audit` and config allowlists | Run `openclaw doctor --fix` or replace `@username` with numeric sender IDs. |
Full troubleshooting: [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`. |
| Typing/token usage but no Discord message | Check whether this is an ambient room event or an opted-in `message_tool` room where the model missed `message(action=send)` | Inspect the gateway verbose log for suppressed final payload metadata, verify `messages.groupChat.unmentionedInbound`, read [Ambient room events](/channels/ambient-room-events), or keep `messages.groupChat.visibleReplies: "automatic"` for normal group requests. |
| DM replies missing | `openclaw pairing list discord` | Approve DM pairing or adjust DM policy. |
Full troubleshooting: [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; watch for `botTokenStatus` / `appTokenStatus = configured_unavailable` on SecretRef-backed setups. |
| 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: [Slack troubleshooting](/channels/slack#troubleshooting)
## iMessage
### iMessage failure signatures
| Symptom | Fastest check | Fix |
| ------------------------------------ | ------------------------------------------------------- | --------------------------------------------------------------------- |
| `imsg` missing or fails on non-macOS | `openclaw channels status --probe --channel imessage` | Run OpenClaw on the Messages Mac or use an SSH wrapper for `cliPath`. |
| 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` | Approve pairing or update allowlist. |
Full troubleshooting: [iMessage troubleshooting](/channels/imessage#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: [Signal troubleshooting](/channels/signal#troubleshooting)
## QQ Bot
### QQ Bot failure signatures
| Symptom | Fastest check | Fix |
| ------------------------------- | ------------------------------------------- | --------------------------------------------------------------- |
| Bot replies "gone to Mars" | Verify `appId` and `clientSecret` in config | Set credentials or restart the gateway. |
| No inbound messages | `openclaw channels status --probe` | Verify credentials on the QQ Open Platform. |
| Voice not transcribed | Check STT provider config | Configure `channels.qqbot.stt` or `tools.media.audio`. |
| Proactive messages not arriving | Check QQ platform interaction requirements | QQ may block bot-initiated messages without recent interaction. |
Full troubleshooting: [QQ Bot troubleshooting](/channels/qqbot#troubleshooting)
## Matrix
### Matrix failure signatures
| Symptom | Fastest check | Fix |
| ----------------------------------- | -------------------------------------- | ------------------------------------------------------------------------- |
| Logged in but ignores room messages | `openclaw channels status --probe` | Check `groupPolicy`, room allowlist, and mention gating. |
| DMs do not process | `openclaw pairing list matrix` | Approve sender or adjust DM policy. |
| Encrypted rooms fail | `openclaw matrix verify status` | Re-verify the device, then check `openclaw matrix verify backup status`. |
| Backup restore is pending/broken | `openclaw matrix verify backup status` | Run `openclaw matrix verify backup restore` or rerun with a recovery key. |
| Cross-signing/bootstrap looks wrong | `openclaw matrix verify bootstrap` | Repair secret storage, cross-signing, and backup state in one pass. |
Full setup and config: [Matrix](/channels/matrix)
## Related
- [Pairing](/channels/pairing)
- [Channel routing](/channels/channel-routing)
- [Gateway troubleshooting](/gateway/troubleshooting)

369
docs/channels/twitch.md Normal file
View File

@@ -0,0 +1,369 @@
---
summary: "Twitch chat bot: install, credentials, access control, token refresh"
read_when:
- Setting up Twitch chat integration for OpenClaw
title: "Twitch"
sidebarTitle: "Twitch"
---
Twitch chat support over Twitch's chat (IRC) interface via the Twurple client. OpenClaw signs in as a Twitch bot account, joins one channel per configured account, and replies in that channel.
## Install
Twitch ships as an official plugin; it is not part of the core install.
<Tabs>
<Tab title="npm registry">
```bash
openclaw plugins install @openclaw/twitch
```
</Tab>
<Tab title="Local checkout">
```bash
openclaw plugins install ./path/to/local/twitch-plugin
```
</Tab>
</Tabs>
`plugins install` registers and enables the plugin. Picking Twitch during `openclaw onboard` or `openclaw channels add` installs it on demand. Use the bare package name to follow the current release; pin an exact version only for reproducible installs. Requires OpenClaw 2026.4.10 or newer.
Details: [Plugins](/tools/plugin)
## Quick setup
<Steps>
<Step title="Install the plugin">
See [Install](#install) above.
</Step>
<Step title="Create a Twitch bot account">
Create a dedicated Twitch account for the bot (or use an existing account).
</Step>
<Step title="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**
</Step>
<Step title="Find your Twitch user ID">
Use [https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/](https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/) to convert a username to a Twitch user ID.
</Step>
<Step title="Configure the token">
- Env: `OPENCLAW_TWITCH_ACCESS_TOKEN=...` (default account only)
- Or config: `channels.twitch.accessToken`
If both are set, config takes precedence (the env var is only a fallback for the default account).
</Step>
<Step title="Start the gateway">
```bash
openclaw gateway run
```
</Step>
</Steps>
<Warning>
Add access control (`allowFrom` or `allowedRoles`) to prevent unauthorized users from triggering the bot. `requireMention` defaults to `true`.
</Warning>
Minimal config:
```json5
{
channels: {
twitch: {
enabled: true,
username: "openclaw", // Bot's Twitch account (authenticates)
accessToken: "oauth:abc123...", // OAuth access token (or use OPENCLAW_TWITCH_ACCESS_TOKEN env var)
clientId: "xyz789...", // Client ID from Token Generator
channel: "yourchannel", // Which Twitch channel's chat to join (required)
allowFrom: ["123456789"], // (recommended) Your Twitch user ID only
},
},
}
```
## What it is
- A Twitch channel owned by the Gateway.
- Deterministic routing: replies always go back to the Twitch channel the message came from.
- Each joined channel maps to an isolated group session key `agent:<agentId>:twitch:group:<channel>`.
- `username` is the bot's account (who authenticates), `channel` is which chat room to join. One account entry joins exactly one channel.
- Tokens work with or without the `oauth:` prefix; OpenClaw normalizes both ways (the setup wizard expects the `oauth:` form).
## Token refresh (optional)
Tokens from [Twitch Token Generator](https://twitchtokengenerator.com/) cannot be refreshed by OpenClaw - regenerate when expired (they last a few hours; no app registration needed).
For automatic refresh, create your own app at the [Twitch Developer Console](https://dev.twitch.tv/console) and add:
```json5
{
channels: {
twitch: {
clientSecret: "your_client_secret",
refreshToken: "your_refresh_token",
},
},
}
```
With both set, the plugin uses a refreshing auth provider that renews tokens before expiration and logs each refresh. Without `refreshToken` it logs `token refresh disabled (no refresh token)`; without `clientSecret` it falls back to a static (non-refreshing) token.
## Multi-account support
Use `channels.twitch.accounts` with per-account credentials. See [Configuration](/gateway/configuration) for the shared pattern.
Example (one bot account in two channels):
```json5
{
channels: {
twitch: {
accounts: {
channel1: {
username: "openclaw",
accessToken: "oauth:abc123...",
clientId: "xyz789...",
channel: "yourchannel",
},
channel2: {
username: "openclaw",
accessToken: "oauth:def456...",
clientId: "uvw012...",
channel: "secondchannel",
},
},
},
},
}
```
<Note>
Every account entry needs its own `accessToken` (the env var covers only the default account). An account joins exactly one channel, so joining two channels means two accounts. `channels.twitch.defaultAccount` picks which account is the default.
</Note>
## Access control
`allowFrom` is a hard allowlist of Twitch user IDs. When it is set, `allowedRoles` is ignored; leave `allowFrom` unset to use role-based access instead.
**Available roles:** `"moderator"`, `"owner"`, `"vip"`, `"subscriber"`, `"all"`.
<Tabs>
<Tab title="User ID allowlist (most secure)">
```json5
{
channels: {
twitch: {
accounts: {
default: {
allowFrom: ["123456789", "987654321"],
},
},
},
},
}
```
</Tab>
<Tab title="Role-based">
```json5
{
channels: {
twitch: {
accounts: {
default: {
allowedRoles: ["moderator", "vip"],
},
},
},
},
}
```
</Tab>
<Tab title="Disable @mention requirement">
By default, `requireMention` is `true`. To respond to all allowed messages:
```json5
{
channels: {
twitch: {
accounts: {
default: {
requireMention: false,
},
},
},
},
}
```
</Tab>
</Tabs>
<Note>
**Why user IDs?** Usernames can change, allowing impersonation. User IDs are permanent.
Find yours with the [username to ID converter](https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/).
</Note>
## Troubleshooting
First, run diagnostic commands:
```bash
openclaw doctor
openclaw channels status --probe
```
<AccordionGroup>
<Accordion title="Bot does not 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 mention gate:** With `requireMention: true` (default), messages must @mention the bot username.
- **Check the bot is in the channel:** The bot only joins the channel named in `channel`.
</Accordion>
<Accordion title="Token issues">
"Failed to connect" or authentication errors:
- Verify `accessToken` is the OAuth access token value (the `oauth:` prefix is optional)
- Check the token has `chat:read` and `chat:write` scopes
- If using token refresh, verify `clientSecret` and `refreshToken` are set
</Accordion>
<Accordion title="Token refresh not working">
Check logs for refresh events:
```text
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
</Accordion>
</AccordionGroup>
## Config
### Account config
<ParamField path="username" type="string" required>
Bot username (the authenticating account).
</ParamField>
<ParamField path="accessToken" type="string" required>
OAuth access token with `chat:read` and `chat:write` (config or env for the default account).
</ParamField>
<ParamField path="clientId" type="string" required>
Twitch Client ID (from Token Generator or your app). Optional in the schema but required to connect.
</ParamField>
<ParamField path="channel" type="string" required>
Channel to join.
</ParamField>
<ParamField path="enabled" type="boolean" default="true">
Enable this account.
</ParamField>
<ParamField path="clientSecret" type="string">
Optional: for automatic token refresh.
</ParamField>
<ParamField path="refreshToken" type="string">
Optional: for automatic token refresh.
</ParamField>
<ParamField path="expiresIn" type="number">
Token expiry in seconds (refresh tracking).
</ParamField>
<ParamField path="obtainmentTimestamp" type="number">
Timestamp when the token was obtained (refresh tracking).
</ParamField>
<ParamField path="allowFrom" type="string[]">
User ID allowlist. When set, roles are ignored.
</ParamField>
<ParamField path="allowedRoles" type='Array<"moderator" | "owner" | "vip" | "subscriber" | "all">'>
Role-based access control.
</ParamField>
<ParamField path="requireMention" type="boolean" default="true">
Require @mention to trigger the bot.
</ParamField>
<ParamField path="responsePrefix" type="string">
Outbound response prefix override for this account.
</ParamField>
### Provider options
- `channels.twitch.enabled` - Enable/disable channel startup
- `channels.twitch.username` / `accessToken` / `clientId` / `channel` - Simplified single-account config (implicit `default` account; takes precedence over `accounts.default`)
- `channels.twitch.accounts.<accountName>` - Multi-account config (all account fields above)
- `channels.twitch.defaultAccount` - Which account name is the default
- `channels.twitch.markdown.tables` - Markdown table rendering mode (`off` | `bullets` | `code` | `block`)
Full example:
```json5
{
channels: {
twitch: {
enabled: true,
username: "openclaw",
accessToken: "oauth:abc123...",
clientId: "xyz789...",
channel: "yourchannel",
clientSecret: "secret123...",
refreshToken: "refresh456...",
allowFrom: ["123456789"],
accounts: {
second: {
username: "mybot",
accessToken: "oauth:def456...",
clientId: "uvw012...",
channel: "your_channel",
enabled: true,
expiresIn: 14400,
obtainmentTimestamp: 1706092800000,
allowedRoles: ["moderator"],
},
},
},
},
}
```
## Tool actions
The agent can send Twitch messages through the message tool `send` action:
```json5
{
channel: "twitch",
action: "send",
to: "#mychannel",
message: "Hello Twitch!",
}
```
`to` is optional and defaults to the account's configured `channel`.
## Safety and 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; longer replies are chunked at word boundaries.
- Markdown is stripped before sending (Twitch chat is plain text; newlines become spaces).
- OpenClaw adds no rate limiting of its own; the Twurple chat client handles Twitch rate limits.
## Related
- [Channel Routing](/channels/channel-routing) — session routing for messages
- [Channels Overview](/channels) — all supported channels
- [Groups](/channels/groups) — group chat behavior and mention gating
- [Pairing](/channels/pairing) — DM authentication and pairing flow
- [Security](/gateway/security) — access model and hardening

173
docs/channels/wechat.md Normal file
View File

@@ -0,0 +1,173 @@
---
summary: "WeChat channel setup through the external openclaw-weixin plugin"
read_when:
- You want to connect OpenClaw to WeChat or Weixin
- You are installing or troubleshooting the openclaw-weixin channel plugin
- You need to understand how external channel plugins run beside the Gateway
title: "WeChat"
---
OpenClaw connects to WeChat through Tencent's external
`@tencent-weixin/openclaw-weixin` channel plugin.
Status: external plugin, maintained by the Tencent Weixin team. Direct chats and
media are supported. Group chats are not advertised by the plugin capability
metadata (it declares direct chats only).
## Naming
- **WeChat** is the user-facing name in these docs.
- **Weixin** is the name used by Tencent's package and by the plugin id.
- `openclaw-weixin` is the OpenClaw channel id (`weixin` and `wechat` work as aliases).
- `@tencent-weixin/openclaw-weixin` is the npm package.
Use `openclaw-weixin` in CLI commands and config paths.
## How it works
The WeChat code does not live in the OpenClaw core repo. OpenClaw provides the
generic channel plugin contract, and the external plugin provides the
WeChat-specific runtime:
1. `openclaw plugins install` installs `@tencent-weixin/openclaw-weixin`.
2. The Gateway discovers the plugin manifest and loads the plugin entrypoint.
3. The plugin registers channel id `openclaw-weixin`.
4. `openclaw channels login --channel openclaw-weixin` starts QR login.
5. The plugin stores account credentials under the OpenClaw state directory
(`~/.openclaw` by default).
6. When the Gateway starts, the plugin starts its Weixin monitor for each
configured account.
7. Inbound WeChat messages are normalized through the channel contract, routed to
the selected OpenClaw agent, and sent back through the plugin outbound path.
That separation matters: OpenClaw core stays channel-agnostic. WeChat login,
Tencent iLink API calls, media upload/download, context tokens, and account
monitoring are owned by the external plugin.
## Install
Quick install:
```bash
npx -y @tencent-weixin/openclaw-weixin-cli install
```
Manual install:
```bash
openclaw plugins install "@tencent-weixin/openclaw-weixin"
openclaw config set plugins.entries.openclaw-weixin.enabled true
```
Restart the Gateway after install:
```bash
openclaw gateway restart
```
## Login
Run QR login on the same machine that runs the Gateway:
```bash
openclaw channels login --channel openclaw-weixin
```
Scan the QR code with WeChat on your phone and confirm the login. The plugin saves
the account token locally after a successful scan.
To add another WeChat account, run the same login command again. For multiple
accounts, isolate direct-message sessions by account, channel, and sender:
```bash
openclaw config set session.dmScope per-account-channel-peer
```
## Access control
Direct messages use the normal OpenClaw pairing and allowlist model for channel
plugins.
Approve new senders:
```bash
openclaw pairing list openclaw-weixin
openclaw pairing approve openclaw-weixin <CODE>
```
For the full access-control model, see [Pairing](/channels/pairing).
## Compatibility
The plugin checks the host OpenClaw version at startup.
| Plugin line | OpenClaw version | npm tag |
| ----------- | --------------------------------------------------------------- | -------- |
| `2.x` | `>=2026.5.12` (current 2.4.6; early 2.x accepted `>=2026.3.22`) | `latest` |
| `1.x` | `>=2026.1.0 <2026.3.22` | `legacy` |
If the plugin reports that your OpenClaw version is too old, either update
OpenClaw or install the legacy plugin line:
```bash
openclaw plugins install @tencent-weixin/openclaw-weixin@legacy
```
## Sidecar process
The WeChat plugin can run helper work beside the Gateway while it monitors the
Tencent iLink API. In issue #68451, that helper path exposed a bug in OpenClaw's
generic stale-Gateway cleanup: a child process could try to clean up the parent
Gateway process, causing restart loops under process managers such as systemd.
Current OpenClaw startup cleanup excludes the current process and its ancestors,
so a channel helper cannot kill the Gateway that launched it. This fix is
generic; it is not a WeChat-specific path in core.
## Troubleshooting
Check install and status:
```bash
openclaw plugins list
openclaw channels status --probe
openclaw --version
```
If the channel shows as installed but does not connect, confirm that the plugin is
enabled and restart:
```bash
openclaw config set plugins.entries.openclaw-weixin.enabled true
openclaw gateway restart
```
If the Gateway restarts repeatedly after enabling WeChat, update both OpenClaw and
the plugin:
```bash
npm view @tencent-weixin/openclaw-weixin version
openclaw plugins install "@tencent-weixin/openclaw-weixin" --force
openclaw gateway restart
```
If startup reports that the installed plugin package `requires compiled runtime
output for TypeScript entry`, the npm package was published without the compiled
JavaScript runtime files OpenClaw needs. Update/reinstall after the plugin
publisher ships a fixed package, or temporarily disable/uninstall the plugin.
Temporary disable:
```bash
openclaw config set plugins.entries.openclaw-weixin.enabled false
openclaw gateway restart
```
## Related docs
- Channel overview: [Chat Channels](/channels)
- Pairing: [Pairing](/channels/pairing)
- Channel routing: [Channel Routing](/channels/channel-routing)
- Plugin architecture: [Plugin Architecture](/plugins/architecture)
- Channel plugin SDK: [Channel Plugin SDK](/plugins/sdk-channel-plugins)
- External package: [@tencent-weixin/openclaw-weixin](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin)

693
docs/channels/whatsapp.md Normal file
View File

@@ -0,0 +1,693 @@
---
summary: "WhatsApp channel support, access controls, delivery behavior, and operations"
read_when:
- Working on WhatsApp/web channel behavior or inbox routing
title: "WhatsApp"
---
Status: production-ready via WhatsApp Web (Baileys). The gateway owns the linked session(s); there is no separate Twilio WhatsApp channel.
## Install
`openclaw onboard` and `openclaw channels add --channel whatsapp` prompt to install the plugin the first time you select it; `openclaw channels login --channel whatsapp` offers the same install flow if the plugin is missing. Dev checkouts use the local plugin path; stable/beta installs `@openclaw/whatsapp` from ClawHub first, falling back to npm. The WhatsApp runtime ships outside the core OpenClaw npm package, so its runtime dependencies stay with the external plugin. Manual install:
```bash
openclaw plugins install clawhub:@openclaw/whatsapp
```
Use the bare npm package (`@openclaw/whatsapp`) only for the registry fallback; pin an exact version only for a reproducible install.
<CardGroup cols={3}>
<Card title="Pairing" icon="link" href="/channels/pairing">
Default DM policy is pairing for unknown senders.
</Card>
<Card title="Channel troubleshooting" icon="wrench" href="/channels/troubleshooting">
Cross-channel diagnostics and repair playbooks.
</Card>
<Card title="Gateway configuration" icon="settings" href="/gateway/configuration">
Full channel config patterns and examples.
</Card>
</CardGroup>
## Quick setup
<Steps>
<Step title="Configure access policy">
```json5
{
channels: {
whatsapp: {
dmPolicy: "pairing",
allowFrom: ["+15551234567"],
groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"],
},
},
}
```
</Step>
<Step title="Link WhatsApp (QR)">
```bash
openclaw channels login --channel whatsapp
```
Login is QR-only. On remote or headless hosts, have a reliable path to deliver the live QR to the phone before starting login; terminal-rendered QRs, screenshots, or chat attachments can expire in transit.
For a specific account:
```bash
openclaw channels login --channel whatsapp --account work
```
To attach an existing/custom auth directory before login:
```bash
openclaw channels add --channel whatsapp --account work --auth-dir /path/to/wa-auth
openclaw channels login --channel whatsapp --account work
```
</Step>
<Step title="Start the gateway">
```bash
openclaw gateway
```
</Step>
<Step title="Approve the first pairing request (pairing mode)">
```bash
openclaw pairing list whatsapp
openclaw pairing approve whatsapp <CODE>
```
Pairing requests expire after 1 hour; pending requests are capped at 3 per account.
</Step>
</Steps>
<Note>
A separate WhatsApp number is recommended (setup and metadata are optimized for it), but personal-number/self-chat setups are fully supported.
</Note>
## Deployment patterns
<AccordionGroup>
<Accordion title="Dedicated number (recommended)">
- separate WhatsApp identity for OpenClaw
- clearer DM allowlists and routing boundaries
- lower chance of self-chat confusion
```json5
{
channels: {
whatsapp: {
dmPolicy: "allowlist",
allowFrom: ["+15551234567"],
},
},
}
```
</Accordion>
<Accordion title="Personal-number fallback">
Onboarding supports personal-number mode and writes a self-chat-friendly baseline: `dmPolicy: "allowlist"`, `allowFrom` including your own number, `selfChatMode: true`. Runtime self-chat protections key off the linked self number plus `allowFrom`.
</Accordion>
</AccordionGroup>
## Runtime model
- The gateway owns the WhatsApp socket and reconnect loop.
- A watchdog tracks two signals independently: raw WhatsApp Web transport activity and application-message activity. A quiet-but-connected session is not restarted just because no message arrived recently; it forces reconnect only when transport frames stop arriving for a fixed internal window (not user-configurable) or application messages stay silent past 4x the normal message timeout. Right after a reconnect for a recently active session, that first window uses the shorter normal message timeout instead of the 4x window.
- Baileys socket timings are explicit under `web.whatsapp.*`: `keepAliveIntervalMs` (application ping interval), `connectTimeoutMs` (opening handshake timeout), `defaultQueryTimeoutMs` (Baileys query waits, plus OpenClaw's outbound send/presence and inbound read-receipt timeouts).
- Outbound sends require an active WhatsApp listener for the target account; sends fail fast otherwise.
- Group sends attach native mention metadata for `@+<digits>` and `@<digits>` tokens (in text and media captions) when the token matches current participant metadata, including LID-backed groups.
- Status and broadcast chats (`@status`, `@broadcast`) are ignored.
- Direct chats use DM session rules (`session.dmScope`; default `main` collapses DMs into the agent main session). Group sessions are isolated per JID (`agent:<agentId>:whatsapp:group:<jid>`).
- WhatsApp Channels/Newsletters can be explicit outbound targets via their native `@newsletter` JID, using channel session metadata (`agent:<agentId>:whatsapp:channel:<jid>`) rather than DM semantics.
- WhatsApp Web transport honors standard proxy environment variables on the gateway host (`HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY`, lowercase variants). Prefer host-level proxy config over per-channel settings.
- With `messages.removeAckAfterReply` enabled, OpenClaw clears the ack reaction once a visible reply is delivered.
## Call the current requester with MeowCaller (experimental)
The plugin can expose `whatsapp_call` in WhatsApp-originated agent turns. It uses [MeowCaller](https://github.com/purpshell/meowcaller) to place a WhatsApp voice call to the current authorized requester and play an OpenClaw TTS message after they answer. The tool has no destination-number parameter, so a prompt cannot redirect the call. Disabled by default.
<Warning>
MeowCaller is experimental, has no tagged release, and uses a separately paired whatsmeow linked-device session — it cannot reuse the plugin's Baileys credentials. Pairing adds another linked device to the same WhatsApp account; scan with the identity used by OpenClaw. Personal-number/self-chat mode cannot call itself; use a dedicated OpenClaw number to call your personal number.
</Warning>
<Steps>
<Step title="Enable experimental calls">
Add `actions.calls: true` to the WhatsApp channel config and restart the gateway:
```json
{
"channels": {
"whatsapp": {
"actions": {
"calls": true
}
}
}
}
```
When absent or `false`, OpenClaw does not expose the `whatsapp_call` tool.
</Step>
<Step title="Install the reviewed MeowCaller CLI">
The adapter expects a `meowcaller` executable on the gateway host's `PATH`. Until [MeowCaller PR #7](https://github.com/purpshell/meowcaller/pull/7) merges, build the reviewed branch:
```bash
git clone --branch feat/send-only-notify https://github.com/steipete/meowcaller.git
cd meowcaller
git checkout 752050471fc2bf7a8cdfbf7dbd3cd4e865d85d3f
mkdir -p "$HOME/.local/bin"
go build -o "$HOME/.local/bin/meowcaller" ./cmd/meowcaller
```
Ensure `$HOME/.local/bin` is on the gateway service's `PATH`. This revision has explicit `pair` and send-only `notify` commands; `notify` opens no microphone, speaker, video device, or diagnostic capture. Do not substitute the upstream example CLI's `play` command.
</Step>
<Step title="Pair the MeowCaller linked device">
Ask the WhatsApp agent to check call setup (`whatsapp_call` status action reports the account-specific state directory and pairing command). For the default account:
```bash
state_dir="$HOME/.openclaw/credentials/whatsapp-calls/default"
mkdir -p "$state_dir"
chmod 700 "$state_dir"
meowcaller pair --store "$state_dir/wa-voip.db"
```
Run this interactively, scan the QR from **WhatsApp > Linked devices**, and wait for `MeowCaller linked device ready`. Keep `wa-voip.db` private — it is the MeowCaller session. Non-default accounts get their own store path from the status action; on Windows, run its PowerShell command.
</Step>
<Step title="Configure TTS and call from WhatsApp">
Configure a telephony-capable [TTS provider](/tools/tts), restart the gateway, then send a request such as `Call me and say the build finished.` The tool resolves the sender from trusted inbound context, synthesizes a temporary private WAV file, runs MeowCaller for a bounded call window, and deletes the audio file afterward. OpenClaw passes the account's store explicitly, waits for a zero exit status after answer/playback/hangup, and treats a timeout or nonzero exit as a failed tool call.
</Step>
</Steps>
Limits: one-to-one outbound audio calls only, no arbitrary destination numbers, no shared auth with the chat connection, no self-calls from personal-number/self-chat mode, synthesized audio capped at 60 seconds, no handset-side audibility receipt beyond MeowCaller's answer/playback/hangup completion, and OpenClaw stops the companion process after a bounded 115-175 second window (covering MeowCaller's connection, answer, playback, and shutdown phases).
## Approval prompts
WhatsApp can render exec and plugin approval prompts as `👍`/`👎` reactions, controlled by the top-level approval forwarding config:
```json5
{
approvals: {
exec: {
enabled: true,
mode: "session",
},
plugin: {
enabled: true,
mode: "targets",
targets: [{ channel: "whatsapp", to: "+15551234567" }],
},
},
}
```
`approvals.exec` and `approvals.plugin` are independent; enabling WhatsApp as a channel only links the transport and sends nothing unless the matching approval family is enabled and routed there. Session mode delivers native emoji approvals only for approvals that originate from WhatsApp. Target mode uses the shared forwarding pipeline for explicit targets and does not create separate approver-DM fanout.
WhatsApp approval reactions require explicit approvers in `allowFrom` (or `"*"`). `defaultTo` sets ordinary default message targets, not an approver list. Manual `/approve` commands still pass the normal WhatsApp sender-authorization path before approval resolution.
## Plugin hooks and privacy
Inbound WhatsApp messages can carry personal content, phone numbers, group identifiers, sender names, and session correlation fields. WhatsApp does not broadcast inbound `message_received` hook payloads to plugins unless you opt in:
```json5
{
channels: {
whatsapp: {
pluginHooks: {
messageReceived: true,
},
},
},
}
```
Scope the opt-in to one account under `channels.whatsapp.accounts.<id>.pluginHooks.messageReceived`. Only enable this for plugins you trust with inbound WhatsApp content and identifiers.
## Access control and activation
<Tabs>
<Tab title="DM policy">
`channels.whatsapp.dmPolicy`:
| Value | Behavior |
| --- | --- |
| `pairing` (default) | Unknown senders request pairing; owner approves |
| `allowlist` | Only `allowFrom` senders admitted |
| `open` | Requires `allowFrom` to include `"*"` |
| `disabled` | Block all DMs |
`allowFrom` accepts E.164-style numbers (normalized internally). It is a DM sender access-control list only — it does not gate explicit outbound sends to group JIDs or `@newsletter` channel JIDs.
Multi-account override: `channels.whatsapp.accounts.<id>.dmPolicy` (and `.allowFrom`) take precedence over channel-level defaults for that account.
Runtime notes:
- pairings persist in the channel allow-store and merge with configured `allowFrom`
- scheduled automation and heartbeat recipient fallback use explicit delivery targets or configured `allowFrom`; DM pairing approvals are not implicit cron/heartbeat recipients
- if no allowlist is configured, the linked self number is allowed by default
- OpenClaw never auto-pairs outbound `fromMe` DMs (messages you send yourself from the linked device)
</Tab>
<Tab title="Group policy and allowlists">
Group access has two layers:
1. **Group membership allowlist** (`channels.whatsapp.groups`): if `groups` is omitted, all groups are eligible; if present, it acts as a group allowlist (`"*"` admits all).
2. **Group sender policy** (`channels.whatsapp.groupPolicy` + `groupAllowFrom`): `open` bypasses the sender allowlist, `allowlist` requires a `groupAllowFrom` (or `*`) match, `disabled` blocks all group inbound.
If `groupAllowFrom` is unset, sender checks fall back to `allowFrom` when it has entries. Sender allowlists are evaluated before mention/reply activation.
If no `channels.whatsapp` block exists at all, runtime falls back to `groupPolicy: "allowlist"` (with a warning log), even if `channels.defaults.groupPolicy` is set to something else.
<Note>
Group-membership resolution has a single-account safety net: if only one WhatsApp account is configured and its `accounts.<id>.groups` is an explicit empty object (`{}`), that is treated as "not set" and falls back to the root `channels.whatsapp.groups` map, instead of silently blocking every group. With 2+ accounts configured, an explicit empty account map stays empty and does not fall back — this lets one account intentionally disable all groups without affecting siblings.
</Note>
</Tab>
<Tab title="Mentions and /activation">
Group replies require a 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`)
- inbound voice-note transcripts for authorized group messages
- implicit reply-to-bot detection (reply sender matches bot identity)
Security: quote/reply only satisfies mention gating — it does **not** grant sender authorization. With `groupPolicy: "allowlist"`, non-allowlisted senders stay blocked even replying to an allowlisted user's message.
Session-level activation command: `/activation mention` or `/activation always`. This updates session state (not global config) and is owner-gated.
</Tab>
</Tabs>
## Configured ACP bindings
WhatsApp supports persistent ACP bindings via top-level `bindings[]`:
```json5
{
bindings: [
{
type: "acp",
agentId: "codex",
match: {
channel: "whatsapp",
accountId: "work",
peer: { kind: "direct", id: "+15555550123" },
},
},
{
type: "acp",
agentId: "codex",
match: {
channel: "whatsapp",
accountId: "work",
peer: { kind: "group", id: "120363424282127706@g.us" },
},
},
],
}
```
Direct chats match E.164 numbers; groups match WhatsApp group JIDs. Group allowlists, sender policy, and mention/activation gating run before OpenClaw ensures the bound ACP session exists. A matched binding owns the route — broadcast groups do not fan that turn out to ordinary WhatsApp sessions.
## Personal-number and self-chat behavior
When the linked self number is also present in `allowFrom`, self-chat safeguards activate: skip read receipts for self-chat turns, ignore mention-JID auto-trigger behavior that would ping yourself, and default replies to `[{identity.name}]` (or `[openclaw]`) when `messages.responsePrefix` is unset.
## Message normalization and context
<AccordionGroup>
<Accordion title="Inbound envelope and reply context">
Incoming messages are wrapped in the shared inbound envelope. A quoted reply appends context in this form:
```text
[Replying to <sender> id:<stanzaId>]
<quoted body or media placeholder>
[/Replying]
```
Reply metadata (`ReplyToId`, `ReplyToBody`, `ReplyToSender`, sender JID/E.164) is populated when available. If the quoted target is downloadable media, OpenClaw saves it through the normal inbound media store and exposes `MediaPath`/`MediaType` so the agent can inspect it directly instead of seeing only `<media:image>`.
</Accordion>
<Accordion title="Media placeholders and location/contact extraction">
Media-only messages normalize to placeholders: `<media:image>`, `<media:video>`, `<media:audio>`, `<media:document>`, `<media:sticker>`.
Authorized group voice notes are transcribed before mention gating when the body is only `<media:audio>`, so saying the bot mention in the voice note can trigger the reply. If the transcript still does not mention the bot, it stays in pending group history instead of the raw placeholder.
Location bodies render as terse coordinate text. Location labels/comments and contact/vCard details render as fenced untrusted metadata, not inline prompt text.
</Accordion>
<Accordion title="Pending group history injection">
Unprocessed group messages buffer and inject 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]` and `[Current message - respond to this]`.
</Accordion>
<Accordion title="Read receipts">
Enabled by default for accepted inbound messages. Disable globally:
```json5
{ channels: { whatsapp: { sendReadReceipts: false } } }
```
Per-account override: `channels.whatsapp.accounts.<id>.sendReadReceipts`. Self-chat turns skip read receipts even when globally enabled.
</Accordion>
</AccordionGroup>
## Delivery, chunking, and media
<AccordionGroup>
<Accordion title="Text chunking">
- default chunk limit: `channels.whatsapp.textChunkLimit = 4000`
- `channels.whatsapp.chunkMode = "length" | "newline"`; `newline` prefers paragraph boundaries (blank lines), then falls back to length-safe chunking
</Accordion>
<Accordion title="Outbound media behavior">
- supports image, video, audio (PTT voice-note), and document payloads
- audio is sent as the Baileys `audio` payload with `ptt: true`, rendering as a push-to-talk voice note; `audioAsVoice` is preserved on reply payloads so TTS voice-note output stays on this path regardless of the provider's source format
- native Ogg/Opus audio sends as `audio/ogg; codecs=opus`; anything else (including Microsoft Edge TTS MP3/WebM output) is transcoded with `ffmpeg` to 48 kHz mono Ogg/Opus before PTT delivery
- `/tts latest` sends the latest assistant reply as one voice note and suppresses repeat sends for the same reply; `/tts chat on|off|default` controls auto-TTS for the current chat
- `gifPlayback: true` on video sends enables animated GIF playback
- `forceDocument`/`asDocument` routes outbound images, GIFs, and videos through the Baileys document payload to avoid WhatsApp's media compression, preserving the resolved filename and MIME type
- captions apply to the first media item in a multi-media reply, except PTT voice notes: the audio sends first with no caption, then the caption sends as a separate text message (WhatsApp clients do not render voice-note captions consistently)
- media source can be HTTP(S), `file://`, or a local path
</Accordion>
<Accordion title="Media size limits and fallback behavior">
- inbound save cap and outbound send cap: `channels.whatsapp.mediaMaxMb` (default `50`)
- per-account override: `channels.whatsapp.accounts.<id>.mediaMaxMb`
- images auto-optimize (resize/quality sweep) to fit limits unless `forceDocument`/`asDocument` requests document delivery
- on media send failure, the first-item fallback sends a text warning instead of dropping the response silently
</Accordion>
</AccordionGroup>
## Reply quoting
`channels.whatsapp.replyToMode` controls native reply quoting (outbound replies visibly quote the inbound message):
| Value | Behavior |
| ----------------- | -------------------------------------------------------------- |
| `"off"` (default) | Never quote; send as a plain message |
| `"first"` | Quote only the first outbound reply chunk |
| `"all"` | Quote every outbound reply chunk |
| `"batched"` | Quote queued batched replies; leave immediate replies unquoted |
Per-account override: `channels.whatsapp.accounts.<id>.replyToMode`.
```json5
{ channels: { whatsapp: { replyToMode: "first" } } }
```
## Reaction level
`channels.whatsapp.reactionLevel` controls how broadly the agent uses emoji reactions:
| Level | Ack reactions | Agent-initiated reactions |
| --------------------- | ------------- | -------------------------- |
| `"off"` | No | No |
| `"ack"` | Yes | No |
| `"minimal"` (default) | Yes | Yes, conservative guidance |
| `"extensive"` | Yes | Yes, encouraged guidance |
Per-account override: `channels.whatsapp.accounts.<id>.reactionLevel`.
```json5
{ channels: { whatsapp: { reactionLevel: "ack" } } }
```
## Acknowledgment reactions
`channels.whatsapp.ackReaction` sends an immediate reaction on inbound receipt, gated by `reactionLevel` (suppressed when `"off"`):
```json5
{
channels: {
whatsapp: {
ackReaction: {
emoji: "👀",
direct: true,
group: "mentions", // always | mentions | never
},
},
},
}
```
Notes: sent immediately after inbound is accepted (pre-reply); if `ackReaction` is present without `emoji`, WhatsApp uses the routed agent's identity emoji falling back to "👀" (omit `ackReaction` or set `emoji: ""` for no ack); failures are logged but do not block reply delivery; group mode `mentions` reacts only on mention-triggered turns, while group activation `always` bypasses that check; WhatsApp uses `channels.whatsapp.ackReaction` only (legacy `messages.ackReaction` does not apply here).
## Lifecycle status reactions
Set `messages.statusReactions.enabled: true` to let WhatsApp replace the ack reaction during a turn instead of leaving a static receipt emoji, cycling through states such as queued, thinking, tool activity, compaction, done, and error:
```json5
{
messages: {
statusReactions: {
enabled: true,
emojis: {
deploy: "🛫",
build: "🏗️",
concierge: "💁",
},
},
},
}
```
Notes: `channels.whatsapp.ackReaction` still controls eligibility for direct messages and groups; the queued state uses the same effective emoji as plain ack reactions; WhatsApp has one bot reaction slot per message, so lifecycle updates replace the current reaction in place; `messages.removeAckAfterReply: true` clears the final status reaction after the configured done/error hold; tool emoji categories include `tool`, `coding`, `web`, `deploy`, `build`, and `concierge`.
## Multi-account and credentials
<AccordionGroup>
<Accordion title="Account selection and defaults">
Account ids come from `channels.whatsapp.accounts`. Default account selection is `default` if present, otherwise the first configured account id (alphabetically sorted). Account ids are normalized internally for lookup.
</Accordion>
<Accordion title="Credential paths and legacy compatibility">
- current auth path: `~/.openclaw/credentials/whatsapp/<accountId>/creds.json` (backup: `creds.json.bak`)
- legacy default auth in `~/.openclaw/credentials/` is still recognized/migrated for default-account flows
</Accordion>
<Accordion title="Logout behavior">
`openclaw channels logout --channel whatsapp [--account <id>]` clears WhatsApp auth state for that account. When a gateway is reachable, logout stops the live listener for that account first, so the linked session stops receiving messages before the next restart. `openclaw channels remove --channel whatsapp` also stops the live listener before disabling or deleting account config.
In legacy auth directories, `oauth.json` is preserved while Baileys auth files are removed.
</Accordion>
</AccordionGroup>
## Tools, actions, and config writes
- Agent tool support includes the WhatsApp reaction action (`react`).
- Action gates: `channels.whatsapp.actions.reactions`, `channels.whatsapp.actions.polls` (existing actions default to `true`), `channels.whatsapp.actions.calls` (default `false`, see MeowCaller above).
- Channel-initiated config writes are enabled by default; disable via `channels.whatsapp.configWrites: false`.
## Troubleshooting
<AccordionGroup>
<Accordion title="Not linked (QR required)">
Symptom: channel status reports not linked.
```bash
openclaw channels login --channel whatsapp
openclaw channels status
```
</Accordion>
<Accordion title="Linked but disconnected / reconnect loop">
Symptom: linked account with repeated disconnects or reconnect attempts.
Quiet accounts can stay connected past the normal message timeout; the watchdog restarts only when WhatsApp Web transport activity stops, the socket closes, or application-level activity stays silent beyond the longer safety window (see Runtime model above).
If logs show repeated `status=408 Request Time-out Connection was lost`, tune Baileys socket timings under `web.whatsapp`. Start by shortening `keepAliveIntervalMs` below your network's idle timeout and increasing `connectTimeoutMs` on slow or lossy links:
```json5
{
web: {
whatsapp: {
keepAliveIntervalMs: 15000,
connectTimeoutMs: 60000,
defaultQueryTimeoutMs: 60000,
},
},
}
```
Fix:
```bash
openclaw channels status --probe
openclaw doctor
openclaw logs --follow
openclaw gateway status
```
If the loop persists after host connectivity and timing are fixed, back up the account auth directory and re-link:
```bash
cp -a ~/.openclaw/credentials/whatsapp/<accountId> \
~/.openclaw/credentials/whatsapp/<accountId>.bak
openclaw channels logout --channel whatsapp --account <accountId>
openclaw channels login --channel whatsapp --account <accountId>
```
If `~/.openclaw/logs/whatsapp-health.log` says `Gateway inactive` but `openclaw gateway status` and `openclaw channels status --probe` both show healthy, run `openclaw doctor`. On Linux, doctor warns about legacy crontab entries invoking the retired `~/.openclaw/bin/ensure-whatsapp.sh` script; remove those entries with `crontab -e` — cron can lack the systemd user-bus environment and make that old script misreport gateway health.
</Accordion>
<Accordion title="QR login times out behind a proxy">
Symptom: `openclaw channels login --channel whatsapp` fails before showing a usable QR with `status=408 Request Time-out` or a TLS socket disconnect.
WhatsApp Web login uses the gateway host's standard proxy environment (`HTTPS_PROXY`, `HTTP_PROXY`, lowercase variants, `NO_PROXY`). Verify the gateway process inherits the proxy env and that `NO_PROXY` does not match `mmg.whatsapp.net`.
</Accordion>
<Accordion title="No active listener when sending">
Outbound sends fail fast when no active gateway listener exists for the target account. Confirm the gateway is running and the account is linked.
</Accordion>
<Accordion title="Reply appears in transcript but not in WhatsApp">
Transcript rows record what the agent generated; WhatsApp delivery is checked separately. OpenClaw only treats an auto-reply as sent after Baileys returns an outbound message id for at least one visible text or media send.
Ack reactions are independent pre-reply receipts — a successful reaction does not prove the later text/media reply was accepted. Check gateway logs for `auto-reply delivery failed` or `auto-reply was not accepted by WhatsApp provider`.
</Accordion>
<Accordion title="Group messages unexpectedly ignored">
Check in this order: `groupPolicy`, `groupAllowFrom`/`allowFrom`, `groups` allowlist entries, mention gating (`requireMention` + mention patterns), and duplicate keys in `openclaw.json` (JSON5 later entries override earlier ones — keep a single `groupPolicy` per scope).
If `channels.whatsapp.groups` is present, WhatsApp can still observe messages from other groups, but OpenClaw drops them before session routing. Add the group JID to `channels.whatsapp.groups`, or add `groups["*"]` to admit all groups while keeping sender authorization under `groupPolicy`/`groupAllowFrom`.
</Accordion>
<Accordion title="Bun runtime warning">
WhatsApp gateway runtime should use Node. Bun is flagged as incompatible for stable WhatsApp/Telegram gateway operation.
</Accordion>
</AccordionGroup>
## System prompts
WhatsApp supports Telegram-style system prompts for groups and direct chats via the `groups` and `direct` maps.
Resolution for group messages: the effective `groups` map is determined first — if the account defines its own `groups` key at all, it fully replaces the root `groups` map (no deep merge). Prompt lookup then runs on that single resulting map:
1. **Group-specific prompt** (`groups["<groupId>"].systemPrompt`): used when the group entry exists **and** its `systemPrompt` key is defined. An empty string (`""`) suppresses the wildcard and applies no prompt.
2. **Group wildcard prompt** (`groups["*"].systemPrompt`): used when the specific group entry is absent, or exists without a `systemPrompt` key.
Resolution for direct messages follows the identical pattern against the `direct` map and `direct["*"]`.
<Note>
`dms` remains the lightweight per-DM history override bucket (`dms.<id>.historyLimit`). Prompt overrides live under `direct`.
</Note>
<Note>
This account-replaces-root behavior for prompt resolution is a plain shallow override: any account `groups`/`direct` key, including an explicit empty object, replaces the root map. It differs from the group-membership allowlist check described above, which has a single-account safety net for an accidentally empty `groups: {}`.
</Note>
**Difference from Telegram:** Telegram suppresses root `groups` for every account in a multi-account setup (even accounts with no `groups` of their own) to stop a bot receiving group messages for groups it does not belong to. WhatsApp does not apply that guard — root `groups`/`direct` are inherited by any account without its own override, regardless of account count. In a multi-account WhatsApp setup, define the full map under each account explicitly if you want per-account prompts.
Important behavior:
- `channels.whatsapp.groups` is both a per-group config map and the chat-level group allowlist. At either root or account scope, `groups["*"]` means "all groups are admitted" for that scope.
- Only add a wildcard `systemPrompt` when you already want that scope to admit all groups. To keep only a fixed set of group IDs eligible, repeat the prompt on each explicitly allowlisted entry instead of using `groups["*"]`.
- Group admission and sender authorization are separate checks. `groups["*"]` widens which groups reach group handling; it does not authorize every sender in those groups — that stays controlled by `groupPolicy`/`groupAllowFrom`.
- `channels.whatsapp.direct` has no equivalent side effect for DMs: `direct["*"]` only supplies a default config after a DM is already admitted by `dmPolicy` plus `allowFrom` or pairing-store rules.
Example:
```json5
{
channels: {
whatsapp: {
groups: {
// Use only if all groups should be admitted at the root scope.
// Applies to all accounts that do not define their own groups map.
"*": { systemPrompt: "Default prompt for all groups." },
},
direct: {
// Applies to all accounts that do not define their own direct map.
"*": { systemPrompt: "Default prompt for all direct chats." },
},
accounts: {
work: {
groups: {
// This account defines its own groups, so root groups are fully
// replaced. To keep a wildcard, define "*" explicitly here too.
"120363406415684625@g.us": {
requireMention: false,
systemPrompt: "Focus on project management.",
},
// Use only if all groups should be admitted in this account.
"*": { systemPrompt: "Default prompt for work groups." },
},
direct: {
// This account defines its own direct map, so root direct entries are
// fully replaced. To keep a wildcard, define "*" explicitly here too.
"+15551234567": { systemPrompt: "Prompt for a specific work direct chat." },
"*": { systemPrompt: "Default prompt for work direct chats." },
},
},
},
},
},
}
```
## Configuration reference pointers
Primary reference: [Configuration reference - WhatsApp](/gateway/config-channels#whatsapp)
| Area | Fields |
| ---------------- | -------------------------------------------------------------------------------------------------------------- |
| Access | `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups` |
| Delivery | `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel` |
| Multi-account | `accounts.<id>.enabled`, `accounts.<id>.authDir`, and other per-account overrides |
| Operations | `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`, `web.whatsapp.*` |
| Session behavior | `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms.<id>.historyLimit` |
| Prompts | `groups.<id>.systemPrompt`, `groups["*"].systemPrompt`, `direct.<id>.systemPrompt`, `direct["*"].systemPrompt` |
## Related
- [Pairing](/channels/pairing)
- [Groups](/channels/groups)
- [Security](/gateway/security)
- [Channel routing](/channels/channel-routing)
- [Multi-agent routing](/concepts/multi-agent)
- [Troubleshooting](/channels/troubleshooting)

358
docs/channels/yuanbao.md Normal file
View File

@@ -0,0 +1,358 @@
---
summary: "Yuanbao bot overview, features, and configuration"
read_when:
- You want to connect a Yuanbao bot
- You are configuring the Yuanbao channel
title: Yuanbao
---
Tencent Yuanbao is Tencent's AI assistant platform. The community-maintained `openclaw-plugin-yuanbao` plugin connects Yuanbao bots to OpenClaw over WebSocket for direct messages and group chats.
**Status:** production-ready for bot DMs and group chats. WebSocket is the only supported connection mode. This plugin is maintained by the Tencent Yuanbao team as an external catalog entry, not by core OpenClaw; the config/behavior details below (beyond install and the generic CLI surface) come from the plugin's own docs and are not verified against OpenClaw core source.
## Quick start
Requires OpenClaw 2026.4.10 or above. Check with `openclaw --version`; upgrade with `openclaw update`.
<Steps>
<Step title="Add the Yuanbao channel with your credentials">
```bash
openclaw channels add --channel yuanbao --token "appKey:appSecret"
```
`--token` uses colon-separated `appKey:appSecret`. Get these from the Yuanbao app by creating a bot in your application settings.
</Step>
<Step title="Restart the gateway to apply the change">
```bash
openclaw gateway restart
```
</Step>
</Steps>
### Interactive setup (alternative)
```bash
openclaw channels login --channel yuanbao
```
Follow the prompts to enter your App ID and App Secret.
## Access control
### Direct messages
`channels.yuanbao.dm.policy`:
| Value | Behavior |
| ---------------- | ------------------------------------------------- |
| `open` (default) | Allow all users |
| `pairing` | Unknown users get a pairing code; approve via CLI |
| `allowlist` | Only users in `allowFrom` can chat |
| `disabled` | Disable all DMs |
Approve a pairing request:
```bash
openclaw pairing list yuanbao
openclaw pairing approve yuanbao <CODE>
```
### Group chats
`channels.yuanbao.requireMention` (default `true`): require an @mention before the bot responds in a group. Replying to the bot's own message is treated as an implicit mention.
## Configuration examples
Basic setup, open DM policy:
```json5
{
channels: {
yuanbao: {
appKey: "your_app_key",
appSecret: "your_app_secret",
dm: {
policy: "open",
},
},
},
}
```
Restrict DMs to specific users:
```json5
{
channels: {
yuanbao: {
appKey: "your_app_key",
appSecret: "your_app_secret",
dm: {
policy: "allowlist",
allowFrom: ["user_id_1", "user_id_2"],
},
},
},
}
```
Disable the @mention requirement in groups:
```json5
{
channels: {
yuanbao: {
requireMention: false,
},
},
}
```
Outbound delivery tuning:
```json5
{
channels: {
yuanbao: {
outboundQueueStrategy: "merge-text",
minChars: 2800, // buffer until this many chars
maxChars: 3000, // force split above this limit
idleMs: 5000, // auto-flush after idle timeout (ms)
},
},
}
```
Set `outboundQueueStrategy: "immediate"` to send each chunk without buffering.
## Common commands
| Command | Description |
| ---------- | --------------------------- |
| `/help` | Show available commands |
| `/status` | Show bot status |
| `/new` | Start a new session |
| `/stop` | Stop the current run |
| `/restart` | Restart OpenClaw |
| `/compact` | Compact the session context |
Yuanbao supports native slash-command menus; commands sync to the platform automatically when the gateway starts.
## Troubleshooting
**Bot does not respond in group chats:**
1. Confirm the bot is added to the group
2. Confirm you @mention the bot (required by default)
3. Check logs: `openclaw logs --follow`
**Bot does not receive messages:**
1. Confirm the bot is created and approved in the Yuanbao app
2. Confirm `appKey` and `appSecret` are correctly configured
3. Confirm the gateway is running: `openclaw gateway status`
4. Check logs: `openclaw logs --follow`
**Bot sends empty or fallback replies:**
1. Check whether the AI model is returning valid content
2. Default fallback reply: "暂时无法解答,你可以换个问题问问我哦"
3. Customize with `channels.yuanbao.fallbackReply`
**App Secret leaked:**
1. Reset the App Secret in the Yuanbao app
2. Update the value in your config
3. Restart the gateway: `openclaw gateway restart`
## Advanced configuration
### Multiple accounts
```json5
{
channels: {
yuanbao: {
defaultAccount: "main",
accounts: {
main: {
appKey: "key_xxx",
appSecret: "secret_xxx",
name: "Primary bot",
},
backup: {
appKey: "key_yyy",
appSecret: "secret_yyy",
name: "Backup bot",
enabled: false,
},
},
},
},
}
```
`defaultAccount` controls which account is used when outbound APIs do not specify an `accountId`.
### Message limits
- `maxChars`: single message max character count (default `3000`)
- `mediaMaxMb`: media upload/download limit (default `20` MB)
- `overflowPolicy`: behavior when a message exceeds the limit, `"split"` (default) or `"stop"`
### Streaming
Yuanbao supports block-level streaming output; the bot sends text in chunks as it generates.
```json5
{
channels: {
yuanbao: {
disableBlockStreaming: false, // block streaming enabled (default)
},
},
}
```
Set `disableBlockStreaming: true` to send the complete reply in one message.
### Group chat history context
```json5
{
channels: {
yuanbao: {
historyLimit: 100, // default: 100, set 0 to disable
},
},
}
```
Controls how many historical messages are included in the AI context for group chats.
### Reply-to mode
```json5
{
channels: {
yuanbao: {
replyToMode: "first", // "off" | "first" | "all" (default: "first")
},
},
}
```
| Value | Behavior |
| ------- | -------------------------------------------------------- |
| `off` | No quote reply |
| `first` | Quote only the first reply per inbound message (default) |
| `all` | Quote every reply |
### Markdown hint injection
By default, the bot injects a system-prompt instruction to prevent the model from wrapping the entire reply in a markdown code block.
```json5
{
channels: {
yuanbao: {
markdownHintEnabled: true, // default: true
},
},
}
```
### Debug mode
```json5
{
channels: {
yuanbao: {
debugBotIds: ["bot_user_id_1", "bot_user_id_2"],
},
},
}
```
Enables unsanitized log output for the listed bot IDs.
### Multi-agent routing
Use `bindings` to route Yuanbao DMs or groups to different agents:
```json5
{
agents: {
list: [
{ id: "main" },
{ id: "agent-a", workspace: "/home/user/agent-a" },
{ id: "agent-b", workspace: "/home/user/agent-b" },
],
},
bindings: [
{
agentId: "agent-a",
match: {
channel: "yuanbao",
peer: { kind: "direct", id: "user_xxx" },
},
},
{
agentId: "agent-b",
match: {
channel: "yuanbao",
peer: { kind: "group", id: "group_zzz" },
},
},
],
}
```
- `match.channel`: `"yuanbao"`
- `match.peer.kind`: `"direct"` (DM) or `"group"` (group chat)
- `match.peer.id`: user ID or group code
## Configuration reference
Full configuration: [Gateway configuration](/gateway/configuration)
| Setting | Description | Default |
| ------------------------------------------ | ------------------------------------------------- | -------------------------------------- |
| `channels.yuanbao.enabled` | Enable/disable the channel | `true` |
| `channels.yuanbao.defaultAccount` | Default account for outbound routing | `default` |
| `channels.yuanbao.accounts.<id>.appKey` | App Key (signing + ticket generation) | - |
| `channels.yuanbao.accounts.<id>.appSecret` | App Secret (signing) | - |
| `channels.yuanbao.accounts.<id>.token` | Pre-signed token (skips automatic ticket signing) | - |
| `channels.yuanbao.accounts.<id>.name` | Account display name | - |
| `channels.yuanbao.accounts.<id>.enabled` | Enable/disable a specific account | `true` |
| `channels.yuanbao.dm.policy` | DM policy | `open` |
| `channels.yuanbao.dm.allowFrom` | DM allowlist (user ID list) | - |
| `channels.yuanbao.requireMention` | Require @mention in groups | `true` |
| `channels.yuanbao.overflowPolicy` | Long message handling (`split` or `stop`) | `split` |
| `channels.yuanbao.replyToMode` | Group reply-to strategy (`off`, `first`, `all`) | `first` |
| `channels.yuanbao.outboundQueueStrategy` | Outbound strategy (`merge-text` or `immediate`) | `merge-text` |
| `channels.yuanbao.minChars` | Merge-text: min chars to trigger send | `2800` |
| `channels.yuanbao.maxChars` | Merge-text: max chars per message | `3000` |
| `channels.yuanbao.idleMs` | Merge-text: idle timeout before auto-flush (ms) | `5000` |
| `channels.yuanbao.mediaMaxMb` | Media size limit (MB) | `20` |
| `channels.yuanbao.historyLimit` | Group chat history context entries | `100` |
| `channels.yuanbao.disableBlockStreaming` | Disable block-level streaming output | `false` |
| `channels.yuanbao.fallbackReply` | Fallback reply when the model returns no content | `暂时无法解答,你可以换个问题问问我哦` |
| `channels.yuanbao.markdownHintEnabled` | Inject markdown anti-wrapping instructions | `true` |
| `channels.yuanbao.debugBotIds` | Debug allowlist bot IDs (unsanitized logs) | `[]` |
## Supported message types
**Receive:** text, images, files, audio/voice, video, stickers/custom emoji, custom elements (link cards).
**Send:** text (markdown), images, files, audio, video, stickers.
**Threads and replies:** quote replies (configurable via `replyToMode`); thread replies are not supported by the platform.
## Related
- [Channels Overview](/channels) - all supported channels
- [Pairing](/channels/pairing) - DM authentication and pairing flow
- [Groups](/channels/groups) - group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) - session routing for messages
- [Security](/gateway/security) - access model and hardening

179
docs/channels/zalo.md Normal file
View File

@@ -0,0 +1,179 @@
---
summary: "Zalo bot support status, capabilities, and configuration"
read_when:
- Working on Zalo features or webhooks
title: "Zalo"
---
Status: experimental. Direct messages and group chats are both implemented; the [Capabilities](#capabilities) table below reflects verified behavior on Zalo Bot Creator / Marketplace bots.
## Bundled plugin
Zalo ships as a bundled plugin in current OpenClaw releases, so packaged builds do not need a separate install.
On an older build or a custom install that excludes Zalo, install the npm package directly:
- Install: `openclaw plugins install @openclaw/zalo`
- Pinned version: `openclaw plugins install @openclaw/zalo@2026.6.11`
- From a local checkout: `openclaw plugins install ./path/to/local/zalo-plugin`
- Details: [Plugins](/tools/plugin)
## Quick setup
1. Create a bot token at [https://bot.zaloplatforms.com](https://bot.zaloplatforms.com) (sign in, create a bot, configure settings). The token is `numeric_id:secret`; for Marketplace bots the usable runtime token may appear in the bot's welcome message.
2. Set the token, either as env `ZALO_BOT_TOKEN=...` (default account only) or in config.
3. Restart the gateway.
4. Approve the pairing code on first DM contact (default DM policy is pairing).
Minimal config:
```json5
{
channels: {
zalo: {
enabled: true,
accounts: {
default: {
botToken: "12345689:abc-xyz",
dmPolicy: "pairing",
},
},
},
},
}
```
Multi-account: add more entries under `channels.zalo.accounts.<id>`, each with its own `botToken`/`name`. `channels.zalo.botToken` (flat, no `accounts`) is a legacy single-account shorthand; prefer `accounts.<id>.*` for new configs.
## What it is
Zalo is a Vietnam-focused messaging app. Its Bot API lets the Gateway run a bot for both 1:1 conversations and group chats, with deterministic routing back to Zalo (the model never chooses channels).
This page covers **Zalo Bot Creator / Marketplace bots**. **Zalo Official Account (OA) bots** are a different product surface and may behave differently; this page does not cover them.
## How it works
- Inbound messages are normalized into the shared channel envelope with media placeholders.
- Replies always route back to the same Zalo chat; quote-reply is not used (`replyToMode` is fixed off).
- Long-polling (`getUpdates`) by default; webhook mode available via `channels.zalo.webhookUrl`.
- Groups require an @mention to trigger the bot; this is not configurable per channel.
## Limits
| Limit | Value |
| ------------------------------ | ----------------------------------------------------------------------------- |
| Outbound text chunk size | 2000 characters (Zalo API limit) |
| Media size (inbound/outbound) | `channels.zalo.mediaMaxMb`, default `5` MB |
| Webhook request body | 1 MB, 30s read timeout |
| Webhook rate limit | 120 requests / 60s per path+client IP, then HTTP 429 |
| Webhook duplicate-event window | 5 minutes (keyed on path + account + event name + chat + sender + message id) |
## Access control
### Direct messages
- `channels.zalo.dmPolicy`: `pairing` (default) | `allowlist` | `open` | `disabled`.
- Pairing: unknown senders get a pairing code; messages are ignored until approved. Codes expire after 1 hour.
- `openclaw pairing list zalo`
- `openclaw pairing approve zalo <CODE>`
- Details: [Pairing](/channels/pairing)
- `channels.zalo.allowFrom` accepts numeric Zalo user IDs (no username lookup). `open` requires `"*"`.
### Groups
Group chats are supported by the plugin (`chatTypes: ["direct", "group"]`) and gated by mention plus group policy:
- `channels.zalo.groupPolicy`: `open` | `allowlist` | `disabled`.
- `channels.zalo.groupAllowFrom` restricts which sender IDs can trigger the bot in groups; falls back to `allowFrom` when unset.
- Default resolution: when `channels.zalo` is configured, an unset `groupPolicy` resolves to `open`. When `channels.zalo` is missing entirely, runtime fails closed to `allowlist`.
- Reported real-world caveat: on some Marketplace-bot setups the bot could not be added to a group at all. If you hit that, verify with your bot's Zalo Bot Platform settings; it is a platform-side constraint, not an OpenClaw policy.
## Long-polling vs webhook
- Default: long-polling (no public URL required).
- Webhook mode: set `channels.zalo.webhookUrl` and `channels.zalo.webhookSecret`.
- Webhook URL must use HTTPS.
- Webhook secret must be 8-256 characters.
- Zalo sends events with an `X-Bot-Api-Secret-Token` header, checked with a constant-time comparison.
- Gateway HTTP handles webhook requests at `channels.zalo.webhookPath` (defaults to the webhook URL's path).
- Requests must use `Content-Type: application/json` (or a `+json` media type).
- getUpdates polling and webhook are mutually exclusive per Zalo API docs.
## Supported message types
- Text: full support, chunked to 2000 characters.
- Media: inbound/outbound, capped by `mediaMaxMb`.
- Reactions, threads, polls, native commands: not supported by the plugin.
- Streaming: the plugin declares block-streaming capability, but Zalo has no dedicated outbound queue/merge-text tuning knobs (unlike some other regional channels); verify current behavior in your environment if this matters for your use case.
## Capabilities
| Feature | Status |
| ------------------------ | --------------------------------- |
| Direct messages | Supported |
| Groups | Supported (mention-gated) |
| Media (inbound/outbound) | Supported, capped by `mediaMaxMb` |
| Reactions | Not supported |
| Threads | Not supported |
| Polls | Not supported |
| Native commands | Not supported |
| Reply-to / quote | Not used (fixed off) |
## Delivery targets (CLI/cron)
Use a chat ID as the target:
```bash
openclaw message send --channel zalo --target 123456789 --message "hi"
```
## Troubleshooting
**Bot does not respond:**
- Check the token: `openclaw channels status --probe`
- Verify the sender is approved (pairing or `allowFrom`)
- Check gateway logs: `openclaw logs --follow`
**Webhook not receiving events:**
- Confirm the webhook URL uses HTTPS
- Confirm the secret is 8-256 characters
- Confirm the gateway HTTP endpoint is reachable on the configured path
- Confirm getUpdates polling is not also running (they are mutually exclusive)
- A burst of requests can return HTTP 429 (120 requests / 60s per path+IP); back off and retry
## Configuration reference
Full configuration: [Configuration](/gateway/configuration)
| Setting | Description | Default |
| -------------------------------------------- | ------------------------------------------------- | --------------------- |
| `channels.zalo.enabled` | Enable/disable channel startup | `true` |
| `channels.zalo.accounts.<id>.botToken` | Bot token from Zalo Bot Platform | - |
| `channels.zalo.accounts.<id>.tokenFile` | Read token from a file (symlinks rejected) | - |
| `channels.zalo.accounts.<id>.name` | Display name | - |
| `channels.zalo.accounts.<id>.enabled` | Enable/disable this account | `true` |
| `channels.zalo.accounts.<id>.dmPolicy` | Per-account DM policy | `pairing` |
| `channels.zalo.accounts.<id>.allowFrom` | DM allowlist (user IDs) | - |
| `channels.zalo.accounts.<id>.groupPolicy` | Per-account group policy | see [Groups](#groups) |
| `channels.zalo.accounts.<id>.groupAllowFrom` | Group sender allowlist; falls back to `allowFrom` | - |
| `channels.zalo.accounts.<id>.mediaMaxMb` | Inbound/outbound media cap (MB) | `5` |
| `channels.zalo.accounts.<id>.webhookUrl` | Enable webhook mode (HTTPS required) | - |
| `channels.zalo.accounts.<id>.webhookSecret` | Webhook secret (8-256 chars) | - |
| `channels.zalo.accounts.<id>.webhookPath` | Webhook path on the gateway HTTP server | webhook URL path |
| `channels.zalo.accounts.<id>.proxy` | Proxy URL for API requests | - |
| `channels.zalo.accounts.<id>.responsePrefix` | Outbound response prefix override | - |
| `channels.zalo.defaultAccount` | Default account when multiple are configured | `default` |
`channels.zalo.botToken`, `channels.zalo.dmPolicy`, and other flat top-level keys are the legacy single-account shorthand for the fields above; both forms are supported.
Env option: `ZALO_BOT_TOKEN=...` resolves the default account's token only.
## Related
- [Channels Overview](/channels) - all supported channels
- [Pairing](/channels/pairing) - DM authentication and pairing flow
- [Groups](/channels/groups) - group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) - session routing for messages
- [Security](/gateway/security) - access model and hardening

View File

@@ -0,0 +1,89 @@
---
summary: "Zalo ClawBot channel setup through the external openclaw-zaloclawbot plugin"
read_when:
- You want a personal Zalo assistant bot with QR-code login
- You are installing or troubleshooting the openclaw-zaloclawbot channel plugin
title: "Zalo ClawBot"
---
OpenClaw connects to Zalo ClawBot through the catalog-listed external `@zalo-platforms/openclaw-zaloclawbot` plugin. Login uses a Zalo Mini App QR code; the plugin id in config is `openclaw-zaloclawbot`.
## Compatibility
| Plugin Version | OpenClaw Version | npm dist-tag | Status |
| -------------- | ---------------- | ------------ | ------------- |
| 0.1.4 | >=2026.4.10 | `latest` | Active / Beta |
## Prerequisites
- Node.js >= 22
- [OpenClaw](https://docs.openclaw.ai/install) installed (`openclaw` CLI available)
- A Zalo account on a mobile device to scan the login QR code
## Install with onboard (recommended)
```bash
openclaw onboard
```
Pick **Zalo ClawBot** from the channel menu. The wizard installs the plugin from the official catalog (integrity-verified), renders the login QR in the terminal, and finishes the channel once you scan it with the Zalo app.
## Manual installation
To add the channel to an already-onboarded gateway:
### 1. Install the plugin
```bash
openclaw plugins install "@zalo-platforms/openclaw-zaloclawbot@0.1.4"
```
Use the exact pinned version so OpenClaw verifies the package against the catalog integrity hash during install.
### 2. Enable the plugin in config
```bash
openclaw config set plugins.entries.openclaw-zaloclawbot.enabled true
```
### 3. Generate a QR code and log in
```bash
openclaw channels login --channel openclaw-zaloclawbot
```
Scan the terminal-rendered QR code with the Zalo mobile app, accept the Terms of Use inside the Zalo Mini App, and authorize the session.
### 4. Restart the gateway
```bash
openclaw gateway restart
```
## How it works
Unlike the standard Zalo channel, which requires registering your own Zalo Official Account (OA) and configuring static developer credentials, Zalo ClawBot is an **owner-bound personal assistant** on shared official infrastructure:
1. **Onboarding:** the QR code resolves to a Zalo Mini App that binds a newly provisioned, private bot under a shared official OA directly to your Zalo user ID.
2. **Owner-bound privacy:** the bot only communicates with its owner. Messages from other users are dropped at the platform level.
3. **Official API path:** the plugin uses Zalo Bot Platform APIs, not browser or web-session automation.
## Under the hood
The plugin communicates with Zalo via a persistent long-polling loop (`getUpdates`). Webhooks are disabled by default for local desktop/terminal gateway runs. Messages are processed client-side and mapped to your local agent runtime.
The plugin manages bot credentials under the OpenClaw state directory. Treat that directory as sensitive and cover it under the same access-control and backup policy as the rest of OpenClaw state.
This plugin's runtime lives entirely in the external `@zalo-platforms/openclaw-zaloclawbot` package; behavior details below beyond install/config are as reported by the plugin's maintainers and are not verified against OpenClaw core source.
## Troubleshooting
- **QR login timeout:** the login token (`zbsk`) expires after 5 minutes for security. If the QR code expires before you scan it, rerun the login command to generate a new one.
- **Gateway fails to load:** confirm your OpenClaw host version is `2026.4.10` or higher. Older versions do not support the external npm-plugin installation ledger this ID requires.
## Related
- [Channels Overview](/channels) - all supported channels
- [Zalo](/channels/zalo) - the bundled Zalo Bot Creator / Marketplace channel
- [Pairing](/channels/pairing) - DM authentication and pairing flow
- [Plugins](/tools/plugin) - installing and managing plugins

211
docs/channels/zalouser.md Normal file
View File

@@ -0,0 +1,211 @@
---
summary: "Zalo personal account support via native zca-js (QR login), capabilities, and configuration"
read_when:
- Setting up Zalo Personal for OpenClaw
- Debugging Zalo Personal login or message flow
title: "Zalo personal"
---
Status: experimental. This integration automates a **personal Zalo account** via native `zca-js`, in-process, with no external CLI binary.
<Warning>
This is an unofficial integration and may result in account suspension or ban. Use at your own risk.
</Warning>
## Install
Zalo Personal is an official external plugin, not bundled in core. Install it before use:
```bash
openclaw plugins install @openclaw/zalouser
```
- Pin a version: `openclaw plugins install @openclaw/zalouser@<version>`
- From a source checkout: `openclaw plugins install ./path/to/local/zalouser-plugin`
- Details: [Plugins](/tools/plugin)
## Quick setup
1. Install the plugin (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
{
channels: {
zalouser: {
enabled: true,
dmPolicy: "pairing",
},
},
}
```
4. Restart the Gateway (or finish setup).
5. DM access defaults to pairing; approve the pairing code on first contact.
## What it is
- Runs entirely in-process via the `zca-js` library (no external `zca`/`openzca` binary).
- Uses native event listeners (`message`, `error`) to receive inbound messages.
- Sends replies directly through the JS API (text/media/link).
- Designed for "personal account" use cases where the Zalo Bot API is not available.
## Naming
Channel id is `zalouser` to make it explicit this automates a **personal Zalo user account** (unofficial). `zalo` is reserved for a potential future official Zalo API integration.
## Finding IDs (directory)
```bash
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 limit).
- Streaming is not supported.
## Access control (DMs)
`channels.zalouser.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`).
`channels.zalouser.allowFrom` should use stable Zalo user IDs. It can also reference static sender access groups (`accessGroup:<name>`). During interactive setup, entered names can be resolved to IDs using the plugin's in-process contact lookup.
If a raw name remains in config, startup resolves it only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled. Without that opt-in, runtime sender checks are ID-only and raw names are ignored for authorization.
Approve via:
- `openclaw pairing list zalouser`
- `openclaw pairing approve zalouser <code>`
## Group access (optional)
- Default: `channels.zalouser.groupPolicy = "allowlist"` (groups require an explicit allowlist entry).
- Open all groups: `channels.zalouser.groupPolicy = "open"`.
- Block all groups: `channels.zalouser.groupPolicy = "disabled"`.
- With `groupPolicy = "allowlist"`:
- `channels.zalouser.groups` keys should be stable group IDs; names resolve to IDs on startup only when `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled.
- `channels.zalouser.groupAllowFrom` controls which senders in allowed groups can trigger the bot; static sender access groups can be referenced with `accessGroup:<name>`.
- The configure wizard can prompt for group allowlists.
- Group allowlist matching is ID-only by default. Unresolved names are ignored for auth unless `channels.zalouser.dangerouslyAllowNameMatching: true` is enabled.
- `channels.zalouser.dangerouslyAllowNameMatching: true` is a break-glass compatibility mode that re-enables mutable startup name resolution and runtime group-name matching.
- `groupAllowFrom` does **not** fall back to `allowFrom` for normal group messages: leaving it empty on an allowlisted group opens that group to any sender. Authorized control commands (for example `/new`) are the exception; command sender checks fall back to `allowFrom` when `groupAllowFrom` is empty.
Example:
```json5
{
channels: {
zalouser: {
groupPolicy: "allowlist",
groupAllowFrom: ["1471383327500481391"],
groups: {
"123456789": { enabled: true },
"Work Chat": { enabled: true },
},
},
},
}
```
<Note>
`channels.zalouser.groups.<id>.allow` is a legacy field name; current config uses `enabled`. `openclaw doctor --fix` migrates `allow` to `enabled` automatically.
</Note>
### Group mention gating
- `channels.zalouser.groups.<group>.requireMention` controls whether group replies require a mention.
- Resolution order: group id -> `group:<id>` alias -> group name/slug (name-based candidates only apply when `dangerouslyAllowNameMatching: true`) -> `*` -> default (`true`).
- Applies both to allowlisted groups and open group mode.
- Quoting a bot message counts as an implicit mention for group activation.
- Authorized control commands (for example `/new`) can bypass mention gating.
- When a group message is skipped because a mention is required, OpenClaw stores it as pending group history and includes it on the next processed group message.
- Group history limit: `channels.zalouser.historyLimit`, then `messages.groupChat.historyLimit`, then a fallback of `50`.
Example:
```json5
{
channels: {
zalouser: {
groupPolicy: "allowlist",
groups: {
"*": { enabled: true, requireMention: true },
"Work Chat": { enabled: true, requireMention: false },
},
},
},
}
```
## Multi-account
Accounts map to `zalouser` profiles in OpenClaw state. Example:
```json5
{
channels: {
zalouser: {
enabled: true,
defaultAccount: "default",
accounts: {
work: { enabled: true, profile: "work" },
},
},
},
}
```
## Environment variables
Profile selection can also come from environment variables:
| Var | Purpose |
| ------------------ | -------------------------------------------------------------------------- |
| `ZALOUSER_PROFILE` | Profile name to use when no `profile` is set in channel or account config. |
| `ZCA_PROFILE` | Legacy fallback, used only when `ZALOUSER_PROFILE` is not set. |
Profile names select the saved Zalo login credentials in OpenClaw state. Resolution order:
1. Explicit `profile` in config.
2. `ZALOUSER_PROFILE`.
3. `ZCA_PROFILE`.
4. The account id for non-default accounts, or `default` for the default account.
For multi-account setups, prefer setting `profile` on each account in config so one environment variable does not make multiple accounts share the same login session.
## 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`/`groupAllowFrom` and stable group IDs in `groups`. If you intentionally need exact friend/group names, enable `channels.zalouser.dangerouslyAllowNameMatching: true`.
**Upgraded from an old external `zca`/CLI-based setup:**
- Remove any external `zca` process assumptions; the channel now runs fully in-process via `zca-js`, with no external CLI binary.
## Related
- [Channels Overview](/channels) - all supported channels
- [Pairing](/channels/pairing) - DM authentication and pairing flow
- [Groups](/channels/groups) - group chat behavior and mention gating
- [Channel Routing](/channels/channel-routing) - session routing for messages
- [Security](/gateway/security) - access model and hardening