Discord access control: DM policy, allowlists, and role routing

Explains who can reach the Discord bot, which guild channels it responds in, and what actions it may perform. Covers DM policy, guild allowlists, mention gating, role routing, command auth, and action gates.

Read this when

  • Deciding who may talk to the Discord bot and where
  • Routing guild channels or roles to specific agents
  • Gating Discord message actions and native commands

Who can reach the bot, which guild channels it responds in, and what Discord actions it may perform.

Access control and routing

DM policy

DM access is governed by channels.discord.dmPolicy. The DM allowlist lives in channels.discord.allowFrom.

  • pairing (default)
  • allowlist (needs at least one sender in allowFrom)
  • open (needs channels.discord.allowFrom to contain "*")
  • disabled

When DM policy is not open, users who are unknown get blocked (in pairing mode they are instead prompted to pair).

Precedence across multiple accounts:

  • An omitted account dmPolicy and groupPolicy take their values from the channel root. Policies set explicitly on an account take priority; if neither scope is set, the defaults stay pairing and allowlist respectively.
  • Only the default account is affected by channels.discord.accounts.default.allowFrom.
  • Within a single account, allowFrom outranks the legacy dm.allowFrom.
  • A named account inherits channels.discord.allowFrom whenever its own allowFrom and legacy dm.allowFrom are unset.
  • Named accounts do not inherit channels.discord.accounts.default.allowFrom.

For compatibility, legacy channels.discord.dm.policy and channels.discord.dm.allowFrom are still read. Where access would not change, openclaw doctor --fix migrates them into dmPolicy and allowFrom.

DM target format used for delivery:

  • user:<id>
  • <@id> mention

When a channel default is active, bare numeric IDs usually resolve as channel IDs. For compatibility, though, any ID present in the account's effective DM allowFrom is treated as a user DM target.

Access groups

Dynamic accessGroup:<name> entries in channels.discord.allowFrom can be used for Discord DMs and for authorizing text commands.

Message channels share access group names. For a static group whose members are written in each channel's usual allowFrom syntax, use type: "message.senders". Use type: "discord.channelAudience" when membership should instead be defined dynamically by the current ViewChannel audience of a Discord channel. Shared access-group behavior: Access groups.

{
  accessGroups: {
    operators: {
      type: "message.senders",
      members: {
        "*": ["global-owner-id"],
        discord: ["discord:123456789012345678"],
        telegram: ["987654321"],
      },
    },
  },
  channels: {
    discord: {
      dmPolicy: "allowlist",
      allowFrom: ["accessGroup:operators"],
    },
  },
}

Discord text channels have no member list of their own. Membership under type: "discord.channelAudience" is modeled as follows: the DM sender belongs to the configured guild and holds effective ViewChannel permission on the configured channel at that moment, after role and channel overwrites are applied.

Example: let anyone who can see #maintainers DM the bot, and keep DMs closed for everyone else.

{
  accessGroups: {
    maintainers: {
      type: "discord.channelAudience",
      guildId: "1456350064065904867",
      channelId: "1456744319972282449",
      membership: "canViewChannel",
    },
  },
  channels: {
    discord: {
      dmPolicy: "allowlist",
      allowFrom: ["accessGroup:maintainers"],
    },
  },
}

Dynamic and static entries can be combined:

{
  accessGroups: {
    maintainers: {
      type: "discord.channelAudience",
      guildId: "1456350064065904867",
      channelId: "1456744319972282449",
    },
  },
  channels: {
    discord: {
      dmPolicy: "allowlist",
      allowFrom: ["accessGroup:maintainers", "discord:123456789012345678"],
    },
  },
}

A failed lookup grants no access. The DM sender is treated as unauthorized if Discord returns Missing Access, if the member lookup fails, or if the channel sits in a different guild.

Turn on Server Members Intent in the Discord Developer Portal when you use channel-audience access groups. Because DMs carry no guild member state, OpenClaw looks up the member through Discord REST at the time of authorization.

Guild policy

How guilds are handled is set by channels.discord.groupPolicy:

  • open

  • allowlist

  • disabled

    When channels.discord exists, the secure baseline is allowlist.

    allowlist behavior:

  • the guild has to match channels.discord.guilds (id is preferred, a slug is also accepted)

  • sender allowlists are optional: users (stable IDs are recommended) and roles (role IDs only); once either one is set, a sender is allowed when it matches users OR roles

  • matching by name or tag is off by default; turn on channels.discord.dangerouslyAllowNameMatching: true only as a break-glass compatibility mode

  • users does support names and tags, but IDs are the safer choice; openclaw security audit emits a warning whenever name or tag entries appear

  • when a guild has channels configured, channels that are not listed are denied

  • when a guild has no channels block, every channel in that allowlisted guild is allowed

    Example:

{
  channels: {
    discord: {
      groupPolicy: "allowlist",
      guilds: {
        "123456789012345678": {
          requireMention: true,
          ignoreOtherMentions: true,
          users: ["987654321098765432"],
          roles: ["123456789012345678"],
          channels: {
            general: { enabled: true },
            help: { enabled: true, requireMention: true },
          },
        },
      },
    },
  },
}

openclaw doctor --fix migrates the legacy per-channel allow key over to enabled.

With no channels.discord block present, the Gateway will not auto-start Discord from DISCORD_BOT_TOKEN. After that block exists, DISCORD_BOT_TOKEN still serves as the default-account token fallback. If you pass --ambient-channels, you opt into env-only auto-configuration; that route relies on groupPolicy="allowlist" and writes a warning to the log, even when channels.defaults.groupPolicy is open.

Mentions and group DMs

By default, guild messages require a mention.

Mention detection covers:

  • a direct mention of the bot
  • configured mention patterns (agents.entries.*.groupChat.mentionPatterns, with messages.groupChat.mentionPatterns as the fallback)
  • implicit reply-to-bot behavior where it is supported

For outbound Discord messages, stick to the supported mention syntax: <@USER_ID> for users, <#CHANNEL_ID> for channels, <@&ROLE_ID> for roles. The legacy <@!USER_ID> nickname mention form is not to be used.

requireMention is set per guild/channel (channels.discord.guilds...).

To accept unmentioned follow-ups in threads this bot created, including threads made through message tools, set requireMentionInBotThreads: false on a guild or channel. A channel value takes precedence over the guild setting. Setting it to true means a mention is required in those threads, even when autoThread is on; conversational reply-to-bot signals by themselves will not get around that. Leaving it out keeps the existing behavior: threads owned by the bot skip mentions only if autoThread is enabled. Sessions bound to a thread keep their own route activation rules.

Fold this into the existing guild entry without disturbing its channel and sender allowlists:

{
  channels: {
    discord: {
      intents: { messageContent: true },
      guilds: {
        "123456789012345678": {
          requireMention: true,
          requireMentionInBotThreads: false,
        },
      },
    },
  },
}

For a named account, use channels.discord.accounts.<accountId>.intents and .guilds. To scope the override to a single parent channel, add requireMentionInBotThreads: false to that channel's existing guild channels.<channelId> entry. A new channel map also modifies the guild allowlist.

The guild/channel and sender allowlists continue to apply. Parent channels, along with threads started by other users, keep whatever mention requirements they are configured with; an unknown thread owner does not switch the override on. Turn on Message Content Intent in the Discord Developer Portal and also set intents.messageContent in OpenClaw. Without both, Discord leaves out the content of ordinary unmentioned guild messages. To verify, have the bot create a thread, then send an unmentioned follow-up there from an allowed account. Other channels are covered by the bot-created thread policy.

ignoreOtherMentions can optionally drop messages aimed at a different identity rather than the bot. That includes explicit user/role mentions (but not @everyone/@here) and replies to another non-webhook bot. A direct mention of the current bot still takes priority.

Group DMs:

  • default: ignored (dm.groupEnabled=false)
  • optional allowlist through dm.groupChannels (channel IDs or slugs)

Guild channel maps are allowlists

When a guild entry has no channels map, the bot may work in every channel it can see, as long as the guild's requireMention and users rules permit it. The moment you add a single channel entry, that map becomes an allowlist: a channel that no entry matches is denied outright, not just left at the guild defaults.

This catches people off guard when they add one channel to give it special settings, then discover the bot has gone quiet in every other channel. Keep the rest of the guild reachable with the "*" wildcard key:

{
  channels: {
    discord: {
      guilds: {
        YOUR_SERVER_ID: {
          requireMention: true,
          users: ["YOUR_USER_ID"],
          channels: {
            // always-on room: everyone in it can talk to the bot, no mention needed
            YOUR_CHANNEL_ID: { enabled: true, requireMention: false, users: ["*"] },
            // every other channel keeps the guild defaults
            "*": { enabled: true, requireMention: true },
          },
        },
      },
    },
  },
}

Because channel entries take precedence over guild-level values, a channel entry with users: ["*"] opens that one room to any sender even if the guild users list is narrow. Entries are matched by channel ID, name, or slug, and a thread falls back to the entry of its parent channel.

Channel ID entries take priority; name or slug keys, once resolved at startup or on a policy reload, bind tool and mention policies to that channel ID, so a thread or a renamed channel sharing the same name does not inherit them. Name keys that stay unresolved still match by name, so give entries that grant extra tools a channel ID.

Where a thread or forum post matches a parent channel entry, the tool policy configured on that entry, sender overrides included, carries over into the agent. An explicit thread ID entry wins over the parent.

Resolved guild-name entries likewise keep their tool policies bound to the guild ID. An explicit guild ID entry wins over a resolved name, and on policy reload each account replaces its bindings.

Applying access-policy changes

Policy-only changes made in the Control UI for running Discord accounts take effect via the Gateway's validated runtime config publication. No restart of the Discord connection is needed, and you do not have to wait for active Control UI turns to complete. This applies to groupPolicy, dmPolicy, allowFrom, dm, guilds, allowBots, and dangerouslyAllowNameMatching, at channels.discord as well as under channels.discord.accounts.<accountId>.

The published policy governs new messages and interactions, covering guild/channel membership, user and role allowlists, and mention requirements. Before admission, name-based entries are resolved and cached against the policy revision; work that was already admitted keeps its existing context. Should a name-policy lookup fail to complete within an interaction's response budget, components display an ephemeral policy-updating message and autocomplete returns no choices. The resolved policy is used by a later interaction, and the expired interaction is never resumed.

Restart-required settings still follow the channel's restart path and drain deferral: token, application ID, proxy, intents, command registration, voice configuration, and account enablement. When a write mixes policy settings with restart-required settings, it remains a single deferred transaction. A manual channel stop/start reads the committed config; it does not publish a pending transport change from disk.

Role-based agent routing

To route Discord guild members to different agents by role ID, use bindings[].match.roles. Role-based bindings take role IDs only, and they are evaluated after peer or parent-peer bindings and before guild-only bindings. Where a binding also sets other match fields (for example peer + guildId + roles), every configured field must match.

{
  bindings: [
    {
      agentId: "opus",
      match: {
        channel: "discord",
        guildId: "123456789012345678",
        roles: ["111111111111111111"],
      },
    },
    {
      agentId: "sonnet",
      match: {
        channel: "discord",
        guildId: "123456789012345678",
      },
    },
  ],
}

Native commands and command auth

  • commands.native defaults to "auto" and is enabled for Discord.
  • Per-channel override: channels.discord.commands.native.
  • commands.native=false skips Discord slash-command registration and cleanup during startup. Commands registered previously may stay visible in Discord until you remove them from the Discord app.
  • Native command auth relies on the same Discord allowlists/policies that normal message handling uses.
  • Unauthorized users may still see commands in the Discord UI; execution enforces OpenClaw auth and replies "not authorized".
  • Default slash command settings: ephemeral: true (channels.discord.slashCommand.ephemeral).

See Slash commands for the command catalog and behavior.

Tools and action gates

Discord message actions span messaging, channel admin, moderation, presence, and metadata.

Core examples:

  • messaging: sendMessage, readMessages, editMessage, deleteMessage, threadReply
  • reactions: react, reactions, emoji-list
  • moderation: timeout, kick, ban
  • presence: setPresence

To discover the current server's custom emoji, use emoji-list:

{ "action": "emoji-list", "channel": "discord", "limit": 25 }

guildId defaults to the current conversation's server; supply it explicitly to query another server. Results are sorted by name, and limit defaults to and cannot exceed 100:

{
  "ok": true,
  "emojis": [
    { "name": "dance", "identifier": "dance:456", "animated": true },
    { "name": "party", "identifier": "party:123" }
  ]
}

Pass identifier directly to react. Unicode emoji, custom name:id identifiers, and the <:name:id> or <a:name:id> forms are all accepted by Discord. emoji-list, react, and reactions are all controlled by channels.discord.actions.reactions.

The event-create action accepts an optional image parameter (URL or local file path) to set the scheduled event cover image.

The list-pins action retrieves all pinned messages across Discord's paginated pin list. Pin actions are controlled by channels.discord.actions.pins.

Action gates live under channels.discord.actions.*.

Default gate behavior:

Action groupDefault
reactions, messages, threads, pins, polls, search, memberInfo, roleInfo, channelInfo, channels, voiceStatus, events, stickers, emojiUploads, stickerUploads, permissionsenabled
rolesdisabled
moderationdisabled
presencedisabled
2,098 words · updated Oct 26, 2026