# NousResearch/hermes-agent · Discord wildcard user allowlist bug and fix

## Open now

- 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.

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #53910. Verify it addresses your case.
- **Recommended:** read PR #53910 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-53910`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.50`: the share of member 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.

## Closed / duplicate issues

- #22334: \[Bug\]: claw migrate emits "*" wildcard in DISCORD_ALLOWED_USERS but _is_allowed_user does not honor it, blocking all non-self users

## Prior & competing PRs

- **#22335: fix(discord): honor "*" wildcard in DISCORD_ALLOWED_USERS**. closed without merge · fix tier `suggested`
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#22334`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#53910: fix(discord): honor "*" wildcard in DISCORD_ALLOWED_USERS (#22334)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `plugins/platforms/discord/adapter.py`, `scripts/release.py`, `tests/gateway/test_discord_connect.py`, `tests/gateway/test_voice_command.py`
  - fix hunks:
  - `plugins/platforms/discord/adapter.py`

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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:
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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)
         "267614622+agt-user@users.noreply.github.com": "agt-user",  # PR #48496 salvage (telegram CLOSE-WAIT polling heartbeat, #48495)
         "80915+DavidMetcalfe@users.noreply.github.com": "DavidMetcalfe",  # PR #52272 salvage (route reasoning-model thinking-timeouts to timeout not context_overflow + reasoning-specific guidance; #52271)
    ```
  - `tests/gateway/test_discord_connect.py`

    ```diff
    @@ -1,5 +1,6 @@
     import asyncio
     import json
    +import os
     import sys
     from types import SimpleNamespace
     from unittest.mock import AsyncMock, MagicMock
    ```

    ```diff
    @@ -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):
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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."""
    ```
- **#22395: fix(discord): honor "*" wildcard in DISCORD_ALLOWED_USERS**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#53910`, `#22335`

## Fix status

- Merged to main in PR #53910 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-28`.
- Subsystem: `gateway`
