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 policyBehavior
pairing (default)Unknown senders receive a one-time pairing code; owner approval required
allowlistOnly senders found in allowFrom (or paired allow store)
openPermit all inbound DMs (needs allowFrom: ["*"])
disabledDisregard all inbound DMs
Group policyBehavior
allowlist (default)Only groups that match the configured allowlist
openSkip group allowlists (mention-gating still enforced)
disabledDeny all group/room messages

Note

When a provider's groupPolicy is not set, channels.defaults.groupPolicy supplies 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 to allowlist (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:

ChannelDM key formExample
Discordraw user ID987654321
Feishufeishu:ou_...feishu:ou_a8b6cab7e945387de5f253775d9b4d85
MatrixMatrix user ID@user:matrix.org
Slackuser:U...user:U12345
Telegramraw user ID123456789
WhatsAppphone number or JID15551234567
{
  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-level groupPolicy is 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, and threadParticipation each default to true, maintaining current behavior. The names are positive: setting a flag to false prevents 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 with channels.<channel>.implicitMentions or per account with channels.<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 (default false).
  • channels.defaults.heartbeatVisibility.showAlerts: include degraded/error statuses in heartbeat output (default true).
  • channels.defaults.heartbeatVisibility.useIndicator: render compact indicator-style heartbeat output (default true).

WhatsApp

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 with type: "acp" configure persistent ACP bindings for WhatsApp DMs and groups. Use an E.164 direct number or WhatsApp group JID in match.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 default if present; otherwise the first configured account id (sorted).
  • Optional channels.whatsapp.defaultAccount overrides that fallback default account selection when it matches a configured account id.
  • Legacy single-account Baileys auth dir is migrated by openclaw doctor into whatsapp/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.botToken or channels.telegram.tokenFile (only regular files are accepted, symlinks are refused), with TELEGRAM_BOT_TOKEN serving as the fallback for the default account.
  • apiRoot points exclusively to the Telegram Bot API root. Opt for https://api.telegram.org or your own self-hosted/proxy root instead of https://api.telegram.org/bot<TOKEN>; openclaw doctor --fix strips any stray trailing /bot<TOKEN> suffix.
  • When running a self-hosted Bot API server in --local mode, trustedLocalFileRoots enumerates 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-api are linked to those roots. Any other absolute paths stay blocked.
  • An optional channels.telegram.defaultAccount can 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.defaultAccount or channels.telegram.accounts.default) to prevent fallback routing; openclaw doctor issues a warning if this is absent or invalid.
  • configWrites: false prevents Telegram-initiated config changes (supergroup ID migrations, /config set|unset).
  • actions.reactions governs both message reactions and emoji-list, which enumerates the standard and custom reactions permitted in the current chat.
  • Top-level bindings[] entries paired with type: "acp" set up persistent ACP bindings for forum topics (use the canonical chatId:topic:topicId within match.peer.id). The field semantics are detailed in ACP Agents.
  • Telegram stream previews rely on sendMessage plus editMessageText (functional in both direct and group chats).
  • network.dnsResultOrder is 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, with DISCORD_BOT_TOKEN serving 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.defaultAccount overrides the default account selection if it matches a configured account id.
  • Use user:<id> (DM) or channel:<id> (guild channel) as delivery targets; bare numeric IDs are not accepted.
  • actions.reactions governs react, reactions, and emoji-list; emoji discovery falls back to the current server unless guildId is 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: true turns them on; use allowBots: "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.botLoopProtection for 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.mentionAliases maps stable outbound @handle text 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 under channels.discord.accounts.<accountId>.mentionAliases.
  • maxLinesPerMessage (default 17) splits tall messages even when under 2000 chars.
  • channels.discord.suppressEmbeds defaults to true, so outbound URLs do not expand into Discord link previews unless disabled. Explicit embeds payloads still send normally; per-message tool calls can override with suppressEmbeds.
  • channels.discord.threadBindings controls 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 (0 disables)
    • maxAgeHours: Discord override for hard max age in hours (0 disables)
    • spawnSessions: switch for sessions_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 with type: "acp" configure persistent ACP bindings for channels and threads (use channel/thread id in match.peer.id). Field semantics are shared in ACP Agents.
  • channels.discord.agentComponents.ttlMs controls how long sent Discord component callbacks remain registered. Default 1800000 (30 minutes), maximum 86400000 (24 hours). Per-account overrides live under channels.discord.accounts.<accountId>.agentComponents.ttlMs. Prefer the shortest TTL that fits the workflow.
  • channels.discord.voice enables Discord voice channel conversations and optional auto-join + LLM + TTS overrides. Text-only Discord configs leave voice off by default; set channels.discord.voice.enabled=true to opt in.
  • channels.discord.voice.autoJoin[].whenOccupied keeps an auto-managed voice channel disconnected until a human is present, then leaves when the last human departs. It defaults to false; bots do not count as occupants, and manual or ad-hoc voice sessions are not managed by this policy.
  • channels.discord.voice.model optionally overrides the LLM model used for Discord voice channel responses.
  • channels.discord.voice.daveEncryption (default true) and channels.discord.voice.decryptionFailureTolerance (default 24) are forwarded to the @discordjs/voice DAVE options.
  • The initial @discordjs/voice Ready wait for /vc join and auto-join attempts are governed by channels.discord.voice.connectTimeoutMs, with a default of 30000.
  • The duration a disconnected voice session may take before entering reconnect signalling, after which OpenClaw terminates it, is set by channels.discord.voice.reconnectGraceMs (default 15000).
  • 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.streaming serves as the canonical stream mode key. Discord preview streaming defaults to off; to opt into one edited tool/work progress message, set streaming.mode: "progress", or pick partial or block for answer previews. Legacy flat keys (streamMode, chunkMode, blockStreaming, draftChunk, blockStreamingCoalesce) are no longer read at runtime; execute openclaw doctor --fix to 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>.presenceEvents into one configured Discord channel as agent system events. Eligible members must have visibility of channelId; public threads inherit parent visibility, whereas private threads also require membership or Manage Threads. users can further restrict that audience. Current online members are seeded from complete GUILD_CREATE snapshots, 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, 0 disables) and burstLimit/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 requires channels.discord.intents.presence=true, the privileged Presence Intent in Discord's Developer Portal, and an enabled agent heartbeat.
  • channels.discord.intents.messageContent defaults to true. Only set it to false for 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, keep requireMention: true on 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 from approvers or commands.ownerAllowFrom.
    • approvers: Discord user IDs allowed to approve exec requests. Falls back to commands.ownerAllowFrom when 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: when true, 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_ACCOUNT or GOOGLE_CHAT_SERVICE_ACCOUNT_FILE.
  • For delivery destinations, opt for spaces/<spaceId> or users/<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 botToken and appToken (with SLACK_BOT_TOKEN plus SLACK_APP_TOKEN serving as the fallback for default account environment variables).
  • HTTP mode needs botToken alongside signingSecret (configured either at the root or per-account).
  • User identity (postAs: "user") operates as the authorizing human for both posting and reading. This requires userToken and appToken when using Socket Mode, or userToken and signingSecret in 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 accommodate disabled, open, allowlist, and workspace-scoped pairing. Channel policies demand team:<team-id>:channel:<channel-id>. User policies work with either an org-wide stable user ID or team:<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 enterpriseOrgInstall key gets removed by openclaw doctor --fix at both the Slack root and account levels.
  • Plaintext strings or SecretRef objects are accepted by botToken, appToken, signingSecret, and userToken.
  • Slack account snapshots reveal per-credential source/status fields, including botTokenSource, botTokenStatus, userTokenSource, userTokenStatus, appTokenStatus, and, in HTTP mode, signingSecretStatus. When configured_unavailable appears, 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.defaultAccount takes precedence over default account selection when it matches a configured account id.
  • dm.groupEnabled and dm.groupChannels restrict 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 with conversations.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 by channels.slack.streaming.nativeTransport (default true). Legacy streamMode, boolean streaming, chunkMode, blockStreaming, blockStreamingCoalesce, and nativeStreaming values are ignored at runtime; execute openclaw doctor --fix to migrate persisted config to streaming.{mode,chunkMode,block.enabled,block.coalesce,nativeTransport}.
  • Slack's chat.postMessage link and media unfurl booleans are passed through for bot replies via unfurlLinks and unfurlMedia. unfurlLinks defaults to false, so outbound bot links stay unexpanded inline unless explicitly enabled; unfurlMedia is left out unless configured. To override the top-level value for a single account, set either at channels.slack.accounts.<accountId>.
  • Delivery targets are specified with user:<id> (for DMs) or channel:<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, typingReaction drops 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, and target ("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 via approvals.plugin for Slack-origin sessions or Slack targets. Plugin approvals draw their approvers from allowFrom with default routing, not from exec approvers.
Action groupDefaultNotes
reactionsenabledReact + list reactions
messagesenabledRead/send/edit/delete
pinsenabledPin/unpin/list
memberInfoenabledMember info
emojiListenabledList 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.callbackPath has to be a path, for instance /api/channels/mattermost/command, never a full URL.
  • commands.callbackUrl must 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.AllowedUntrustedInternalConnections to 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 @mention before replying in channels.
  • channels.mattermost.groups.<channelId>.requireMention: per-channel override for mention gating ("*" is the default).
  • Optional channels.mattermost.defaultAccount takes 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.defaultAccount takes 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.defaultAccount takes 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. Use imsg chats --limit 20 to enumerate chats.
  • In SSH deployments, cliPath is a full path located on the Gateway host. remoteHost (host or user@host) identifies the Messages Mac, and dbPath is resolved on that Mac. Supply an absolute remote database path rather than deriving it from the Gateway user's home directory.
  • When remoteHost is 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.
  • attachmentRoots and remoteAttachmentRoots constrain 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 preferred imsg RPC 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; bridge demands private-API delivery; applescript forces the public Messages automation path.
  • channels.imessage.actions.*: turn on private API actions that are additionally gated by imsg status / openclaw channels status --probe.
  • channels.imessage.includeAttachments starts disabled; set it to true before 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: true configs remain honored as a deprecated compatibility profile; catchup is disabled unless enabled.
  • channels.imessage.groups: the group registry and per-group settings. With groupPolicy: "allowlist", set either explicit chat_id keys or a "*" wildcard entry so group messages can clear the registry gate.
  • Top-level bindings[] entries carrying type: "acp" can attach iMessage conversations to persistent ACP sessions. Use a normalized handle or an explicit chat target (chat_id:*, chat_guid:*, chat_identifier:*) inside match.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 combines userId with password.
  • Matrix HTTP traffic can be directed through a designated HTTP(S) proxy using channels.matrix.proxy. Named accounts have the option to override this via channels.matrix.accounts.<id>.proxy.
  • Private or internal homeservers are supported through channels.matrix.network.dangerouslyAllowPrivateNetwork. The settings proxy and this network opt-in operate independently of each other.
  • In configurations with multiple accounts, channels.matrix.defaultAccount determines which account takes precedence.
  • By default, channels.matrix.autoJoin is set to "off", meaning invited rooms and newly created DM-style invites are not acted upon until you configure autoJoin: "allowlist" using either autoJoinAllowlist or autoJoin: "always".
  • channels.matrix.execApprovals handles Matrix-native delivery of exec approvals and the authorization of approvers.
    • enabled: options are true, false, or "auto" (the default). When set to auto, exec approvals become active as long as approvers can be identified from approvers or commands.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 default per-user groups by the routed peer, while per-room keeps 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.msteams and channels.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, and channels.irc.nickserv.*.
  • When channels.irc.defaultAccount matches 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 accountId is not provided, default is 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 into channels.<channel>.accounts.default; Matrix may instead preserve an existing matching named or default target.
  • Channel-only bindings that lack accountId continue 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 on accounts.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, false bypasses native command registration and cleanup during startup.
  • Use channels.<provider>.commands.nativeSkills to override native skill registration per channel.
  • channels.telegram.customCommands appends extra entries to the Telegram bot menu.
  • bash: true turns on ! <cmd> for the host shell, which needs tools.elevated.enabled and a sender listed in tools.elevated.allowFrom.<channel>.
  • config: true turns on /config (which reads and writes openclaw.json). For gateway chat.send clients, persistent /config set|unset writes also demand operator.admin; read-only /config show remains accessible to normal write-scoped operator clients.
  • mcp: true turns on /mcp for MCP server config managed by OpenClaw under mcp.servers.
  • plugins: true turns on /plugins for plugin discovery, installation, and enable or disable controls.
  • channels.<provider>.configWrites controls config mutations per channel, defaulting to true.
  • For channels with multiple accounts, channels.<provider>.accounts.<id>.configWrites also controls writes aimed at that account, such as /allowlist --config --account <id> or /config set channels.<provider>.accounts.<id>....
  • restart: false turns off /restart and external SIGUSR1 restart requests. The default is true.
  • ownerAllowFrom is the explicit owner allowlist for owner-only commands and owner-gated channel actions, and it is distinct from allowFrom.
  • allowFrom applies per provider. When set, it becomes the only source of authorization for commands and directives.
  • When allowFrom is not set, command authorization follows channel allowlists and pairing state. Access-group entries in channel allowlists get resolved automatically.
  • Command docs map:

6,998 words · updated Aug 24, 2026