Discord wildcard user allowlist bug and fix
Cluster 22334 · gateway. Data as of
Nothing open on GitHub. Fix merged in #53910 on 28 Jun 2026. Check it covers your case.
The short answer
- Known?
- Yes. 4 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #53910. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #53910. That is separate from the fix event. - Still open
- Nothing open on GitHub.
A bug was identified where the '*' wildcard in DISCORD_ALLOWED_USERS was not honored by the user-check logic, blocking non-self users. The members track the bug report and associated fix attempts.
Open issues0
Open PRs0
Fixes on record
-
#53910 fix(discord): honor "*" wildcard in DISCORD_ALLOWED_USERS (#22334)
files:
plugins/platforms/discord/adapter.py,scripts/release.py,tests/gateway/test_discord_connect.py,tests/gateway/test_voice_command.pyShow the diff
-
plugins/platforms/discord/adapter.py@@ -934,7 +934,12 @@ async def connect(self, *, is_reconnect: bool = False) -> bool: intents.dm_messages = True intents.guild_messages = True intents.members = ( - any(not entry.isdigit() for entry in self._allowed_user_ids) + # ``"*"`` is the open-mode wildcard (honored in _is_allowed_user), + # not a username to resolve, so it must not pull in the privileged + # Server Members intent — exactly the migrate-from-OpenClaw path + # the wildcard fix targets would otherwise silently fail to come + # online when Members Intent isn't enabled in the Developer Portal. + any(entry != "*" and not entry.isdigit() for entry in self._allowed_user_ids) or bool(self._allowed_role_ids) # Need members intent for role lookup ) intents.voice_states = True @@ -2766,8 +2771,12 @@ def _is_allowed_user( has_roles = bool(allowed_roles) if not has_users and not has_roles: return True - # Check user ID allowlist (works for both DMs and guild messages) - if has_users and user_id in allowed_users: + # Check user ID allowlist (works for both DMs and guild messages). + # ``"*"`` is honored as an open-mode wildcard, mirroring + # ``SIGNAL_ALLOWED_USERS`` and the existing ``DISCORD_ALLOWED_CHANNELS`` / + # ``DISCORD_IGNORED_CHANNELS`` / ``DISCORD_FREE_RESPONSE_CHANNELS`` + # semantics. This is the convention ``claw migrate`` emits ("*"). + if has_users and ("*" in allowed_users or user_id in allowed_users): return True # Role allowlist is only consulted when configured. if not has_roles: @@ -3363,6 +3372,15 @@ async def _resolve_allowed_usernames(self) -> None: for entry in self._allowed_user_ids: if entry.isdigit(): numeric_ids.add(entry) + elif entry == "*": + # Preserve the open-mode wildcard verbatim. It is not a + # username to resolve; without this branch it would land in + # ``to_resolve``, fail to match any guild member, and then be + # silently dropped from both ``self._allowed_user_ids`` and + # ``DISCORD_ALLOWED_USERS`` by the rewrite below — quietly + # undoing the wildcard fix in ``_is_allowed_user`` after the + # first ``on_ready``. + numeric_ids.add(entry) else: to_resolve.add(entry.lower()) -
scripts/release.py@@ -49,6 +49,7 @@ "[redacted:email]": "Blaryxoff", # PR #32602 salvage (deep-merge PUT /api/config to preserve unrelated sections; #13396) "[redacted:email]": "DiamondEyesFox", # PR #53351 salvage (rebaseline in-place compression flushes to prevent duplicate compacted rows; #9096) "[redacted:email]": "rlaope", # PR #35075 salvage (align cron invisible-unicode set with install-time scanner; #35075) + "[redacted:email]": "bykim0119", # PR #22335 salvage (honor "*" wildcard in DISCORD_ALLOWED_USERS; #22334) "[redacted:email]": "rebel0789", # PR #47308 salvage (redact browser_type typed text across display surfaces; #47197) "[email protected]": "agt-user", # PR #48496 salvage (telegram CLOSE-WAIT polling heartbeat, #48495) "[email protected]": "DavidMetcalfe", # PR #52272 salvage (route reasoning-model thinking-timeouts to timeout not context_overflow + reasoning-specific guidance; #52271) -
tests/gateway/test_discord_connect.py@@ -1,5 +1,6 @@ import asyncio import json +import os import sys from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock @@ -146,6 +147,13 @@ def __init__(self, *, intents, proxy=None): ("769524422783664158", False), ("abhey-gupta", True), ("769524422783664158,abhey-gupta", True), + # ``"*"`` is the open-mode wildcard, not a username to resolve, so it + # must not pull in the privileged Server Members intent. Requesting + # that intent without it being enabled in the Discord Developer Portal + # can prevent the bot from coming online at all — and that is exactly + # the migration-from-OpenClaw path the wildcard fix targets (#22334). + ("*", False), + ("769524422783664158,*", False), ], ) async def test_connect_only_requests_members_intent_when_needed(monkeypatch, allowed_users, expected_members_intent): @@ -182,6 +190,50 @@ def fake_bot_factory(*, command_prefix, intents, proxy=None, allowed_mentions=No await adapter.disconnect() [redacted:email] [redacted:email]( + "initial_allowed", + [ + {"*"}, + {"769524422783664158", "*"}, + ], +) +async def test_resolve_allowed_usernames_preserves_wildcard(monkeypatch, initial_allowed): + """Regression (#22334): ``_resolve_allowed_usernames`` must not strip ``"*"``. + + The method splits entries into numeric-IDs (kept) vs non-numeric (treated + as usernames to resolve), then rewrites ``self._allowed_user_ids`` and the + ``DISCORD_ALLOWED_USERS`` env var from the numeric set. Since ``"*"`` is + non-numeric, without the wildcard branch it ends up in the resolution + bucket, fails to match any guild member, and is silently dropped from both + the in-memory set and the env var on the first ``on_ready`` — quietly + undoing the wildcard fix in ``_is_allowed_user`` for every subsequent + message. + """ + adapter = DiscordAdapter(PlatformConfig(enabled=True, token="[redacted:secret-assignment]")) + adapter._allowed_user_ids = set(initial_allowed) + adapter._client = SimpleNamespace(guilds=[]) # no guilds → no resolution work + + monkeypatch.setenv("DISCORD_ALLOWED_USERS", ",".join(sorted(initial_allowed))) + + await adapter._resolve_allowed_usernames() + + assert "*" in adapter._allowed_user_ids, ( + "wildcard must survive _resolve_allowed_usernames; it is the open-mode " + "marker, not a username to resolve" + ) + env_entries = { + e.strip() + for e in os.environ.get("DISCORD_ALLOWED_USERS", "").split(",") + if e.strip() + } + assert "*" in env_entries, ( + "DISCORD_ALLOWED_USERS env must still contain '*' after resolution; " + "downstream readers (and any restart-time re-parse) rely on it" + ) + + + @pytest.mark.asyncio async def test_reconnect_closes_previous_client_to_prevent_zombie_websocket(monkeypatch): """Regression for #18187: calling connect() twice without disconnect() in -
tests/gateway/test_voice_command.py@@ -1223,6 +1223,32 @@ def test_is_allowed_user_not_in_list(self): adapter._allowed_user_ids = {"99"} assert adapter._is_allowed_user("42") is False + def test_is_allowed_user_wildcard_only(self): + """``DISCORD_ALLOWED_USERS="*"`` opens access to all users. + + Mirrors ``SIGNAL_ALLOWED_USERS`` and the existing + ``DISCORD_ALLOWED_CHANNELS`` / ``_IGNORED_CHANNELS`` / + ``_FREE_RESPONSE_CHANNELS`` wildcard handling. This is the + convention ``claw migrate`` emits (#22334). + """ + adapter = self._make_adapter() + adapter._allowed_user_ids = {"*"} + assert adapter._is_allowed_user("42") is True + assert adapter._is_allowed_user("999999999999999999") is True + + def test_is_allowed_user_wildcard_mixed_with_ids(self): + """``DISCORD_ALLOWED_USERS="123,*"`` honors ``*`` for any user.""" + adapter = self._make_adapter() + adapter._allowed_user_ids = {"123456789012345678", "*"} + assert adapter._is_allowed_user("42") is True + assert adapter._is_allowed_user("123456789012345678") is True + + def test_is_allowed_user_wildcard_in_dm(self): + """Wildcard short-circuits before role-auth gating, so DMs honor it too.""" + adapter = self._make_adapter() + adapter._allowed_user_ids = {"*"} + assert adapter._is_allowed_user("42", is_dm=True) is True + @pytest.mark.asyncio async def test_process_voice_input_success(self): """Successful voice input: PCM->WAV->STT->callback."""
-
2 PRs closed without merging
From the close comments: merged via another change 2
Closed issues · 1
- Closed#22334[Bug]: claw migrate emits "*" wildcard in DISCORD_ALLOWED_USERS but _is_allowed_user does not honor it, blocking all non-self users
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- gateway
- Merged PRs
- #53910
- Closed issues
- 1
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 28 Jun 2026