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.
The approval system supports three modes, configured via approvals.mode in ~/.hermes/config.yaml:
approvals:
mode: manual# manual | smart | off
timeout: 60# seconds to wait for user response (default: 60)
Mode
Behavior
manual (default)
Always prompt the user for approval on dangerous commands
smart
Use an auxiliary LLM to assess risk. Low-risk commands (e.g., python -c "print('hello')") are auto-approved. Genuinely dangerous commands are auto-denied. Uncertain cases escalate to a manual prompt.
off
Disable all approval checks — equivalent to running with --yolo. All commands execute without prompts.
Warning
Setting approvals.mode: off disables all safety prompts. Use only in trusted environments (CI/CD, containers, etc.).
YOLO mode bypasses all dangerous command approval prompts for the current session. It can be activated three ways:
CLI flag: Start a session with hermes --yolo or hermes chat --yolo
Slash command: Type /yolo during a session to toggle it on/off
Environment variable: Set HERMES_YOLO_MODE=1
The /yolo command is a toggle — each use flips the mode on or off:
> /yolo
⚡ YOLO mode ON — all commands auto-approved. Use with caution.
> /yolo
⚠ YOLO mode OFF — dangerous commands will require approval.
YOLO mode is available in both CLI and gateway sessions. Internally, it sets the HERMES_YOLO_MODE environment variable which is checked before every command execution.
Danger
YOLO mode disables all dangerous command safety checks for the session — except the hardline blocklist (see below). Use only when you fully trust the commands being generated (e.g., well-tested automation scripts in disposable environments).
Some commands are so catastrophic — irreversible filesystem wipes, fork bombs, direct block-device writes — that Hermes refuses to run them regardless of:
--yolo / /yolo toggled on
approvals.mode: off
Cron jobs running in headless approve mode
User explicitly clicking “allow always”
The blocklist is the floor below --yolo. It trips before the approval layer even sees the command, and there’s no override flag. Patterns currently covered (not exhaustive; kept in sync with tools/approval.py::UNRECOVERABLE_BLOCKLIST):
Pattern
Why it’s hardline
rm -rf / and obvious variants
Wipes the filesystem root
rm -rf --no-preserve-root /
The explicit “yes I mean root” variant
:(){ :|:& };: (bash fork bomb)
Pegs the host until reboot
mkfs.* on a mounted root device
Formats the live system
dd if=/dev/zero of=/dev/sd*
Zeroes a physical disk
Piping untrusted URLs to sh at the rootfs top level
Remote-code-execution attack vector too broad to approve
If you hit the blocklist, the tool call returns an explanatory error to the agent and nothing runs. If a legitimate workflow needs one of these commands (you’re the operator of a wipe-and-reinstall pipeline, for example), run it outside the agent.
When a dangerous command prompt appears, the user has a configurable amount of time to respond. If no response is given within the timeout, the command is denied by default (fail-closed).
The following patterns trigger approval prompts (defined in tools/approval.py):
Pattern
Description
rm -r / rm --recursive
Recursive delete
rm ... /
Delete in root path
chmod 777/666 / o+w / a+w
World/other-writable permissions
chmod --recursive with unsafe perms
Recursive world/other-writable (long flag)
chown -R root / chown --recursive root
Recursive chown to root
mkfs
Format filesystem
dd if=
Disk copy
> /dev/sd
Write to block device
DROP TABLE/DATABASE
SQL DROP
DELETE FROM (without WHERE)
SQL DELETE without WHERE
TRUNCATE TABLE
SQL TRUNCATE
> /etc/
Overwrite system config
systemctl stop/restart/disable/mask
Stop/restart/disable system services
kill -9 -1
Kill all processes
pkill -9
Force kill processes
Fork bomb patterns
Fork bombs
bash -c / sh -c / zsh -c / ksh -c
Shell command execution via -c flag (including combined flags like -lc)
python -e / perl -e / ruby -e / node -c
Script execution via -e/-c flag
curl ... | sh / wget ... | sh
Pipe remote content to shell
bash <(curl ...) / sh <(wget ...)
Execute remote script via process substitution
tee to /etc/, ~/.ssh/, ~/.hermes/.env
Overwrite sensitive file via tee
> / >> to /etc/, ~/.ssh/, ~/.hermes/.env
Overwrite sensitive file via redirection
xargs rm
xargs with rm
find -exec rm / find -delete
Find with destructive actions
cp/mv/install to /etc/
Copy/move file into system config
sed -i / sed --in-place on /etc/
In-place edit of system config
pkill/killall hermes/gateway
Self-termination prevention
gateway run with &/disown/nohup/setsid
Prevents starting gateway outside service manager
InfoContainer bypass: When running in docker, singularity, modal, daytona, or vercel_sandbox backends, dangerous command checks are skipped because the container itself is the security boundary. Destructive commands inside a container can’t harm the host.
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:
An unknown user sends a DM to the bot
The bot replies with an 8-character pairing code
The bot owner runs hermes pairing approve <platform> <code> on the CLI
The user is permanently approved for that platform
Control how unauthorized direct messages are handled in ~/.hermes/config.yaml:
unauthorized_dm_behavior: pair
whatsapp:
unauthorized_dm_behavior: ignore
pair is the default. Unauthorized DMs get a pairing code reply.
ignore silently drops unauthorized DMs.
Platform sections override the global default, so you can keep pairing on Telegram while keeping WhatsApp silent.
Security features (based on OWASP + NIST SP 800-63-4 guidance):
Feature
Details
Code format
8-char from 32-char unambiguous alphabet (no 0/O/1/I)
Randomness
Cryptographic (secrets.choice())
Code TTL
1 hour expiry
Rate limiting
1 request per user per 10 minutes
Pending limit
Max 3 pending codes per platform
Lockout
5 failed approval attempts → 1-hour lockout
File security
chmod 0600 on all pairing data files
Logging
Codes are never logged to stdout
Pairing CLI commands:
Terminal window
# List pending and approved users
hermespairinglist
# Approve a pairing code
hermespairingapprovetelegramABC12DEF
# Revoke a user's access
hermespairingrevoketelegram123456789
# Clear all pending codes
hermespairingclear-pending
Storage: Pairing data is stored in ~/.hermes/pairing/ with per-platform JSON files:
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, modal, daytona, or vercel_sandbox backend to isolate agent commands from your host system. This eliminates the need for dangerous command approval entirely.
Warning
If you add names to terminal.docker_forward_env, those variables are intentionally injected into the container for terminal commands. This is useful for task-specific credentials like GITHUB_TOKEN, but it also means code running in the container can read and exfiltrate them.
Both execute_code and terminal strip sensitive environment variables from child processes to prevent credential exfiltration by LLM-generated code. However, skills that declare required_environment_variables legitimately need access to those vars.
Two mechanisms allow specific variables through the sandbox filters:
1. Skill-scoped passthrough (automatic)
When a skill is loaded (via skill_view or the /skill command) and declares required_environment_variables, any of those vars that are actually set in the environment are automatically registered as passthrough. Missing vars (still in setup-needed state) are not registered.
# In a skill's SKILL.md frontmatter
required_environment_variables:
- name: TENOR_API_KEY
prompt: Tenor API key
help: Get a key from https://developers.google.com/tenor
After loading this skill, TENOR_API_KEY passes through to execute_code, terminal (local), and remote backends (Docker, Modal) — no manual configuration needed.
Info: Docker & Modal
Prior to v0.5.1, Docker’s forward_env was a separate system from the skill passthrough. They are now merged — skill-declared env vars are automatically forwarded into Docker containers and Modal sandboxes without needing to add them to docker_forward_env manually.
2. Config-based passthrough (manual)
For env vars not declared by any skill, add them to terminal.env_passthrough in config.yaml:
Some skills need files (not just env vars) in the sandbox — for example, Google Workspace stores OAuth tokens as google_token.json under the active profile’s HERMES_HOME. Skills declare these in frontmatter:
required_credential_files:
- path: google_token.json
description: Google OAuth2 token (created by setup script)
- path: google_client_secret.json
description: Google OAuth2 client credentials
When loaded, Hermes checks if these files exist in the active profile’s HERMES_HOME and registers them for mounting:
You can restrict which websites the agent can access through its web and browser tools. This is useful for preventing the agent from accessing internal services, admin panels, or other sensitive URLs.
# In ~/.hermes/config.yaml
security:
website_blocklist:
enabled: true
domains:
- "*.internal.company.com"
- "admin.example.com"
shared_files:
- "/etc/hermes/blocked-sites.txt"
When a blocked URL is requested, the tool returns an error explaining the domain is blocked by policy. The blocklist is enforced across web_search, web_extract, browser_navigate, and all URL-capable tools.
All URL-capable tools (web search, web extract, vision, browser) validate URLs before fetching them to prevent Server-Side Request Forgery (SSRF) attacks. Blocked addresses include:
SSRF protection is always active for internet-facing use and DNS failures are treated as blocked (fail-closed). Redirect chains are re-validated at each hop to prevent redirect-based bypasses.
Some setups legitimately need private/internal URL access — home networks that resolve home.arpa to RFC 1918 space, LAN-only Ollama/llama.cpp endpoints, internal wikis, cloud metadata debugging, and the like. For those cases there’s a global opt-out:
security:
allow_private_urls: true# default: false
When on, web tools, the browser, vision URL fetches, and gateway media downloads no longer reject RFC 1918 / loopback / link-local / CGNAT / cloud-metadata destinations. This is a deliberate trust boundary — only enable it on machines where the agent running arbitrary prompt-injected URLs against the local network is an acceptable risk. Public-facing gateways should leave it off.
The host-substring guard (which blocks lookalike Unicode domain tricks even when the underlying IP is public) stays on regardless of this setting.
tirith_path: "tirith"# Path to tirith binary (default: PATH lookup)
tirith_timeout: 5# Subprocess timeout in seconds
tirith_fail_open: true# Allow execution when tirith is unavailable (default: true)
When tirith_fail_open is true (default), commands proceed if tirith is not installed or times out. Set to false in high-security environments to block commands when tirith is unavailable.
Tirith’s verdict integrates with the approval flow: safe commands pass through, while both suspicious and blocked commands trigger user approval with the full tirith findings (severity, title, description, safer alternatives). Users can approve or deny — the default choice is deny to keep unattended scenarios secure.