Channel Configuration: Access, Pairing, and Keys
Learn to configure per-channel settings for DM and group access, pairing, and channel-specific keys across Slack, Discord, Telegram, WhatsApp, Matrix, iMessage, and more.
Read this when
- Configuring a channel plugin (auth, access control, multi-account)
- Troubleshooting per-channel config keys
- Auditing DM policy, group policy, or mention gating
Per-channel settings under channels.* cover DM and group access, multi-account configurations, mention gating, and channel-specific keys for Slack, Discord, Telegram, WhatsApp, Matrix, iMessage, and other channel plugins.
For agent, tool, gateway runtime, and other top-level settings, consult the Configuration reference.
Channels
A channel launches automatically whenever its config block exists (unless enabled: false). Telegram is bundled with the core openclaw package. Other official channels (iMessage, Discord, Slack, WhatsApp, Matrix, Microsoft Teams, IRC, Google Chat, Signal, Mattermost, and others) are installed as separate plugins via openclaw plugins install <spec>; the complete list and installation instructions are in Channels.
DM and group access
Every channel supports both DM policies and group policies:
| DM policy | Behavior |
|---|---|
pairing (default) | Unknown senders receive a one-time pairing code; owner approval required |
allowlist | Only senders found in allowFrom (or paired allow store) |
open | Permit all inbound DMs (needs allowFrom: ["*"]) |
disabled | Disregard all inbound DMs |
| Group policy | Behavior |
|---|---|
allowlist (default) | Only groups that match the configured allowlist |
open | Skip group allowlists (mention-gating still enforced) |
disabled | Deny all group/room messages |
Note
When a provider's
groupPolicyis not set,channels.defaults.groupPolicysupplies the default. Pairing codes are valid for 1 hour. Pending pairing requests are limited to 3 per account (scoped by channel and account id). If a provider block is completely missing (channels.<provider>absent), the runtime group policy defaults toallowlist(fail-closed) and logs a startup warning.
Channel model overrides
To pin specific channel IDs or direct-message peers to a model, use channels.modelByChannel. Values accept provider/model or configured model aliases. The channel mapping applies only when a session has no active model override already (for instance, one set via /model).
For group/thread conversations, keys are channel-specific group IDs, topic IDs, or channel names. For direct-message (DM) conversations, keys are peer identifiers derived from the channel's sender identity (nativeDirectUserId, origin.from, origin.to, OriginatingTo, From, or SenderId). The exact key form varies by channel:
| Channel | DM key form | Example |
|---|---|---|
| Discord | raw user ID | 987654321 |
| Feishu | feishu:ou_... | feishu:ou_a8b6cab7e945387de5f253775d9b4d85 |
| Matrix | Matrix user ID | @user:matrix.org |
| Slack | user:U... | user:U12345 |
| Telegram | raw user ID | 123456789 |
| phone number or JID | 15551234567 |
{
channels: {
modelByChannel: {
discord: {
"123456789012345678": "anthropic/claude-opus-4-6",
},
slack: {
C1234567890: "openai/gpt-5.6-sol",
"user:U12345": "openai/gpt-5.4-mini",
},
telegram: {
"-1001234567890": "openai/gpt-5.4-mini",
"-1001234567890:topic:99": "anthropic/claude-sonnet-4-6",
"123456789": "openai/gpt-4.1",
},
},
},
}
DM-specific keys are matched only in direct-message conversations; they have no effect on group/thread routing.
Channel defaults and heartbeat
For shared group-policy, implicit-mention, and heartbeat behavior across providers, use channels.defaults:
{
channels: {
defaults: {
groupPolicy: "allowlist", // open | allowlist | disabled
contextVisibility: "all", // all | allowlist | allowlist_quote
implicitMentions: {
replyToBot: true,
quotedBot: true,
threadParticipation: true,
},
heartbeatVisibility: {
showOk: false,
showAlerts: true,
useIndicator: true,
},
},
},
}
channels.defaults.groupPolicy: fallback group policy applied when a provider-levelgroupPolicyis not set.channels.defaults.contextVisibility: default supplemental context visibility mode across all channels. Possible values:all(default, include all quoted/thread/history context),allowlist(only include context from allowlisted senders),allowlist_quote(same as allowlist but keep explicit quote/reply context). Per-channel override:channels.<channel>.contextVisibility.channels.defaults.implicitMentions: determines which supported inbound facts are treated as mentions.replyToBot,quotedBot, andthreadParticipationeach default totrue, maintaining current behavior. The names are positive: setting a flag tofalseprevents that fact from bypassing mention gating. Among bundled channels, Mattermost, Slack, and Tlon read this policy; on those channels you can also override it per channel withchannels.<channel>.implicitMentionsor per account withchannels.<channel>.accounts.<id>.implicitMentions, and each flag resolves account -> channel -> defaults independently. Other bundled channels that produce implicit mention facts do not currently read these settings, so on those channels the facts always count as mentions and the override has no effect. Native explicit mentions are always allowed, and a flag has no effect when the channel does not produce that fact. See Mention gating for the current producer matrix. These settings do not change outbound reply/thread modes or authorized command handling.channels.defaults.heartbeatVisibility.showOk: include healthy channel statuses in heartbeat output (defaultfalse).channels.defaults.heartbeatVisibility.showAlerts: include degraded/error statuses in heartbeat output (defaulttrue).channels.defaults.heartbeatVisibility.useIndicator: render compact indicator-style heartbeat output (defaulttrue).
WhatsApp operates through the gateway's web channel (Baileys Web). It launches automatically when a linked session exists.
{
channels: {
whatsapp: {
enabled: true,
dmPolicy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["+15555550123", "+447700900123"],
textChunkLimit: 4000,
streaming: { chunkMode: "length" }, // length | newline
mediaMaxMb: 50,
sendReadReceipts: true, // blue ticks (false in self-chat mode)
groups: {
"*": { requireMention: true },
},
groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"],
},
},
}
- Top-level
bindings[]entries withtype: "acp"configure persistent ACP bindings for WhatsApp DMs and groups. Use an E.164 direct number or WhatsApp group JID inmatch.peer.id. Field semantics are shared in ACP Agents.
Multi-account WhatsApp
{
channels: {
whatsapp: {
accounts: {
default: {},
personal: {},
biz: {
// authDir: "~/.openclaw/credentials/whatsapp/biz",
},
},
},
},
}
- Outbound commands default to account
defaultif present; otherwise the first configured account id (sorted). - Optional
channels.whatsapp.defaultAccountoverrides that fallback default account selection when it matches a configured account id. - Legacy single-account Baileys auth dir is migrated by
openclaw doctorintowhatsapp/default. - Per-account overrides:
channels.whatsapp.accounts.<id>.sendReadReceipts,channels.whatsapp.accounts.<id>.dmPolicy,channels.whatsapp.accounts.<id>.allowFrom.
Telegram
{
channels: {
telegram: {
enabled: true,
botToken: "your-bot-token",
dmPolicy: "pairing",
allowFrom: ["tg:123456789"],
groups: {
"*": { requireMention: true },
"-1001234567890": {
allowFrom: ["@admin"],
systemPrompt: "Keep answers brief.",
topics: {
"99": {
requireMention: false,
skills: ["search"],
systemPrompt: "Stay on topic.",
},
},
},
},
customCommands: [
{ command: "backup", description: "Git backup" },
{ command: "generate", description: "Create an image" },
],
historyLimit: 50,
replyToMode: "first", // off | first | all | batched
linkPreview: true,
streaming: { mode: "partial" }, // off | partial | block | progress (default: partial)
actions: { reactions: true, sendMessage: true },
reactionNotifications: "own", // off | own | all
mediaMaxMb: 100,
network: {
autoSelectFamily: true,
dnsResultOrder: "ipv4first",
},
apiRoot: "https://api.telegram.org",
trustedLocalFileRoots: ["/srv/telegram-bot-api-data"],
proxy: "socks5://localhost:9050",
webhookUrl: "https://example.com/telegram-webhook",
webhookSecret: "secret",
webhookPath: "/telegram-webhook",
},
},
}
- The bot token is supplied via
channels.telegram.botTokenorchannels.telegram.tokenFile(only regular files are accepted, symlinks are refused), withTELEGRAM_BOT_TOKENserving as the fallback for the default account. apiRootpoints exclusively to the Telegram Bot API root. Opt forhttps://api.telegram.orgor your own self-hosted/proxy root instead ofhttps://api.telegram.org/bot<TOKEN>;openclaw doctor --fixstrips any stray trailing/bot<TOKEN>suffix.- When running a self-hosted Bot API server in
--localmode,trustedLocalFileRootsenumerates the host paths OpenClaw is permitted to access. Attach the server data volume to the OpenClaw host, then set either its data root or a per-token directory; container paths beneath/var/lib/telegram-bot-apiare linked to those roots. Any other absolute paths stay blocked. - An optional
channels.telegram.defaultAccountcan override the default account selection when it aligns with a configured account id. - In setups with multiple accounts (2+ account ids), define an explicit default (
channels.telegram.defaultAccountorchannels.telegram.accounts.default) to prevent fallback routing;openclaw doctorissues a warning if this is absent or invalid. configWrites: falseprevents Telegram-initiated config changes (supergroup ID migrations,/config set|unset).actions.reactionsgoverns both message reactions andemoji-list, which enumerates the standard and custom reactions permitted in the current chat.- Top-level
bindings[]entries paired withtype: "acp"set up persistent ACP bindings for forum topics (use the canonicalchatId:topic:topicIdwithinmatch.peer.id). The field semantics are detailed in ACP Agents. - Telegram stream previews rely on
sendMessagepluseditMessageText(functional in both direct and group chats). network.dnsResultOrderis set to"ipv4first"by default to sidestep frequent IPv6 fetch issues.- Retry policy: consult Retry policy.
Discord
{
channels: {
discord: {
enabled: true,
token: "your-bot-token",
mediaMaxMb: 100,
allowBots: false,
actions: {
reactions: true,
stickers: true,
polls: true,
permissions: true,
messages: true,
threads: true,
pins: true,
search: true,
memberInfo: true,
roleInfo: true,
roles: false,
channelInfo: true,
voiceStatus: true,
events: true,
moderation: false,
},
replyToMode: "off", // off | first | all | batched
dmPolicy: "pairing",
allowFrom: ["1234567890", "123456789012345678"],
dm: { enabled: true, groupEnabled: false, groupChannels: ["openclaw-dm"] },
guilds: {
"123456789012345678": {
slug: "friends-of-openclaw",
requireMention: false,
ignoreOtherMentions: true,
reactionNotifications: "own",
users: ["987654321098765432"],
channels: {
general: { enabled: true },
help: {
enabled: true,
requireMention: true,
users: ["987654321098765432"],
skills: ["docs"],
systemPrompt: "Short answers only.",
},
},
},
},
historyLimit: 20,
textChunkLimit: 2000,
suppressEmbeds: true,
streaming: {
mode: "progress", // explicit opt-in; Discord defaults to off
chunkMode: "length", // length | newline
progress: {
label: "auto",
maxLines: 8,
maxLineChars: 120,
toolProgress: true,
},
},
maxLinesPerMessage: 17,
threadBindings: {
enabled: true,
idleHours: 24,
maxAgeHours: 0,
spawnSessions: true,
defaultSpawnContext: "fork",
},
voice: {
enabled: true,
autoJoin: [
{
guildId: "123456789012345678",
channelId: "234567890123456789",
whenOccupied: true,
},
],
daveEncryption: true,
decryptionFailureTolerance: 24,
connectTimeoutMs: 30000,
reconnectGraceMs: 15000,
tts: {
provider: "openai",
providers: { openai: { speakerVoice: "alloy" } },
},
},
execApprovals: {
enabled: "auto", // true | false | "auto"
approvers: ["987654321098765432"],
agentFilter: ["default"],
sessionFilter: ["discord:"],
target: "dm", // dm | channel | both
cleanupAfterResolve: false,
},
},
},
}
- Token:
channels.discord.token, withDISCORD_BOT_TOKENserving as the fallback for the default account. - When a direct outbound call specifies an explicit Discord
token, that token is used for the call; account policy settings still derive from the account selected in the active runtime snapshot. - An optional
channels.discord.defaultAccountoverrides the default account selection if it matches a configured account id. - Use
user:<id>(DM) orchannel:<id>(guild channel) as delivery targets; bare numeric IDs are not accepted. actions.reactionsgovernsreact,reactions, andemoji-list; emoji discovery falls back to the current server unlessguildIdis supplied.- Guild slugs are lowercase, with spaces replaced by
-; channel keys rely on the slugged name (without#). Guild IDs are preferred. - Bot-authored messages are ignored by default.
allowBots: trueturns them on; useallowBots: "mentions"to accept only bot messages that mention the bot (own messages remain filtered). - Channels accepting bot-authored inbound messages can use shared bot loop protection. Set
channels.defaults.botLoopProtectionfor baseline pair budgets, then override the channel or account only when one surface needs different limits. channels.discord.guilds.<id>.ignoreOtherMentions(and channel overrides) drops messages that mention another user or role but not the bot (excluding @everyone/@here).channels.discord.mentionAliasesmaps stable outbound@handletext to Discord user IDs before sending, so known teammates can be mentioned deterministically even when the transient directory cache is empty. Per-account overrides reside underchannels.discord.accounts.<accountId>.mentionAliases.maxLinesPerMessage(default17) splits tall messages even when under 2000 chars.channels.discord.suppressEmbedsdefaults totrue, so outbound URLs do not expand into Discord link previews unless disabled. Explicitembedspayloads still send normally; per-message tool calls can override withsuppressEmbeds.channels.discord.threadBindingscontrols Discord thread-bound routing:enabled: Discord override for thread-bound session features (/focus,/unfocus,/agents,/session idle,/session max-age, and bound delivery/routing)idleHours: Discord override for inactivity auto-unfocus in hours (0disables)maxAgeHours: Discord override for hard max age in hours (0disables)spawnSessions: switch forsessions_spawn({ thread: true })and ACP thread-spawn auto thread creation/binding (default:true)defaultSpawnContext: native subagent context for thread-bound spawns ("fork"by default)
- Top-level
bindings[]entries withtype: "acp"configure persistent ACP bindings for channels and threads (use channel/thread id inmatch.peer.id). Field semantics are shared in ACP Agents. channels.discord.agentComponents.ttlMscontrols how long sent Discord component callbacks remain registered. Default1800000(30 minutes), maximum86400000(24 hours). Per-account overrides live underchannels.discord.accounts.<accountId>.agentComponents.ttlMs. Prefer the shortest TTL that fits the workflow.channels.discord.voiceenables Discord voice channel conversations and optional auto-join + LLM + TTS overrides. Text-only Discord configs leave voice off by default; setchannels.discord.voice.enabled=trueto opt in.channels.discord.voice.autoJoin[].whenOccupiedkeeps an auto-managed voice channel disconnected until a human is present, then leaves when the last human departs. It defaults tofalse; bots do not count as occupants, and manual or ad-hoc voice sessions are not managed by this policy.channels.discord.voice.modeloptionally overrides the LLM model used for Discord voice channel responses.channels.discord.voice.daveEncryption(defaulttrue) andchannels.discord.voice.decryptionFailureTolerance(default24) are forwarded to the@discordjs/voiceDAVE options.- The initial
@discordjs/voiceReady wait for/vc joinand auto-join attempts are governed bychannels.discord.voice.connectTimeoutMs, with a default of30000. - The duration a disconnected voice session may take before entering reconnect signalling, after which OpenClaw terminates it, is set by
channels.discord.voice.reconnectGraceMs(default15000). - Another user's speaking-start event does not interrupt Discord voice playback. To prevent feedback loops, OpenClaw disregards new voice capture while TTS is active.
- OpenClaw also tries voice receive recovery by leaving and rejoining a voice session following repeated decrypt failures.
channels.discord.streamingserves as the canonical stream mode key. Discord preview streaming defaults tooff; to opt into one edited tool/work progress message, setstreaming.mode: "progress", or pickpartialorblockfor answer previews. Legacy flat keys (streamMode,chunkMode,blockStreaming,draftChunk,blockStreamingCoalesce) are no longer read at runtime; executeopenclaw doctor --fixto migrate persisted config.- Runtime availability is mapped to bot presence via
channels.discord.autoPresence(healthy => online, degraded => idle, exhausted => dnd), with optional status text overrides permitted. - Human availability arrivals are routed by
channels.discord.guilds.<id>.presenceEventsinto one configured Discord channel as agent system events. Eligible members must have visibility ofchannelId; public threads inherit parent visibility, whereas private threads also require membership or Manage Threads.userscan further restrict that audience. Current online members are seeded from completeGUILD_CREATEsnapshots, observed offline-to-online transitions are routed, and a first later online signal for an unseen member is treated as newly available without asserting whether they came online or joined after the snapshot. For guilds exceeding Discord's 75,000-member snapshot limit, an explicit offline update is required first. Throttling knobs:reconnectSuppressSeconds(quiet window after a new Gateway session while guild presence state is rebuilt, default 300,0disables) andburstLimit/burstWindowSeconds(per-guild successfully queued event rate limit, default 8 events per 60s sliding window). Resumed sessions do not trigger the reconnect suppression window. The existing per-user re-greet cooldown remains eight hours. It requireschannels.discord.intents.presence=true, the privileged Presence Intent in Discord's Developer Portal, and an enabled agent heartbeat. channels.discord.intents.messageContentdefaults totrue. Only set it tofalsefor mention-only operation when Discord cannot grant the privileged Message Content intent; DMs and explicit bot mentions still carry message content, while other guild messages do not. In this mode, keeprequireMention: trueon every configured guild channel.- Mutable name/tag matching is re-enabled by
channels.discord.dangerouslyAllowNameMatching(break-glass compatibility mode). channels.discord.execApprovals: Discord-native exec approval delivery and approver authorization.enabled:true,false, or"auto"(default). In auto mode, exec approvals activate when approvers can be resolved fromapproversorcommands.ownerAllowFrom.approvers: Discord user IDs allowed to approve exec requests. Falls back tocommands.ownerAllowFromwhen omitted.agentFilter: optional agent ID allowlist. Omit to forward approvals for all agents.sessionFilter: optional session key patterns (substring or regex).target: where to send approval prompts."dm"(default) sends to approver DMs,"channel"sends to the originating channel,"both"sends to both. When target includes"channel", buttons are only usable by resolved approvers.cleanupAfterResolve: whentrue, deletes approval DMs after approval, denial, or timeout.
Reaction notification modes: off (none), own (bot's messages, default), all (all messages), allowlist (from guilds.<id>.users on all messages).
Google Chat
{
channels: {
googlechat: {
enabled: true,
serviceAccountFile: "/path/to/service-account.json",
audienceType: "app-url", // app-url | project-number
audience: "https://gateway.example.com/googlechat",
webhookPath: "/googlechat",
botUser: "users/1234567890",
dmPolicy: "pairing",
allowFrom: ["users/1234567890"],
groupPolicy: "allowlist",
groups: {
"spaces/AAAA": { enabled: true, requireMention: true },
},
typingIndicator: "message",
mediaMaxMb: 20,
},
},
}
- The service account can be supplied either as inline JSON (
serviceAccount) or through a file reference (serviceAccountFile). - A SecretRef can be passed straight into
serviceAccount. - When no explicit account is set, the default one falls back to environment variables
GOOGLE_CHAT_SERVICE_ACCOUNTorGOOGLE_CHAT_SERVICE_ACCOUNT_FILE. - For delivery destinations, opt for
spaces/<spaceId>orusers/<userId>. - To restore compatibility with mutable email principal matching, turn on
channels.googlechat.dangerouslyAllowNameMatching(break-glass mode).
Slack
{
channels: {
slack: {
enabled: true,
botToken: "xoxb-...",
appToken: "xapp-...",
dmPolicy: "pairing",
allowFrom: ["U123", "U456", "*"],
dm: { enabled: true, groupEnabled: false, groupChannels: ["G123"] },
channels: {
C123: { enabled: true, requireMention: true, allowBots: false },
C456: {
enabled: true,
requireMention: true,
allowBots: false,
users: ["U123"],
skills: ["docs"],
systemPrompt: "Short answers only.",
},
},
historyLimit: 50,
allowBots: false,
reactionNotifications: "own",
reactionAllowlist: ["U123"],
replyToMode: "off", // off | first | all | batched
thread: {
historyScope: "thread", // thread | channel
inheritParent: false,
initialHistoryLimit: 20,
},
actions: {
reactions: true,
messages: true,
pins: true,
memberInfo: true,
emojiList: true,
},
slashCommand: {
enabled: true,
name: "openclaw",
sessionPrefix: "slack:slash",
ephemeral: true,
},
typingReaction: "hourglass_flowing_sand",
unfurlLinks: false,
unfurlMedia: false,
textChunkLimit: 4000,
streaming: {
mode: "partial", // off | partial | block | progress
chunkMode: "length", // length | newline
nativeTransport: true, // use Slack native streaming API when mode=partial
},
mediaMaxMb: 20,
execApprovals: {
enabled: "auto", // true | false | "auto"
approvers: ["U123"],
agentFilter: ["default"],
sessionFilter: ["slack:"],
target: "dm", // dm | channel | both
},
},
},
}
- Socket mode depends on both
botTokenandappToken(withSLACK_BOT_TOKENplusSLACK_APP_TOKENserving as the fallback for default account environment variables). - HTTP mode needs
botTokenalongsidesigningSecret(configured either at the root or per-account). - User identity (
postAs: "user") operates as the authorizing human for both posting and reading. This requiresuserTokenandappTokenwhen using Socket Mode, oruserTokenandsigningSecretin HTTP mode. Neither a bot token nor a bot user is necessary. Refer to User identity for details on user scopes and event subscriptions. - Enterprise Grid org-wide installations are detected automatically by Slack from the bot token using
auth.test; no explicit installation-mode setting is needed. Enterprise DMs accommodatedisabled,open,allowlist, and workspace-scopedpairing. Channel policies demandteam:<team-id>:channel:<channel-id>. User policies work with either an org-wide stable user ID orteam:<team-id>:user:<user-id>for workspace scope. Startup fails if mutable names or unsupported channel prefixes are present. Mention-pattern channel scopes and static route-binding peers rely on workspace-qualified Slack targets. Supported operations include direct Socket Mode or HTTP messages, mentions, workspace-qualified actions, deferred delivery, proactive sends, relevant event listeners and interactions, static route bindings, and Slack-native approvals from workspace-qualified turns. Not available are relay, channel-ID-change events, App Home, Agent and Assistant lifecycle events, configured ACP bindings, and runtime current-conversation bindings. See Enterprise Grid org-wide installs for the least-privilege manifest, setup workflow, and full list of restrictions. - The deprecated
enterpriseOrgInstallkey gets removed byopenclaw doctor --fixat both the Slack root and account levels. - Plaintext strings or SecretRef objects are accepted by
botToken,appToken,signingSecret, anduserToken. - Slack account snapshots reveal per-credential source/status fields, including
botTokenSource,botTokenStatus,userTokenSource,userTokenStatus,appTokenStatus, and, in HTTP mode,signingSecretStatus. Whenconfigured_unavailableappears, the account is set up via SecretRef, yet the secret value could not be resolved by the current command or runtime path. - Config writes initiated by Slack are blocked by
configWrites: false. - Optional
channels.slack.defaultAccounttakes precedence over default account selection when it matches a configured account id. dm.groupEnabledanddm.groupChannelsrestrict filtering to Slack group DMs (MPDMs) where the app is already a member. They do not grant visibility into an existing group DM the app never joined; instead, convert that group DM to a private channel and invite the app, or have the app start a new MPDM withconversations.open. See Group DMs (MPDMs) and bots.- The canonical Slack stream mode key is
channels.slack.streaming.mode(defaulting to"progress"). Slack's native streaming transport is governed bychannels.slack.streaming.nativeTransport(defaulttrue). LegacystreamMode, booleanstreaming,chunkMode,blockStreaming,blockStreamingCoalesce, andnativeStreamingvalues are ignored at runtime; executeopenclaw doctor --fixto migrate persisted config tostreaming.{mode,chunkMode,block.enabled,block.coalesce,nativeTransport}. - Slack's
chat.postMessagelink and media unfurl booleans are passed through for bot replies viaunfurlLinksandunfurlMedia.unfurlLinksdefaults tofalse, so outbound bot links stay unexpanded inline unless explicitly enabled;unfurlMediais left out unless configured. To override the top-level value for a single account, set either atchannels.slack.accounts.<accountId>. - Delivery targets are specified with
user:<id>(for DMs) orchannel:<id>.
Reaction notification modes: off, own (default), all, allowlist (from reactionAllowlist).
Thread session isolation: thread.historyScope applies per-thread by default, or it can be shared across the entire channel. When a new thread starts, thread.inheritParent copies the parent channel transcript into it. The number of existing thread messages pulled in at the start of a new thread session is capped by thread.initialHistoryLimit (defaulting to 20); setting 0 turns off thread history fetching entirely.
- Slack's native streaming and the assistant-style "is typing..." thread status both depend on a reply thread being targeted. Top-level DMs are off-thread unless configured otherwise, so they can rely on Slack's draft post-and-edit previews for streaming instead of the thread-native stream/status display.
- While a reply is in progress,
typingReactiondrops a temporary reaction on the inbound Slack message and removes it once the reply finishes. Use a Slack emoji shortcode like"hourglass_flowing_sand". channels.slack.execApprovals: delivery of approvals through Slack's native client and authorization for exec approvers. The schema matches Discord:enabled(true/false/"auto"),approvers(Slack user IDs),agentFilter,sessionFilter, andtarget("dm","channel", or"both"). For requests originating from Slack, plugin approvals can take this native-client route when Slack plugin approvers are resolvable; Slack-native plugin approval delivery can also be turned on viaapprovals.pluginfor Slack-origin sessions or Slack targets. Plugin approvals draw their approvers fromallowFromwith default routing, not from exec approvers.
| Action group | Default | Notes |
|---|---|---|
| reactions | enabled | React + list reactions |
| messages | enabled | Read/send/edit/delete |
| pins | enabled | Pin/unpin/list |
| memberInfo | enabled | Member info |
| emojiList | enabled | List custom emoji |
Mattermost
Mattermost is delivered as its own plugin, just like Discord, Slack, and WhatsApp:
openclaw plugins install @openclaw/mattermost
Before locking in a version, check npmjs.com/package/@openclaw/mattermost for the current dist-tags.
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
chatmode: "oncall", // oncall | onmessage | onchar
oncharPrefixes: [">", "!"],
groups: {
"*": { requireMention: true },
"team-channel-id": { requireMention: false },
},
commands: {
native: true, // opt-in
nativeSkills: true,
callbackPath: "/api/channels/mattermost/command",
// Optional explicit URL for reverse-proxy/public deployments
callbackUrl: "https://gateway.example.com/api/channels/mattermost/command",
},
textChunkLimit: 4000,
streaming: { chunkMode: "length" },
},
},
}
Chat modes: oncall (reply on @-mention, the default), onmessage (respond to every message), onchar (reply only when a message starts with the trigger prefix).
With Mattermost native commands enabled:
commands.callbackPathhas to be a path, for instance/api/channels/mattermost/command, never a full URL.commands.callbackUrlmust point to the OpenClaw gateway endpoint and stay reachable from the Mattermost server.- Each slash command's callback is authenticated using the tokens Mattermost returns at registration time. If registration fails or no commands are active, OpenClaw rejects callbacks with
Unauthorized: invalid command token. - For callback hosts that are private, on a tailnet, or internal, Mattermost might need
ServiceSettings.AllowedUntrustedInternalConnectionsto include the callback host/domain. Supply host/domain values, not full URLs. channels.mattermost.configWrites: control whether Mattermost can initiate config writes.channels.mattermost.requireMention: demand@mentionbefore replying in channels.channels.mattermost.groups.<channelId>.requireMention: per-channel override for mention gating ("*"is the default).- Optional
channels.mattermost.defaultAccounttakes precedence over default account selection when it matches a configured account id.
Signal
{
channels: {
signal: {
enabled: true,
account: "+15555550123", // optional account binding
dmPolicy: "pairing",
allowFrom: ["+15551234567", "uuid:123e4567-e89b-12d3-a456-426614174000"],
configWrites: true,
reactionNotifications: "own", // off | own | all | allowlist
reactionAllowlist: ["+15551234567", "uuid:123e4567-e89b-12d3-a456-426614174000"],
historyLimit: 50,
},
},
}
Reaction notification modes: off, own (default), all, allowlist (from reactionAllowlist).
channels.signal.account: lock channel startup to a specific Signal account identity.channels.signal.configWrites: control whether Signal can initiate config writes.- Optional
channels.signal.defaultAccounttakes precedence over default account selection when it matches a configured account id.
iMessage
OpenClaw launches imsg rpc (JSON-RPC over stdio). No daemon or port is needed. For new OpenClaw iMessage setups where the host can grant Messages database and Automation permissions, this is the recommended route.
BlueBubbles support is gone. channels.bluebubbles no longer counts as a supported runtime config surface on current OpenClaw. Move old configs to channels.imessage; the short explanation lives at BlueBubbles removal and the imsg iMessage path, and the complete translation table is at Coming from BlueBubbles.
If the Gateway runs on a different machine than the Messages Mac you are signed into, keep channels.imessage.enabled=true and assign channels.imessage.cliPath the full path, on the Gateway host, to an SSH wrapper that executes imsg "$@" on that Mac. Point remoteHost at the Messages Mac, not at the Gateway. For straightforward transparent SSH wrappers, OpenClaw performs automatic detection to ease compatibility; more involved wrappers, however, demand an explicit remoteHost. The default local imsg path exists only on macOS.
Before you depend on an SSH wrapper for production message sends, confirm that an outbound imsg send succeeds through that same wrapper. Certain macOS TCC states route Messages Automation to /usr/libexec/sshd-keygen-wrapper, a situation where reads and probes can succeed while sends fail with AppleEvents -1743; consult the SSH wrapper troubleshooting section on iMessage.
{
channels: {
imessage: {
enabled: true,
cliPath: "/home/openclaw/.openclaw/scripts/imsg-ssh",
dbPath: "/Users/user/Library/Messages/chat.db",
remoteHost: "user@messages-mac",
dmPolicy: "pairing",
allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
historyLimit: 50,
includeAttachments: false,
attachmentRoots: ["/Users/*/Library/Messages/Attachments"],
remoteAttachmentRoots: ["/Users/*/Library/Messages/Attachments"],
mediaMaxMb: 16,
service: "auto",
sendTransport: "auto",
region: "US",
actions: {
reactions: true,
edit: true,
unsend: true,
reply: true,
sendWithEffect: true,
sendAttachment: true,
},
},
},
}
- An optional
channels.imessage.defaultAccounttakes precedence over the default account selection when it aligns with a configured account id. - Full Disk Access to the Messages DB is mandatory.
- Favor
chat_id:<id>targets. Useimsg chats --limit 20to enumerate chats. - In SSH deployments,
cliPathis a full path located on the Gateway host.remoteHost(hostoruser@host) identifies the Messages Mac, anddbPathis resolved on that Mac. Supply an absolute remote database path rather than deriving it from the Gateway user's home directory. - When
remoteHostis set or detected, inbound attachment fetching and outbound file staging operate over the existing strict SSH/SCP transport. Outbound files use a remote temporary path that is owner-only, with best-effort cleanup after success, failure, or timeout; a cleanup failure produces a warning and may leave owner-only residue. attachmentRootsandremoteAttachmentRootsconstrain the paths for inbound attachments (default:/Users/*/Library/Messages/Attachments).- Because SCP enforces strict host-key checking, the Messages Mac host key must already be present in
~/.ssh/known_hosts. channels.imessage.configWrites: permit or block config writes initiated by iMessage.channels.imessage.sendTransport: the preferredimsgRPC send transport for standard outbound replies.auto(the default) uses the IMCore bridge for existing chats while it is active, then falls back to AppleScript;bridgedemands private-API delivery;applescriptforces the public Messages automation path.channels.imessage.actions.*: turn on private API actions that are additionally gated byimsg status/openclaw channels status --probe.channels.imessage.includeAttachmentsstarts disabled; set it totruebefore you expect inbound media during agent turns.- After a bridge or gateway restart, inbound recovery happens automatically (GUID dedupe plus a stale-backlog age fence). Existing
channels.imessage.catchup.enabled: trueconfigs remain honored as a deprecated compatibility profile;catchupis disabled unless enabled. channels.imessage.groups: the group registry and per-group settings. WithgroupPolicy: "allowlist", set either explicitchat_idkeys or a"*"wildcard entry so group messages can clear the registry gate.- Top-level
bindings[]entries carryingtype: "acp"can attach iMessage conversations to persistent ACP sessions. Use a normalized handle or an explicit chat target (chat_id:*,chat_guid:*,chat_identifier:*) insidematch.peer.id. Shared field semantics: ACP Agents.
iMessage SSH wrapper example
#!/usr/bin/env bash
exec ssh -T messages-mac imsg "$@"
With remote imsg v0.13.4, poll votes must go through pollOptionId; its poll.vote RPC method cannot resolve index or text selectors. Attachment replies to nonzero part indices are likewise unavailable remotely. These restrictions leave local imsg behavior unchanged.
Matrix
Matrix is plugin-backed and configured under channels.matrix.
{
channels: {
matrix: {
enabled: true,
homeserver: "https://matrix.example.org",
accessToken: "syt_bot_xxx",
proxy: "http://127.0.0.1:7890",
encryption: true,
initialSyncLimit: 20,
defaultAccount: "ops",
accounts: {
ops: {
name: "Ops",
userId: "@ops:example.org",
accessToken: "syt_ops_xxx",
},
alerts: {
userId: "@alerts:example.org",
password: "secret",
proxy: "http://127.0.0.1:7891",
},
},
},
},
}
- Token-based authentication relies on
accessToken, whereas password-based authentication combinesuserIdwithpassword. - Matrix HTTP traffic can be directed through a designated HTTP(S) proxy using
channels.matrix.proxy. Named accounts have the option to override this viachannels.matrix.accounts.<id>.proxy. - Private or internal homeservers are supported through
channels.matrix.network.dangerouslyAllowPrivateNetwork. The settingsproxyand this network opt-in operate independently of each other. - In configurations with multiple accounts,
channels.matrix.defaultAccountdetermines which account takes precedence. - By default,
channels.matrix.autoJoinis set to"off", meaning invited rooms and newly created DM-style invites are not acted upon until you configureautoJoin: "allowlist"using eitherautoJoinAllowlistorautoJoin: "always". channels.matrix.execApprovalshandles Matrix-native delivery of exec approvals and the authorization of approvers.enabled: options aretrue,false, or"auto"(the default). When set to auto, exec approvals become active as long as approvers can be identified fromapproversorcommands.ownerAllowFrom.approvers: a list of Matrix user IDs (for example,@owner:example.org) that are permitted to approve exec requests.agentFilter: an optional allowlist of agent IDs. If left empty, approvals are forwarded for every agent.sessionFilter: optional patterns for session keys, matching either substrings or regular expressions.target: specifies the destination for approval prompts. The default is"dm", with alternatives being"channel"(the room where the request originated) or"both".- Overrides at the account level are configured with
channels.matrix.accounts.<id>.execApprovals.
- How Matrix DMs are grouped into sessions is governed by
channels.matrix.dm.sessionScope: the defaultper-usergroups by the routed peer, whileper-roomkeeps each DM room separate. - The same proxy policy applied to runtime traffic also governs Matrix status checks and live directory lookups.
- The Matrix documentation contains the full Matrix configuration, targeting rules, and setup examples.
Microsoft Teams
Microsoft Teams operates through a plugin and is set up under channels.msteams.
{
channels: {
msteams: {
enabled: true,
configWrites: true,
// appId, appPassword, tenantId, webhook, team/channel policies:
// see /channels/msteams
},
},
}
- The primary key paths addressed here are
channels.msteamsandchannels.msteams.configWrites. - Complete Teams configuration details, covering credentials, webhooks, DM/group policies, and per-team or per-channel overrides, appear in the Microsoft Teams documentation.
IRC
IRC runs as a plugin and is configured under channels.irc.
{
channels: {
irc: {
enabled: true,
dmPolicy: "pairing",
configWrites: true,
nickserv: {
enabled: true,
service: "NickServ",
password: "${IRC_NICKSERV_PASSWORD}",
register: false,
registerEmail: "bot@example.com",
},
},
},
}
- The key paths covered in this section are
channels.irc,channels.irc.dmPolicy,channels.irc.configWrites, andchannels.irc.nickserv.*. - When
channels.irc.defaultAccountmatches a configured account id, it can optionally take precedence over the default account selection. - The full IRC channel configuration, including host, port, TLS, channels, allowlists, and mention gating, is detailed in the IRC documentation.
Multi-account (all channels)
It is possible to run several accounts on a single channel, each with its own accountId:
{
channels: {
telegram: {
accounts: {
default: {
name: "Primary bot",
botToken: "123456:ABC...",
},
alerts: {
name: "Alerts bot",
botToken: "987654:XYZ...",
},
},
},
},
}
- When
accountIdis not provided,defaultis applied for both CLI and routing purposes. - Environment tokens are only effective for the default account.
- Unless overridden per account, base channel settings apply universally across all accounts.
- To direct each account to a different agent, use
bindings[].match.accountId. - If you introduce a non-default account through
openclaw channels add(or via channel onboarding) while still using a single-account top-level channel configuration, OpenClaw first promotes account-scoped top-level single-account values into the channel account map to ensure the original account continues functioning. For most channels, these values are moved intochannels.<channel>.accounts.default; Matrix may instead preserve an existing matching named or default target. - Channel-only bindings that lack
accountIdcontinue to match the default account, and account-scoped bindings remain optional. - Mixed shapes are also repaired by
openclaw doctor --fix, which relocates account-scoped top-level single-account values into the promoted account selected for that channel. Most channels rely onaccounts.default; Matrix can preserve an existing matching named or default target as an alternative.
Other plugin channels
Many plugin channels are set up through channels.<id> and have their own dedicated channel pages covering them (Feishu, LINE, Nextcloud Talk, Nostr, QQ Bot, Synology Chat, Twitch, and Zalo are examples).
A complete list of channels is available here: Channels.
Group chat mention gating
For group messages, the default behavior is require mention, which means either a metadata mention or a safe regex pattern must be present. This applies to group chats on WhatsApp, Telegram, Discord, Google Chat, and iMessage.
Visible replies are governed by a separate setting. Standard group, channel, and internal WebChat direct requests default to automatic final delivery: the final assistant text is posted through the legacy visible reply path. When model-authored source replies should only be posted after the agent invokes message(action=send), opt into messages.visibleReplies: "message_tool" or messages.groupChat.visibleReplies: "message_tool". In an opted-in tool-only mode, if the model returns a substantive final answer without calling the message tool, that final text remains private, the gateway verbose log records the suppressed payload metadata, and OpenClaw queues a single recovery retry asking the model to resend the same reply through message(action=send).
The tool-only policy applies to assistant source replies and generic tool media. It does not suppress runtime-owned terminal output, such as authorized command responses, durable completion notices, or provider-native artifacts that the owning harness explicitly classifies as host-owned. Host-owned artifacts are dispatched through the normal channel path and still respect outbound sendPolicy denial. Ambient room_event turns stay quiet unless they are explicit commands, even when runtime output is marked host-owned.
Tool-only visible replies depend on a model/runtime that reliably calls tools, and they are recommended for shared ambient rooms on latest-generation models like GPT-5.6 Sol. Some weaker models can produce final text but fail to understand that source-visible output must be transmitted with message(action=send). OpenClaw recovers the common stranded-final case by default only when the final is substantive, the source turn was not a room event, send policy did not deny delivery, and no source reply was already sent. Recovery is limited to one retry; it suppresses persistence for the synthetic retry prompt and excludes that retry from collect batching so it cannot merge with unrelated queued prompts. If the retry also strands or cannot be enqueued, OpenClaw delivers only a sanitized diagnostic such as "I generated a reply but could not deliver it to this chat. Please try again." The original private final text is never marked for automatic source delivery. For models that repeatedly strand replies, use "automatic" so the final assistant turn is the visible reply path, switch to a stronger tool-calling model, inspect the gateway verbose log for the suppressed payload summary, or set messages.groupChat.visibleReplies: "automatic" to use visible final replies for every group/channel request.
When 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.
This rule applies to normal agent final text. Plugin-owned conversation bindings use the owning plugin's returned reply as the visible response for claimed bound-thread turns; the plugin does not need to call message(action=send) for those binding replies.
Troubleshooting: group @mention triggers typing then silence (no error)
Symptom: a group/channel @mention shows the typing indicator and the gateway log reports dispatch complete (queuedFinal=false, replies=0), but no message lands in the room. DMs to the same agent reply normally.
Cause: the group/channel visible-reply mode resolves to "message_tool", so OpenClaw runs the turn but suppresses final assistant text unless the agent calls message(action=send). There is no NO_REPLY contract in this mode; no message-tool call means the original final text is private. For substantive source turns OpenClaw now attempts one guarded recovery retry; short notes, explicit silence, room events, send-policy-denied turns, and already delivered turns are not retried. Normal group and channel turns default to "automatic", so this symptom only appears when messages.groupChat.visibleReplies (or global messages.visibleReplies) is explicitly set to "message_tool". Harness defaultVisibleReplies does not apply here, the group/channel resolver ignores it; it only affects direct/source chats (the Codex harness suppresses direct-chat finals that way).
Fix: either pick a stronger tool-calling model, remove the explicit "message_tool" override to fall back to the "automatic" default, or set messages.groupChat.visibleReplies: "automatic" to force visible replies for every group/channel request. A substantive stranded final should no longer end as silent success; it should either recover through one message(action=send) retry or show the sanitized delivery-failure diagnostic. The gateway hot-reloads messages config after the file is saved; only restart the gateway when file watching or config reload is disabled in the deployment.
Mention types:
- Metadata mentions: Native platform @-mentions. Ignored in WhatsApp self-chat mode.
- Text patterns: Safe regex patterns in
agents.entries.*.groupChat.mentionPatterns. Invalid patterns and unsafe nested repetition are ignored. - Mention gating is enforced only when detection is possible (native mentions or at least one pattern).
{
messages: {
visibleReplies: "automatic", // force old automatic final replies for direct/source chats
groupChat: {
historyLimit: 50,
unmentionedInbound: "room_event", // always-on unmentioned room chatter becomes quiet context
visibleReplies: "message_tool", // opt-in; require message(action=send) for visible room replies
},
},
agents: {
entries: {
main: {
default: true,
groupChat: { mentionPatterns: ["@openclaw", "openclaw"] },
},
},
},
}
messages.groupChat.historyLimit sets the global default. Channels can override with channels.<channel>.historyLimit (or per-account). Set 0 to disable.
messages.groupChat.unmentionedInbound: "room_event" submits unmentioned always-on group/channel messages as quiet room context on supported channels. Mentioned messages, commands, and direct messages remain user requests. See Ambient room events for complete Discord, Slack, and Telegram examples.
messages.visibleReplies is the global source-event default; messages.groupChat.visibleReplies overrides it for group/channel source events. When messages.visibleReplies is unset, direct/source chats use the selected runtime or harness default, but internal WebChat direct turns use automatic final delivery for Pi/Codex prompt parity. Set messages.visibleReplies: "message_tool" to intentionally require message(action=send) for visible output. Channel allowlists and mention gating still decide whether an event is processed.
DM history limits
{
channels: {
telegram: {
dmHistoryLimit: 30,
dms: {
"123456789": { historyLimit: 50 },
},
},
},
}
Resolution: per-DM override → provider default → no limit (all retained).
This resolver reads channels.<provider>.dmHistoryLimit and channels.<provider>.dms.<id>.historyLimit for any channel whose session key follows the standard provider:direct:<id> (or legacy provider:dm:<id>) shape, so it works across bundled and plugin channels alike, not just a fixed list.
Self-chat mode
Include your own number in allowFrom to enable self-chat mode (ignores native @-mentions, only responds to text patterns):
{
channels: {
whatsapp: {
allowFrom: ["+15555550123"],
groups: { "*": { requireMention: true } },
},
},
agents: {
entries: {
main: {
default: true,
groupChat: { mentionPatterns: ["reisponde", "@openclaw"] },
},
},
},
}
Commands (chat command handling)
{
commands: {
native: "auto", // register native commands when supported
nativeSkills: "auto", // register native skill commands when supported
text: true, // parse /commands in chat messages
bash: false, // allow ! (alias: /bash)
bashForegroundMs: 2000,
config: false, // allow /config
mcp: false, // allow /mcp
plugins: false, // allow /plugins
debug: false, // allow /debug
restart: true, // allow /restart + external SIGUSR1 restart requests
ownerAllowFrom: ["discord:123456789012345678"],
allowFrom: {
"*": ["user1"],
discord: ["user:123"],
},
},
}
Command details
- This block sets up the command surfaces. For the list of commands that ship built-in and bundled, check Slash Commands.
- This page serves as a config-key reference, not the complete command catalog. Commands tied to channels or plugins, like QQ Bot
/bot-ping/bot-help/bot-logs, LINE/card, device-pair/pair, memory/dreaming, and Talk/voice, appear in their respective channel/plugin docs and in Slash Commands. - Text commands have to be standalone messages that start with
/. - Setting
native: "auto"activates native commands for Discord and Telegram, while Slack stays off. - Setting
nativeSkills: "auto"activates native skill commands for Discord and Telegram, leaving Slack off. - You can override per channel with
channels.discord.commands.native(a bool or"auto"). On Discord,falsebypasses native command registration and cleanup during startup. - Use
channels.<provider>.commands.nativeSkillsto override native skill registration per channel. channels.telegram.customCommandsappends extra entries to the Telegram bot menu.bash: trueturns on! <cmd>for the host shell, which needstools.elevated.enabledand a sender listed intools.elevated.allowFrom.<channel>.config: trueturns on/config(which reads and writesopenclaw.json). For gatewaychat.sendclients, persistent/config set|unsetwrites also demandoperator.admin; read-only/config showremains accessible to normal write-scoped operator clients.mcp: trueturns on/mcpfor MCP server config managed by OpenClaw undermcp.servers.plugins: trueturns on/pluginsfor plugin discovery, installation, and enable or disable controls.channels.<provider>.configWritescontrols config mutations per channel, defaulting to true.- For channels with multiple accounts,
channels.<provider>.accounts.<id>.configWritesalso controls writes aimed at that account, such as/allowlist --config --account <id>or/config set channels.<provider>.accounts.<id>.... restart: falseturns off/restartand externalSIGUSR1restart requests. The default istrue.ownerAllowFromis the explicit owner allowlist for owner-only commands and owner-gated channel actions, and it is distinct fromallowFrom.allowFromapplies per provider. When set, it becomes the only source of authorization for commands and directives.- When
allowFromis not set, command authorization follows channel allowlists and pairing state. Access-group entries in channel allowlists get resolved automatically. - Command docs map:
Related
- Configuration reference, top-level keys
- Configuration, agents
- Channels overview