feat: support * wildcard in platform allowlists and improve WhatsApp docs

* docs: clarify WhatsApp allowlist behavior and document WHATSAPP_ALLOW_ALL_USERS

- Add WHATSAPP_ALLOW_ALL_USERS and WHATSAPP_DEBUG to env vars reference
- Warn that * is not a wildcard and silently blocks all messages
- Show WHATSAPP_ALLOWED_USERS as optional, not required
- Update troubleshooting with the * trap and debug mode tip
- Fix Security section to mention the allow-all alternative

Prompted by a user report in Discord where WHATSAPP_ALLOWED_USERS=*
caused all incoming messages to be silently dropped at the bridge level.

* feat: support * wildcard in platform allowlists

Follow the precedent set by SIGNAL_GROUP_ALLOWED_USERS which already
supports * as an allow-all wildcard.

Bridge (allowlist.js): matchesAllowedUser() now checks for * in the
allowedUsers set before iterating sender aliases.

Gateway (run.py): _is_authorized() checks for * in allowed_ids after
parsing the allowlist. This is generic — works for all platforms, not
just WhatsApp.

Updated docs to document * as a supported value instead of warning
against it. Added WHATSAPP_ALLOW_ALL_USERS and WHATSAPP_DEBUG to
the env vars reference.

Tests: JS allowlist test + 2 Python gateway tests (WhatsApp + Telegram
to verify cross-platform behavior).
This commit is contained in:
Teknium
2026-03-31 10:42:03 -07:00
committed by GitHub
parent cca0996a28
commit 84a541b619
6 changed files with 81 additions and 5 deletions

View File

@@ -1650,6 +1650,11 @@ class GatewayRunner:
if global_allowlist:
allowed_ids.update(uid.strip() for uid in global_allowlist.split(",") if uid.strip())
# "*" in any allowlist means allow everyone (consistent with
# SIGNAL_GROUP_ALLOWED_USERS precedent)
if "*" in allowed_ids:
return True
check_ids = {user_id}
if "@" in user_id:
check_ids.add(user_id.split("@")[0])

View File

@@ -68,6 +68,11 @@ export function matchesAllowedUser(senderId, allowedUsers, sessionDir) {
return true;
}
// "*" means allow everyone (consistent with SIGNAL_GROUP_ALLOWED_USERS)
if (allowedUsers.has('*')) {
return true;
}
const aliases = expandWhatsAppIdentifiers(senderId, sessionDir);
for (const alias of aliases) {
if (allowedUsers.has(alias)) {

View File

@@ -45,3 +45,15 @@ test('matchesAllowedUser accepts mapped lid sender when allowlist only contains
rmSync(sessionDir, { recursive: true, force: true });
}
});
test('matchesAllowedUser treats * as allow-all wildcard', () => {
const sessionDir = mkdtempSync(path.join(os.tmpdir(), 'hermes-wa-allowlist-'));
try {
const allowedUsers = parseAllowedUsers('*');
assert.equal(matchesAllowedUser('19175395595@s.whatsapp.net', allowedUsers, sessionDir), true);
assert.equal(matchesAllowedUser('267383306489914@lid', allowedUsers, sessionDir), true);
} finally {
rmSync(sessionDir, { recursive: true, force: true });
}
});

View File

@@ -90,6 +90,46 @@ def test_whatsapp_lid_user_matches_phone_allowlist_via_session_mapping(monkeypat
assert runner._is_user_authorized(source) is True
def test_star_wildcard_in_allowlist_authorizes_any_user(monkeypatch):
"""WHATSAPP_ALLOWED_USERS=* should act as allow-all wildcard."""
_clear_auth_env(monkeypatch)
monkeypatch.setenv("WHATSAPP_ALLOWED_USERS", "*")
runner, _adapter = _make_runner(
Platform.WHATSAPP,
GatewayConfig(platforms={Platform.WHATSAPP: PlatformConfig(enabled=True)}),
)
source = SessionSource(
platform=Platform.WHATSAPP,
user_id="99998887776@s.whatsapp.net",
chat_id="99998887776@s.whatsapp.net",
user_name="stranger",
chat_type="dm",
)
assert runner._is_user_authorized(source) is True
def test_star_wildcard_works_for_any_platform(monkeypatch):
"""The * wildcard should work generically, not just for WhatsApp."""
_clear_auth_env(monkeypatch)
monkeypatch.setenv("TELEGRAM_ALLOWED_USERS", "*")
runner, _adapter = _make_runner(
Platform.TELEGRAM,
GatewayConfig(platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="t")}),
)
source = SessionSource(
platform=Platform.TELEGRAM,
user_id="123456789",
chat_id="123456789",
user_name="stranger",
chat_type="dm",
)
assert runner._is_user_authorized(source) is True
@pytest.mark.asyncio
async def test_unauthorized_dm_pairs_by_default(monkeypatch):
_clear_auth_env(monkeypatch)

View File

@@ -170,7 +170,9 @@ For native Anthropic auth, Hermes prefers Claude Code's own credential files whe
| `SLACK_HOME_CHANNEL_NAME` | Display name for the Slack home channel |
| `WHATSAPP_ENABLED` | Enable the WhatsApp bridge (`true`/`false`) |
| `WHATSAPP_MODE` | `bot` (separate number) or `self-chat` (message yourself) |
| `WHATSAPP_ALLOWED_USERS` | Comma-separated phone numbers (with country code, no `+`) |
| `WHATSAPP_ALLOWED_USERS` | Comma-separated phone numbers (with country code, no `+`), or `*` to allow all senders |
| `WHATSAPP_ALLOW_ALL_USERS` | Allow all WhatsApp senders without an allowlist (`true`/`false`) |
| `WHATSAPP_DEBUG` | Log raw message events in the bridge for troubleshooting (`true`/`false`) |
| `SIGNAL_HTTP_URL` | signal-cli daemon HTTP endpoint (for example `http://127.0.0.1:8080`) |
| `SIGNAL_ACCOUNT` | Bot phone number in E.164 format |
| `SIGNAL_ALLOWED_USERS` | Comma-separated E.164 phone numbers or UUIDs |

View File

@@ -94,9 +94,20 @@ Add the following to your `~/.hermes/.env` file:
# Required
WHATSAPP_ENABLED=true
WHATSAPP_MODE=bot # "bot" or "self-chat"
# Access control — pick ONE of these options:
WHATSAPP_ALLOWED_USERS=15551234567 # Comma-separated phone numbers (with country code, no +)
# WHATSAPP_ALLOWED_USERS=* # OR use * to allow everyone
# WHATSAPP_ALLOW_ALL_USERS=true # OR set this flag instead (same effect as *)
```
:::tip Allow-all shorthand
Setting `WHATSAPP_ALLOWED_USERS=*` allows **all** senders (equivalent to `WHATSAPP_ALLOW_ALL_USERS=true`).
This is consistent with [Signal group allowlists](/docs/reference/environment-variables).
To use the pairing flow instead, remove both variables and rely on the
[DM pairing system](/docs/user-guide/security#dm-pairing-system).
:::
Optional behavior settings in `~/.hermes/config.yaml`:
```yaml
@@ -174,7 +185,7 @@ whatsapp:
| **Bridge crashes or reconnect loops** | Restart the gateway, update Hermes, and re-pair if the session was invalidated by a WhatsApp protocol change. |
| **Bot stops working after WhatsApp update** | Update Hermes to get the latest bridge version, then re-pair. |
| **macOS: "Node.js not installed" but node works in terminal** | launchd services don't inherit your shell PATH. Run `hermes gateway install` to re-snapshot your current PATH into the plist, then `hermes gateway start`. See the [Gateway Service docs](./index.md#macos-launchd) for details. |
| **Messages not being received** | Verify `WHATSAPP_ALLOWED_USERS` includes the sender's number (with country code, no `+` or spaces). |
| **Messages not being received** | Verify `WHATSAPP_ALLOWED_USERS` includes the sender's number (with country code, no `+` or spaces), or set it to `*` to allow everyone. Set `WHATSAPP_DEBUG=true` in `.env` and restart the gateway to see raw message events in `bridge.log`. |
| **Bot replies to strangers with a pairing code** | Set `whatsapp.unauthorized_dm_behavior: ignore` in `~/.hermes/config.yaml` if you want unauthorized DMs to be silently ignored instead. |
---
@@ -182,9 +193,10 @@ whatsapp:
## Security
:::warning
**Always set `WHATSAPP_ALLOWED_USERS`** with phone numbers (including country code, without the `+`)
of authorized users. Without this setting, the gateway will **deny all incoming messages** as a
safety measure.
**Configure access control** before going live. Set `WHATSAPP_ALLOWED_USERS` with specific
phone numbers (including country code, without the `+`), use `*` to allow everyone, or set
`WHATSAPP_ALLOW_ALL_USERS=true`. Without any of these, the gateway **denies all incoming
messages** as a safety measure.
:::
By default, unauthorized DMs still receive a pairing code reply. If you want a private WhatsApp number to stay completely silent to strangers, set: