NousResearch/hermes-agent

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

0open issues
0open PRs
4items grouped
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

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #53910 fix(discord): honor "*" wildcard in DISCORD_ALLOWED_USERS (#22334)

    merged to main

    files: plugins/platforms/discord/adapter.py, scripts/release.py, tests/gateway/test_discord_connect.py, tests/gateway/test_voice_command.py

    Show 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

  • #22335 fix(discord): honor "*" wildcard in DISCORD_ALLOWED_USERS · merged via another change → #22334
  • #22395 fix(discord): honor "*" wildcard in DISCORD_ALLOWED_USERS · merged via another change → #53910, #22335
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.00 means 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 Lab

At a glance

Subsystem
gateway
Merged PRs
#53910
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
28 Jun 2026

For your agent

The same facts, typed.