Summary
A one-shot command that only needs the Discord REST API currently opens a Gateway connection and requests every intent, including all privileged intents.
I encountered this while trying to inspect the reactions on one known message:
export DISCORD_BOT_TOKEN="<valid bot token>"
uv run discli reaction list <channel_id> <message_id>
The channel ID and message ID were valid, and the token authenticated successfully. However, the command failed before it attempted to fetch the message because the application did not have every privileged Gateway intent enabled.
Environment
- macOS
discord-cli-agent==0.9.1
- Python 3.12
discord.py>=2.7.1
- discli run from the repository with
uv run
- The same bot token was already being used by a separate, long-running bot process with its own Gateway session
Actual behavior
The command fails with:
discord.errors.PrivilegedIntentsRequired:
Shard ID None is requesting privileged intents that have not been
explicitly enabled in the developer portal.
The traceback points to run_action(), where discli constructs a client with all intents and starts a Gateway connection:
intents = discord.Intents.all()
client = discord.Client(intents=intents)
...
await client.start(token)
Source:
The reaction command itself only does the following after the Gateway becomes ready:
ch = resolve_channel(client, channel)
msg = await ch.fetch_message(int(message_id))
reactions = [
{"emoji": str(reaction.emoji), "count": reaction.count}
for reaction in msg.reactions
]
The message is never fetched because Discord rejects the unnecessary Gateway IDENTIFY first. The failed startup also prints an Unclosed connector warning because the client is not closed when client.start() fails before on_ready.
Expected behavior
reaction list should use the REST API without opening a Gateway WebSocket.
Fetching one known message is supported by:
GET /channels/{channel.id}/messages/{message.id}
Authorization: Bot <token>
For a guild channel, Discord documents VIEW_CHANNEL and READ_MESSAGE_HISTORY as the required permissions. Reading the message's reaction summary does not require Presence, Server Members, Message Content, or reaction event intents.
The command should therefore work when:
- the bot token is valid;
- the bot can view the channel; and
- the bot has permission to read message history.
It should not require users to enable unrelated privileged intents.
Analysis
The behavior appears to come from using one shared Gateway-based runner for both one-shot REST commands and long-running event consumers.
discord.Client.start() performs login and then connects to the Gateway. Because the runner uses discord.Intents.all(), every invocation sends an IDENTIFY requesting GUILD_PRESENCES, GUILD_MEMBERS, and MESSAGE_CONTENT, even if the command only performs an HTTP request.
This has several consequences:
- REST-only commands fail unless unrelated privileged intents are enabled.
- Users are encouraged to grant sensitive access that the requested operation does not need.
- Every CLI invocation creates an unnecessary short-lived Gateway connection and
IDENTIFY attempt.
- This adds avoidable session-start pressure for applications that already have a long-running Gateway bot.
- Discord's Gateway documentation limits clients to 1,000
IDENTIFY calls per 24 hours across the application; reaching that limit terminates active sessions and resets the bot token.
- Resource resolution currently uses cache-only methods such as
client.get_channel(), which unnecessarily couples otherwise REST-capable commands to Gateway-populated state.
This is broader than reaction list: other one-shot commands using the same run_discord() path also open a Gateway session before performing REST operations.
Suggested direction
A reasonable separation would be:
- Use an HTTP-only lifecycle for one-shot commands: authenticate with
Client.login(), resolve resources with REST fetch methods such as fetch_channel(), perform the action, and close the client in finally.
- Reserve
Client.start() and Gateway sessions for commands that actually consume real-time events or voice state, such as listen, serve, and voice functionality.
- For Gateway commands, request only the intents required by the selected features instead of
Intents.all().
- Handle
PrivilegedIntentsRequired explicitly and always close the client on startup failures.
References
Summary
A one-shot command that only needs the Discord REST API currently opens a Gateway connection and requests every intent, including all privileged intents.
I encountered this while trying to inspect the reactions on one known message:
The channel ID and message ID were valid, and the token authenticated successfully. However, the command failed before it attempted to fetch the message because the application did not have every privileged Gateway intent enabled.
Environment
discord-cli-agent==0.9.1discord.py>=2.7.1uv runActual behavior
The command fails with:
The traceback points to
run_action(), where discli constructs a client with all intents and starts a Gateway connection:Source:
src/discli/client.pysrc/discli/commands/reaction.pyThe reaction command itself only does the following after the Gateway becomes ready:
The message is never fetched because Discord rejects the unnecessary Gateway
IDENTIFYfirst. The failed startup also prints anUnclosed connectorwarning because the client is not closed whenclient.start()fails beforeon_ready.Expected behavior
reaction listshould use the REST API without opening a Gateway WebSocket.Fetching one known message is supported by:
For a guild channel, Discord documents
VIEW_CHANNELandREAD_MESSAGE_HISTORYas the required permissions. Reading the message's reaction summary does not require Presence, Server Members, Message Content, or reaction event intents.The command should therefore work when:
It should not require users to enable unrelated privileged intents.
Analysis
The behavior appears to come from using one shared Gateway-based runner for both one-shot REST commands and long-running event consumers.
discord.Client.start()performs login and then connects to the Gateway. Because the runner usesdiscord.Intents.all(), every invocation sends anIDENTIFYrequestingGUILD_PRESENCES,GUILD_MEMBERS, andMESSAGE_CONTENT, even if the command only performs an HTTP request.This has several consequences:
IDENTIFYattempt.IDENTIFYcalls per 24 hours across the application; reaching that limit terminates active sessions and resets the bot token.client.get_channel(), which unnecessarily couples otherwise REST-capable commands to Gateway-populated state.This is broader than
reaction list: other one-shot commands using the samerun_discord()path also open a Gateway session before performing REST operations.Suggested direction
A reasonable separation would be:
Client.login(), resolve resources with REST fetch methods such asfetch_channel(), perform the action, and close the client infinally.Client.start()and Gateway sessions for commands that actually consume real-time events or voice state, such aslisten,serve, and voice functionality.Intents.all().PrivilegedIntentsRequiredexplicitly and always close the client on startup failures.References
Client.start()Client.login()