# NousResearch/hermes-agent · WhatsApp allowlist and self-chat authorization controls

## Open now

- Nothing open on GitHub.

These issues and pull requests address authorization behavior in WhatsApp integration, focusing on how an empty or missing WHATSAPP_ALLOWED_USERS allowlist is handled and how self-chat mode responds to incoming messages from unknown senders.

## TL;DR

- 7 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #21291. Verify it addresses your case.
- **Recommended:** read PR #21291 (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: `partially_resolved` · deciding PR(s): `pr-21291`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.26`: 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

- #8389: \`\[SECURITY\] WhatsApp self-chat mode responds to ALL incoming messages (empty allowlist = allow everyone)\`
- #15108: WhatsApp setup: WHATSAPP_ALLOWED_USERS should be mandatory

## Prior & competing PRs

- **#21291: fix(whatsapp): reject strangers by default, never respond in self-chat (#8389)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `scripts/whatsapp-bridge/allowlist.js`, `scripts/whatsapp-bridge/allowlist.test.mjs`, `scripts/whatsapp-bridge/bridge.js`
  - fix hunks:
  - `scripts/whatsapp-bridge/allowlist.js`

    ```diff
    @@ -64,8 +64,12 @@ export function expandWhatsAppIdentifiers(identifier, sessionDir) {
     }
     
     export function matchesAllowedUser(senderId, allowedUsers, sessionDir) {
    +  // Empty allowlist = NO ONE allowed (secure default, #8389).  Operators
    +  // who want an open bot must set ``WHATSAPP_ALLOWED_USERS=*`` explicitly.
    +  // Previous behaviour (empty → return true) let any stranger DM the
    +  // bridge and trigger a Python-side pairing-code reply.
       if (!allowedUsers || allowedUsers.size === 0) {
    -    return true;
    +    return false;
       }
     
       // "*" means allow everyone (consistent with SIGNAL_GROUP_ALLOWED_USERS)
    ```
  - `scripts/whatsapp-bridge/allowlist.test.mjs`

    ```diff
    @@ -57,3 +57,24 @@ test('matchesAllowedUser treats * as allow-all wildcard', () => {
         rmSync(sessionDir, { recursive: true, force: true });
       }
     });
    +
    +test('matchesAllowedUser rejects everyone when allowlist is empty (#8389)', () => {
    +  // Regression guard: empty allowlist used to return true (allow-everyone),
    +  // which let any stranger DM the bridge and trigger a Python-side
    +  // pairing-code reply. Secure default is now "reject unless explicitly
    +  // configured"; operators who want an open bot must set `*`.
    +  const sessionDir = mkdtempSync(path.join(os.tmpdir(), 'hermes-wa-allowlist-'));
    +
    +  try {
    +    const empty = parseAllowedUsers('');
    +    assert.equal(empty.size, 0);
    +    assert.equal(matchesAllowedUser('[redacted:email]', empty, sessionDir), false);
    +    assert.equal(matchesAllowedUser('267383306489914@lid', empty, sessionDir), false);
    +
    +    // Null/undefined allowlist (defensive) also rejects.
    +    assert.equal(matchesAllowedUser('[redacted:email]', null, sessionDir), false);
    +    assert.equal(matchesAllowedUser('[redacted:email]', undefined, sessionDir), false);
    +  } finally {
    +    rmSync(sessionDir, { recursive: true, force: true });
    +  }
    +});
    ```
  - `scripts/whatsapp-bridge/bridge.js`

    ```diff
    @@ -229,17 +229,34 @@ async function startSocket() {
             if (!isSelfChat) continue;
           }
     
    -      // Check allowlist for messages from others (resolve LID ↔ phone aliases)
    -      if (!msg.key.fromMe && !matchesAllowedUser(senderId, ALLOWED_USERS, SESSION_DIR)) {
    -        try {
    -          console.log(JSON.stringify({
    -            event: 'ignored',
    -            reason: 'allowlist_mismatch',
    -            chatId,
    -            senderId,
    -          }));
    -        } catch {}
    -        continue;
    +      // Handle !fromMe messages (from other people) based on mode.
    +      // Self-chat mode only responds to the user's own messages to
    +      // themselves — stranger DMs / group pings must never reach the
    +      // Python gateway, otherwise a pairing-code reply fires in response
    +      // to arbitrary incoming messages (#8389).
    +      if (!msg.key.fromMe) {
    +        if (WHATSAPP_MODE === 'self-chat') {
    +          try {
    +            console.log(JSON.stringify({
    +              event: 'ignored',
    +              reason: 'self_chat_mode_rejects_non_self',
    +              chatId,
    +              senderId,
    +            }));
    +          } catch {}
    +          continue;
    +        }
    +        if (!matchesAllowedUser(senderId, ALLOWED_USERS, SESSION_DIR)) {
    +          try {
    +            console.log(JSON.stringify({
    +              event: 'ignored',
    +              reason: 'allowlist_mismatch',
    +              chatId,
    +              senderId,
    +            }));
    +          } catch {}
    +          continue;
    +        }
           }
     
           const messageContent = getMessageContent(msg);
    ```

    ```diff
    @@ -625,8 +642,12 @@ if (PAIR_ONLY) {
         console.log(`📁 Session stored in: ${SESSION_DIR}`);
         if (ALLOWED_USERS.size > 0) {
           console.log(`🔒 Allowed users: ${Array.from(ALLOWED_USERS).join(', ')}`);
    +    } else if (WHATSAPP_MODE === 'self-chat') {
    +      console.log(`🔒 Self-chat mode — only your own messages to yourself are processed.`);
         } else {
    -      console.log(`⚠️  No WHATSAPP_ALLOWED_USERS set — all messages will be processed`);
    +      console.log(`🔒 No WHATSAPP_ALLOWED_USERS set — incoming messages are rejected.`);
    +      console.log(`   Set WHATSAPP_ALLOWED_USERS=<phone> to authorize specific users,`);
    +      console.log(`   or WHATSAPP_ALLOWED_USERS=* for an explicit open bot.`);
         }
         console.log();
         startSocket();
    ```
- **#30288: fix(whatsapp): allow group policy gating in self-chat mode**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#20429`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#8410: security(whatsapp): deny unknown senders when WHATSAPP_ALLOWED_USERS is empty (#8389)**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#21291`, `#8389`, `#21291`, `#8431`
- **#8431: fix(whatsapp/bridge):  #8389 patch critical authorization bypass in self-chat mode **. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#8389`
- **#21009: fix(whatsapp): deny empty bridge allowlist**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `413990c94537e9c9da973bb21a6afcd332400b91`

## Fix status

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