Skip to main content

Security

Hermes Agent is designed with a defense-in-depth security model. This page covers every security boundary — from command approval to container isolation to user authorization on messaging platforms.

Overview

The security model has five layers:

  1. User authorization — who can talk to the agent (allowlists, DM pairing)
  2. Dangerous command approval — human-in-the-loop for destructive operations
  3. Container isolation — Docker/Singularity/Modal sandboxing with hardened settings
  4. MCP credential filtering — environment variable isolation for MCP subprocesses
  5. Context file scanning — prompt injection detection in project files

Dangerous Command Approval

Before executing any command, Hermes checks it against a curated list of dangerous patterns. If a match is found, the user must explicitly approve it.

What Triggers Approval

The following patterns trigger approval prompts (defined in tools/approval.py):

PatternDescription
rm -r / rm --recursiveRecursive delete
rm ... /Delete in root path
chmod 777World-writable permissions
mkfsFormat filesystem
dd if=Disk copy
DROP TABLE/DATABASESQL DROP
DELETE FROM (without WHERE)SQL DELETE without WHERE
TRUNCATE TABLESQL TRUNCATE
> /etc/Overwrite system config
systemctl stop/disable/maskStop/disable system services
kill -9 -1Kill all processes
curl ... | shPipe remote content to shell
bash -c, python -eShell/script execution via flags
find -exec rm, find -deleteFind with destructive actions
Fork bomb patternsFork bombs
info

Container bypass: When running in docker, singularity, or modal backends, dangerous command checks are skipped because the container itself is the security boundary. Destructive commands inside a container can't harm the host.

Approval Flow (CLI)

In the interactive CLI, dangerous commands show an inline approval prompt:

  ⚠️  DANGEROUS COMMAND: recursive delete
rm -rf /tmp/old-project

[o]nce | [s]ession | [a]lways | [d]eny

Choice [o/s/a/D]:

The four options:

  • once — allow this single execution
  • session — allow this pattern for the rest of the session
  • always — add to permanent allowlist (saved to config.yaml)
  • deny (default) — block the command

Approval Flow (Gateway/Messaging)

On messaging platforms, the agent sends the dangerous command details to the chat and waits for the user to reply:

  • Reply yes, y, approve, ok, or go to approve
  • Reply no, n, deny, or cancel to deny

The HERMES_EXEC_ASK=1 environment variable is automatically set when running the gateway.

Permanent Allowlist

Commands approved with "always" are saved to ~/.hermes/config.yaml:

# Permanently allowed dangerous command patterns
command_allowlist:
- rm
- systemctl

These patterns are loaded at startup and silently approved in all future sessions.

tip

Use hermes config edit to review or remove patterns from your permanent allowlist.

User Authorization (Gateway)

When running the messaging gateway, Hermes controls who can interact with the bot through a layered authorization system.

Authorization Check Order

The _is_user_authorized() method checks in this order:

  1. Per-platform allow-all flag (e.g., DISCORD_ALLOW_ALL_USERS=true)
  2. DM pairing approved list (users approved via pairing codes)
  3. Platform-specific allowlists (e.g., TELEGRAM_ALLOWED_USERS=12345,67890)
  4. Global allowlist (GATEWAY_ALLOWED_USERS=12345,67890)
  5. Global allow-all (GATEWAY_ALLOW_ALL_USERS=true)
  6. Default: deny

Platform Allowlists

Set allowed user IDs as comma-separated values in ~/.hermes/.env:

# Platform-specific allowlists
TELEGRAM_ALLOWED_USERS=123456789,987654321
DISCORD_ALLOWED_USERS=111222333444555666
WHATSAPP_ALLOWED_USERS=15551234567
SLACK_ALLOWED_USERS=U01ABC123

# Cross-platform allowlist (checked for all platforms)
GATEWAY_ALLOWED_USERS=123456789

# Per-platform allow-all (use with caution)
DISCORD_ALLOW_ALL_USERS=true

# Global allow-all (use with extreme caution)
GATEWAY_ALLOW_ALL_USERS=true
warning

If no allowlists are configured and GATEWAY_ALLOW_ALL_USERS is not set, all users are denied. The gateway logs a warning at startup:

No user allowlists configured. All unauthorized users will be denied.
Set GATEWAY_ALLOW_ALL_USERS=true in ~/.hermes/.env to allow open access,
or configure platform allowlists (e.g., TELEGRAM_ALLOWED_USERS=your_id).

DM Pairing System

For more flexible authorization, Hermes includes a code-based pairing system. Instead of requiring user IDs upfront, unknown users receive a one-time pairing code that the bot owner approves via the CLI.

How it works:

  1. An unknown user sends a DM to the bot
  2. The bot replies with an 8-character pairing code
  3. The bot owner runs hermes pairing approve <platform> <code> on the CLI
  4. The user is permanently approved for that platform

Security features (based on OWASP + NIST SP 800-63-4 guidance):

FeatureDetails
Code format8-char from 32-char unambiguous alphabet (no 0/O/1/I)
RandomnessCryptographic (secrets.choice())
Code TTL1 hour expiry
Rate limiting1 request per user per 10 minutes
Pending limitMax 3 pending codes per platform
Lockout5 failed approval attempts → 1-hour lockout
File securitychmod 0600 on all pairing data files
LoggingCodes are never logged to stdout

Pairing CLI commands:

# List pending and approved users
hermes pairing list

# Approve a pairing code
hermes pairing approve telegram ABC12DEF

# Revoke a user's access
hermes pairing revoke telegram 123456789

# Clear all pending codes
hermes pairing clear-pending

Storage: Pairing data is stored in ~/.hermes/pairing/ with per-platform JSON files:

  • {platform}-pending.json — pending pairing requests
  • {platform}-approved.json — approved users
  • _rate_limits.json — rate limit and lockout tracking

Container Isolation

When using the docker terminal backend, Hermes applies strict security hardening to every container.

Docker Security Flags

Every container runs with these flags (defined in tools/environments/docker.py):

_SECURITY_ARGS = [
"--cap-drop", "ALL", # Drop ALL Linux capabilities
"--security-opt", "no-new-privileges", # Block privilege escalation
"--pids-limit", "256", # Limit process count
"--tmpfs", "/tmp:rw,nosuid,size=512m", # Size-limited /tmp
"--tmpfs", "/var/tmp:rw,noexec,nosuid,size=256m", # No-exec /var/tmp
"--tmpfs", "/run:rw,noexec,nosuid,size=64m", # No-exec /run
]

Resource Limits

Container resources are configurable in ~/.hermes/config.yaml:

terminal:
backend: docker
docker_image: "nikolaik/python-nodejs:python3.11-nodejs20"
container_cpu: 1 # CPU cores
container_memory: 5120 # MB (default 5GB)
container_disk: 51200 # MB (default 50GB, requires overlay2 on XFS)
container_persistent: true # Persist filesystem across sessions

Filesystem Persistence

  • Persistent mode (container_persistent: true): Bind-mounts /workspace and /root from ~/.hermes/sandboxes/docker/<task_id>/
  • Ephemeral mode (container_persistent: false): Uses tmpfs for workspace — everything is lost on cleanup
tip

For production gateway deployments, use docker or modal backend to isolate agent commands from your host system. This eliminates the need for dangerous command approval entirely.

Terminal Backend Security Comparison

BackendIsolationDangerous Cmd CheckBest For
localNone — runs on host✅ YesDevelopment, trusted users
sshRemote machine✅ YesRunning on a separate server
dockerContainer❌ Skipped (container is boundary)Production gateway
singularityContainer❌ SkippedHPC environments
modalCloud sandbox❌ SkippedScalable cloud isolation

MCP Credential Handling

MCP (Model Context Protocol) server subprocesses receive a filtered environment to prevent accidental credential leakage.

Safe Environment Variables

Only these variables are passed through from the host to MCP stdio subprocesses:

PATH, HOME, USER, LANG, LC_ALL, TERM, SHELL, TMPDIR

Plus any XDG_* variables. All other environment variables (API keys, tokens, secrets) are stripped.

Variables explicitly defined in the MCP server's env config are passed through:

mcp_servers:
github:
command: "npx"
args: ["-y", "@modelcontextprotocol/server-github"]
env:
GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_..." # Only this is passed

Credential Redaction

Error messages from MCP tools are sanitized before being returned to the LLM. The following patterns are replaced with [REDACTED]:

  • GitHub PATs (ghp_...)
  • OpenAI-style keys (sk-...)
  • Bearer tokens
  • token=, key=, API_KEY=, password=, secret= parameters

Context File Injection Protection

Context files (AGENTS.md, .cursorrules, SOUL.md) are scanned for prompt injection before being included in the system prompt. The scanner checks for:

  • Instructions to ignore/disregard prior instructions
  • Hidden HTML comments with suspicious keywords
  • Attempts to read secrets (.env, credentials, .netrc)
  • Credential exfiltration via curl
  • Invisible Unicode characters (zero-width spaces, bidirectional overrides)

Blocked files show a warning:

[BLOCKED: AGENTS.md contained potential prompt injection (prompt_injection). Content not loaded.]

Best Practices for Production Deployment

Gateway Deployment Checklist

  1. Set explicit allowlists — never use GATEWAY_ALLOW_ALL_USERS=true in production
  2. Use container backend — set terminal.backend: docker in config.yaml
  3. Restrict resource limits — set appropriate CPU, memory, and disk limits
  4. Store secrets securely — keep API keys in ~/.hermes/.env with proper file permissions
  5. Enable DM pairing — use pairing codes instead of hardcoding user IDs when possible
  6. Review command allowlist — periodically audit command_allowlist in config.yaml
  7. Set MESSAGING_CWD — don't let the agent operate from sensitive directories
  8. Run as non-root — never run the gateway as root
  9. Monitor logs — check ~/.hermes/logs/ for unauthorized access attempts
  10. Keep updated — run hermes update regularly for security patches

Securing API Keys

# Set proper permissions on the .env file
chmod 600 ~/.hermes/.env

# Keep separate keys for different services
# Never commit .env files to version control

Network Isolation

For maximum security, run the gateway on a separate machine or VM:

terminal:
backend: ssh
ssh_host: "agent-worker.local"
ssh_user: "hermes"
ssh_key: "~/.ssh/hermes_agent_key"

This keeps the gateway's messaging connections separate from the agent's command execution.