# How Hermes Agent Implements DM Pairing and User Allowlist Security

> Discover how Hermes Agent secures gateway access with DM pairing and user allowlist controls across platforms. Learn about its two-layer authorization model for enhanced security.

- Repository: [Nous Research/hermes-agent](https://github.com/NousResearch/hermes-agent)
- Tags: how-to-guide
- Published: 2026-03-09

---

**Hermes Agent uses a two-layer authorization model combining cryptographically-secure DM pairing codes with configurable static allowlists to control gateway access across Telegram, Discord, WhatsApp, and other platforms.**

The NousResearch/hermes-agent repository protects its messaging gateways through a defense-in-depth approach to DM pairing and user allowlist security. This system ensures that only explicitly authorized users can interact with the agent, whether through on-demand pairing workflows or pre-configured static lists.

## The Two-Layer Security Model

Hermes Agent implements authorization through two distinct but complementary layers:

1. **Dynamic DM Pairing**: A cryptographically secure, time-limited code system for on-demand user approval
2. **Static Allowlists**: Environment-variable-based user ID lists for permanent access control

These layers operate sequentially in [[`gateway/run.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/run.py)](https://github.com/NousResearch/hermes-agent/blob/main/gateway/run.py), where the `_is_user_authorized` method (around line 1060) first consults the pairing store before falling back to static allowlist checks.

## Layer 1: DM Pairing Workflow

### Code Generation and Rate Limiting

When an unauthorized user sends a direct message, the gateway invokes [`PairingStore.generate_code`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/pairing.py#L26-L66) in [`gateway/pairing.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/pairing.py). This method creates an 8-character code using an unambiguous alphabet (excluding visually similar characters like '0' and 'O') through cryptographically secure random generation.

The system enforces security through multiple mechanisms:

- **Rate limiting**: Prevents spamming of pairing requests
- **Maximum pending limits**: Caps the number of unapproved codes per platform
- **1-hour TTL**: Codes automatically expire after 60 minutes

```python
from gateway.pairing import PairingStore

store = PairingStore()
code = store.generate_code("telegram", "123456789", "Alice")
print(f"Generated pairing code: {code}")

# Output: AB6J9KLM (example)

```

### Owner Approval Process

The gateway sends the pairing code to the user with instructions for the bot owner to approve it via CLI. The owner executes:

```bash
hermes pairing approve telegram AB6J9KLM

```

This command, implemented in [[`hermes_cli/pairing.py`](https://github.com/NousResearch/hermes-agent/blob/main/hermes_cli/pairing.py)](https://github.com/NousResearch/hermes-agent/blob/main/hermes_cli/pairing.py), calls `PairingStore.approve_code`, which:

1. Validates the code exists and hasn't expired
2. Removes the pending entry
3. Persists the user to `{platform}-approved.json`

```python

# hermes_cli/pairing.py

store = PairingStore()
approved = store.approve_code("telegram", "AB6J9KLM")

# Returns: (user_id, user_name) tuple

```

### Persistent Storage

All pairing data resides in `~/.hermes/pairing/` with strict **0600** file permissions (read/write for owner only). The `_secure_write` method ensures sensitive user data remains private on disk. Approved users are stored in platform-specific JSON files (e.g., [`telegram-approved.json`](https://github.com/NousResearch/hermes-agent/blob/main/telegram-approved.json)), while pending codes track expiration timestamps and metadata.

## Layer 2: Static Allowlist Configuration

### Per-Platform Allowlists

Beyond dynamic pairing, Hermes Agent supports static allowlists through environment variables defined in [[`gateway/config.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/config.py)](https://github.com/NousResearch/hermes-agent/blob/main/gateway/config.py). Each platform accepts a comma-separated list of user IDs:

- `TELEGRAM_ALLOWED_USERS`
- `DISCORD_ALLOWED_USERS`
- `WHATSAPP_ALLOWED_USERS`
- `SLACK_ALLOWED_USERS`
- `SIGNAL_ALLOWED_USERS`

For WhatsApp specifically, the system normalizes JIDs by stripping the `@s.whatsapp.net` suffix before comparison.

### Global Overrides

The authorization logic in `_is_user_authorized` supports hierarchical overrides:

1. **Per-platform allow-all flags**: `TELEGRAM_ALLOW_ALL_USERS`, `DISCORD_ALLOW_ALL_USERS`, etc.
2. **Global allowlist**: `GATEWAY_ALLOWED_USERS` (applies across all platforms)
3. **Global allow-all flag**: `GATEWAY_ALLOW_ALL_USERS`

If any allow-all flag is set to a truthy value, authorization bypasses all list checks. Otherwise, the system evaluates the union of per-platform and global allowlists.

```python

# Example environment configuration

export TELEGRAM_ALLOWED_USERS="111111,222222"
export GATEWAY_ALLOWED_USERS="333333"
export GATEWAY_ALLOW_ALL_USERS="false"

```

## Authorization Flow in Practice

When a message arrives at the gateway, the `_handle_message` method (around line 1060 in [`gateway/run.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/run.py)) orchestrates the security checks in strict sequence:

1. **Pairing check**: Query `PairingStore.is_approved(platform, user_id)` – if True, accept immediately
2. **Allowlist evaluation**: If not paired, check environment-based allowlists and allow-all flags
3. **Unauthorized handling**: If all checks fail, either send a pairing code (DMs) or silently drop (group chats)

```python

# Simplified authorization logic from gateway/run.py

def _is_user_authorized(self, source) -> bool:
    # Layer 1: Check pairing store

    if self.pairing_store.is_approved(source.platform, source.user_id):
        return True
    
    # Layer 2: Check static allowlists

    if self._check_allow_all_flags(source.platform):
        return True
    
    return self._check_user_in_allowlists(source.user_id, source.platform)

```

## CLI Management Commands

Hermes Agent provides command-line tools for managing the pairing system without restarting the gateway.

**Approving a pending user:**

```bash
hermes pairing approve telegram AB6J9KLM

```

This validates the code, moves the user from pending to approved status, and persists the change to `~/.hermes/pairing/telegram-approved.json`.

**Revoking access:**

```bash
hermes pairing revoke telegram 123456789

```

Or programmatically:

```python
from gateway.pairing import PairingStore

store = PairingStore()
store.revoke("telegram", "123456789")

# User must request a new pairing code to regain access

```

## Security Considerations

The pairing system implements several defense mechanisms:

- **Cryptographic randomness**: Uses `secrets.choice` with an unambiguous alphabet to prevent guessing attacks
- **Time-bound exposure**: 1-hour TTL on pending codes limits the window for unauthorized approval
- **Rate limiting**: Prevents brute-force attempts to guess valid codes
- **Filesystem security**: 0600 permissions on all pairing files prevent other system users from reading approved user lists
- **Normalization**: WhatsApp JIDs are normalized to prevent bypasses using different JID formats

## Summary

- Hermes Agent implements **DM pairing and user allowlist security** through a two-layer authorization model in [`gateway/run.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/run.py)
- **Dynamic pairing** in [`gateway/pairing.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/pairing.py) generates cryptographically secure 8-character codes with 1-hour TTL, rate limiting, and secure file storage (0600 permissions)
- **Static allowlists** support per-platform (`TELEGRAM_ALLOWED_USERS`) and global (`GATEWAY_ALLOWED_USERS`) configurations with override flags
- The authorization flow checks pairing status first, then falls back to environment-based allowlists, rejecting unauthorized users with pairing prompts or silent drops
- CLI commands `hermes pairing approve` and `hermes pairing revoke` manage access without service restarts

## Frequently Asked Questions

### How does the pairing code generation prevent guessing attacks?

The `PairingStore.generate_code` method in [`gateway/pairing.py`](https://github.com/NousResearch/hermes-agent/blob/main/gateway/pairing.py) uses Python's `secrets` module with a 32-character unambiguous alphabet (excluding visually similar characters like '0', 'O', '1', and 'I'). This produces 8-character codes with approximately 40 bits of entropy, making brute-force guessing computationally infeasible within the 1-hour TTL window.

### Can I use static allowlists without enabling the pairing system?

Yes. If you populate environment variables like `TELEGRAM_ALLOWED_USERS` or `GATEWAY_ALLOWED_USERS` with comma-separated user IDs, the gateway will authorize those users immediately without requiring pairing codes. Set `GATEWAY_ALLOW_ALL_USERS=true` to bypass all restrictions entirely, though this is not recommended for production deployments.

### What happens when a paired user is revoked?

When you run `hermes pairing revoke <platform> <user_id>` or call `PairingStore.revoke()`, the user ID is removed from the `{platform}-approved.json` file in `~/.hermes/pairing/`. Subsequent messages from that user will fail the `PairingStore.is_authorized` check, triggering the gateway to either send a new pairing code (in DMs) or silently drop the message (in group chats), effectively requiring the user to re-authenticate.

### How does WhatsApp authorization handle different JID formats?

The authorization logic in `gateway/run.py::_is_user_authorized` normalizes WhatsApp JIDs by stripping the `@s.whatsapp.net` suffix before comparing against allowlists or approved user lists. This ensures that a user ID like `123456789@s.whatsapp.net` matches an entry of `123456789` in `WHATSAPP_ALLOWED_USERS` or the pairing store, preventing authorization bypasses through JID formatting variations.