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 inallowFrom)open(needschannels.discord.allowFromto 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
dmPolicyandgroupPolicytake their values from the channel root. Policies set explicitly on an account take priority; if neither scope is set, the defaults staypairingandallowlistrespectively. - Only the
defaultaccount is affected bychannels.discord.accounts.default.allowFrom. - Within a single account,
allowFromoutranks the legacydm.allowFrom. - A named account inherits
channels.discord.allowFromwhenever its ownallowFromand legacydm.allowFromare 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 -
disabledWhen
channels.discordexists, the secure baseline isallowlist.allowlistbehavior: -
the guild has to match
channels.discord.guilds(idis preferred, a slug is also accepted) -
sender allowlists are optional:
users(stable IDs are recommended) androles(role IDs only); once either one is set, a sender is allowed when it matchesusersORroles -
matching by name or tag is off by default; turn on
channels.discord.dangerouslyAllowNameMatching: trueonly as a break-glass compatibility mode -
usersdoes support names and tags, but IDs are the safer choice;openclaw security auditemits a warning whenever name or tag entries appear -
when a guild has
channelsconfigured, channels that are not listed are denied -
when a guild has no
channelsblock, every channel in that allowlisted guild is allowedExample:
{
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, withmessages.groupChat.mentionPatternsas 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.nativedefaults to"auto"and is enabled for Discord.- Per-channel override:
channels.discord.commands.native. commands.native=falseskips 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 group | Default |
|---|---|
| reactions, messages, threads, pins, polls, search, memberInfo, roleInfo, channelInfo, channels, voiceStatus, events, stickers, emojiUploads, stickerUploads, permissions | enabled |
| roles | disabled |
| moderation | disabled |
| presence | disabled |