Welcome to Your Agent's Home Base: `~/.hermes/`
Tutorial 7: Configuration Files — easy-to-understand guide based on official docs
Welcome to Your Agent’s Home Base: ~/.hermes/
If you’ve been following along with our tutorial series, you already know Hermes Agent is powerful. But with great power comes great configuration, right? Not anymore. Let’s look at how Hermes keeps everything tidy in one place — and how you can get started in minutes, not hours.
The Magic of ~/.hermes/
All your Hermes settings live in a single directory: ~/.hermes/. Think of it as your agent’s command center. Here’s what you’ll find inside:
~/.hermes/
├── config.yaml # Main settings (model, terminal, TTS, etc.)
├── .env # API keys and secrets
├── auth.json # OAuth credentials (Nous Portal, etc.)
├── SOUL.md # Your agent's personality
├── memories/ # Persistent memory files
├── skills/ # Agent-created skills
├── cron/ # Scheduled jobs
├── sessions/ # Gateway sessions
└── logs/ # Logs (secrets auto-redacted)
The Fastest Path: hermes setup --portal
Before we dive into manual configuration, here’s a pro tip: run hermes setup --portal. One OAuth login gets you a model provider and all four Tool Gateway tools — no YAML editing required. Portal subscribers even get 10% off token-billed providers. It’s the easiest way to go from zero to fully operational.
Managing Your Configuration
The hermes config command is your best friend. Here are the essential commands:
hermes config # View current configuration
hermes config edit # Open config.yaml in your editor
hermes config get KEY # Print a resolved value
hermes config set KEY VAL # Set a specific value
hermes config unset KEY # Remove a user-set value
hermes config check # Check for missing options and stale saved selections
hermes config migrate # Add missing options interactively
Real-world examples:
hermes config get model
hermes config set model anthropic/claude-opus-4
hermes config set terminal.backend docker
hermes config set OPENROUTER_API_KEY sk-or-...
Notice something clever? The set command automatically routes values to the right file. Any UPPER_SNAKE name — OPENROUTER_API_KEY, DISCORD_HOME_CHANNEL, HERMES_TIMEZONE, and friends — is treated as an environment variable and saved to .env, never to config.yaml. Dotted settings like model or terminal.backend go to config.yaml. A few reserved names (HERMES_YOLO_MODE, PATH, …) are refused for safety, and if you write a known key under the wrong prefix you’ll get a did-you-mean hint — add --force if you really mean it. hermes config get on a path Hermes may not read prints the value plus a warning, so a leftover key can’t quietly pass for a live setting.
Understanding Configuration Precedence
When multiple sources define a setting, Hermes follows this order (highest priority first):
- CLI arguments — e.g.,
hermes chat --model anthropic/claude-sonnet-4 ~/.hermes/config.yaml— your primary config file- ~/.hermes/.env` — for secrets and fallback env vars
- Built-in defaults — safe defaults when nothing else is set
Rule of thumb: Secrets (API keys, tokens, passwords) go in .env. Everything else (model, terminal backend, compression settings) goes in config.yaml. When both are set, config.yaml wins for non-secret settings.
Environment Variable Substitution
Here’s a powerful feature: you can reference environment variables directly in config.yaml using ${VAR_NAME} syntax:
auxiliary:
vision:
api_key: ${GOOGLE_API_KEY}
base_url: ${CUSTOM_VISION_URL}
You can even combine multiple references: url: "${HOST}:${PORT}". If a variable isn’t set, the placeholder stays as-is (with a warning logged). Cursor-style syntax like ${env:VAR_NAME} also works, so snippets copied from other tools work unchanged. Note that other SecretRef sources (${file:...}, ${vault:...}, ${bitwarden:...}) are not resolved inline — external secret backends inject their values into the environment at startup via the secrets: block, so reference them as ${env:NAME} instead. Under a multiplexed multi-profile gateway, references in a profile’s config.yaml resolve against that profile’s .env, not the shared process environment. A local hermes chat / TUI session served by the gateway resolves these references, provider_routing and prefill_messages_file when the gateway creates the session, from its profile’s current config.yaml and .env. Edits to them apply to the next new session without a gateway restart, and open sessions keep their creation-time values. A gateway restart doesn’t re-read them either: a session restored after a restart keeps the routing and prefill path stored with it, and reads the prefill file’s contents again, from that stored path, the first time it needs them. On the profile the gateway was launched with, a variable you remove from .env still resolves to the value loaded at gateway start, because a miss falls back to the process environment; restart the gateway to drop it.
Fine-Tuning Performance
Two new sections worth knowing about:
Runtime Limits — For long-running servers, you can set file descriptor limits:
runtime:
nofile_soft_limit: 4096
The default is 4096. Hermes clamps the target to your OS hard limit and never lowers a process that already has a higher soft limit. Set it to 0, false, or null to disable the adjustment.
Database Settings — Control how Hermes handles its SQLite database:
database:
journal_mode: wal # or "delete" for network mounts
# synchronous: FULL # durability level
# wal_autocheckpoint: 1000
# journal_size_limit: 67108864
Use delete on filesystems where WAL is unsafe, like network mounts. On virtiofs/9p bind mounts (Docker Desktop, Podman on macOS, OrbStack), Hermes detects the mount and creates fresh databases in delete mode automatically. Note that an existing WAL database is never live-downgraded — Hermes keeps WAL and logs an error telling you the configured delete did not apply. To convert an existing database, stop every process using it and run hermes sessions set-journal-mode delete (it refuses while anything still holds the file and verifies the converted header). hermes doctor warns you until then, and names the processes currently holding the database so you know what to stop; when the holder scan is partial or unavailable it says cannot prove the database is quiet instead of giving an all-clear.
Sandbox Images and Containers
If you use a sandbox backend, each one has its own image setting. The defaults all point at the same desktop-ready image:
modal_image: "nousresearch/hermes-sandbox:desktop" # Container image for Modal backend
daytona_image: "nousresearch/hermes-sandbox:desktop" # Container image for Daytona backend
client_env_passthrough: [] # Extra non-secret toolchain names (e.g. GEM_HOME) a local CLI/TUI session's tools take from its launching shell
vercel_image: vercel/sandbox/universal:latest # Vercel managed image or a VCR repository[:tag]
For Docker, the default image is nikolaik/python-nodejs (Python 3.13 / Node 26) plus a display stack, so Bot Screen, computer_use and the browser run inside the sandbox. Any other image works for shell work, but the screen then needs bot_desktop.placement: gateway. Writing this key is a decision: a persisted container on another image is recreated on the next terminal call. Left unset, an existing container is kept and the CLI / Screen pane ask first.
Singularity is no longer built into core: it is the official singularity plugin (formerly built in; hermes update installs it for profiles that already use it). Install it with hermes plugins install singularity; it also needs an apptainer or singularity binary in $PATH. Scratch-directory resolution also changed: TERMINAL_SCRATCH_DIR → /scratch/$USER/hermes-agent when /scratch is writable (the HPC convention) → TERMINAL_SANDBOX_DIR/singularity (default ~/.hermes/sandboxes/singularity).
docker_image: "nousresearch/hermes-sandbox:desktop"
For Vercel, terminal.vercel_image picks the image for fresh sandboxes: a managed image such as vercel/sandbox/universal:latest (the default: Ubuntu, Node.js 24, Python 3.14), vercel/sandbox/node:26 or vercel/sandbox/python:3.14, or a repository from your project’s Vercel Container Registry (a bare name resolves to latest; a tag or digest pins it). The older terminal.vercel_runtime presets (node24, node22, python3.13) are deprecated by Vercel; a pinned runtime still works and overrides the image, but the two cannot be combined.
Hermes-managed containers carry labels so later processes (and the orphan reaper) can identify them. One of these is hermes-environment=<digest> — for containers without an explicit shared key, it scopes reuse to the requested image, mount arguments, and active HERMES_HOME. Without an explicit shared key, the reuse probe also requires a matching hermes-environment label, so changing the image, mount arguments, or HERMES_HOME starts a fresh container instead of silently using another configuration. Containers created before this label existed do not match, so the first session after upgrading starts a fresh container; existing running containers are not removed.
Modal persistence: When enabled, the sandbox filesystem is snapshotted on cleanup and restored on the next session. Snapshots are tracked in ~/.hermes/modal_snapshots.json and are retained until you delete them (Hermes opts out of the Modal SDK’s 30-day snapshot expiry). This preserves filesystem state, not live processes, PID space, or background jobs.
Compression and Fast Mode
Absolute token cap — threshold_tokens sets an optional absolute cap for the compression trigger:
threshold_tokens: null # Absolute token cap (optional) — takes lower of ratio vs absolute
When set, compression fires at the lower of the ratio-based threshold and this absolute count, so compaction never fires later than that token count regardless of which model is active. Use it when you want a fixed cost ceiling per call, for example threshold_tokens: 256000 to compact a 1M-window model at 256K instead of 500K. The cap is clamped to the model’s context length, so a value above the window is a no-op. Default null (disabled — ratio-based threshold only). The cap survives model switches and fallback activations.
Stream progress timeout — When the summary streams (compression always streams over chat-completions and Responses routes, e.g. openai-codex or a custom OpenAI-compatible endpoint), two timeouts apply, and they are independent:
auxiliary.compression.timeout— the overall request budget (default 120s).auxiliary.compression.no_progress_timeout— how long the stream may go without a substantive event (a text/reasoning delta or a completed output item) before the attempt aborts (... stream stalled: no new output for Ns) and the normal retry / fallback chain takes over. Default 60s when unset. On a chat-completions stream the window starts at the first token. Before it, a remote endpoint gets the main loop’s stale patience (the provider’sproviders.<id>.stale_timeout_secondswhen set, else 180s raised for large prompts and to 600s for reasoning models such as o3 or DeepSeek R1; capped attimeout), so a model thinking silently is not cut. On a local endpoint (localhost, LAN, Tailscale) a silent prefill is bounded only bytimeout. The same rule applies to every streamed auxiliary task, including MoA reference calls. Keepalive and lifecycle frames (response.in_progress, pings) do not count as progress; every substantive event re-arms the window, so a slow but progressing summary is never cut off by it.
Raising timeout alone does not widen the progress window — a request configured for 600s still aborts after a 60s gap. Set no_progress_timeout to change that gap; the effective window is capped at timeout, and the host’s hard deadline / cancellation still win. The host’s own inactivity budget is the outer cap here: in-agent compaction gives up on a silent summariser after compression.context_timeout_seconds (default 120s, floored at the effective auxiliary.compression.timeout, itself at least 300s) and gateway hygiene after compression.hygiene_timeout_seconds (default 30s), so a no_progress_timeout larger than the applicable host budget is silently cut short by it. The key is per task (auxiliary.<task>.no_progress_timeout), so widening it for compression does not change other auxiliary tasks. A value that is not a positive number is ignored with a warning in the log and the 60s default applies.
auxiliary:
compression:
provider: openai-codex
timeout: 600
no_progress_timeout: 180 # tolerate a 3-minute silent gap on a long reasoning summary
A rate limit that names its reset (Retry-After or a retry_after body field) is waited out, capped at 600 s per retry. The live status line shows the wait and, when the provider names it, when the limit resets. One exception: on the free Nous tier (no sign-in), a session someone is watching (the Desktop app, the TUI, the hermes terminal chat, an ACP editor) does not sit through a cooldown longer than 60 s, because the free tier’s pause covers the whole service and a retry before the reset only meets the same refusal. The turn ends at once and says when the limit lifts; a configured fallback runs first. Unattended runs (hermes chat -q, cron, messaging platforms, delegated subagents) keep waiting.
Fast mode asks the provider for faster output at a premium price: OpenAI Priority Processing (service_tier: priority) and Ultrafast (service_tier: ultrafast) on supported OpenAI models, xAI Priority Processing on Grok 4.6, and Anthropic Fast Mode (speed: fast, Opus 4.8 / Opus 5 / Opus 5.5 only). The openai and openai-api providers use the first-party OpenAI endpoint. It is off by default.
service_tier: "" # "" / normal | fast | priority | ultrafast | auto | cold
/fast normal|fast|ultrafast|auto|cold switches the mode for the session. Add --global to persist to config.yaml. /fast alone shows the current mode.
Auxiliary Task Routing
For auxiliary tasks, base_url takes precedence over provider, so it is the most explicit way to route a task to a specific endpoint. For direct endpoint overrides, Hermes uses the configured api_key or falls back to OPENAI_API_KEY; it does not reuse OPENROUTER_API_KEY for that custom endpoint. With neither set, the main model’s key is reused only when base_url has the main endpoint’s exact origin (scheme, host and port). The main endpoint is the one the session is running on (after a /model switch, that one), paired with its own key, never with a key from a different endpoint.
Language and Tool Progress
The display.language setting picks the interface language. Bundled values: en (default), zh (Simplified Chinese), zh-hant (Traditional Chinese), ja, de, es, fr, tr, uk, af, ko, it, ga, pt, ru, hu, ar. The list is pluggable: a language pack plugin (provides_locales) or a partial <HERMES_HOME>/locales/<lang>.yaml overlay adds a language or overrides wording, and hermes config set display.language <id> accepts any id a bundled catalog, your overlay, or an installed pack provides. Unknown ids are refused with the list of available languages; at runtime an unresolvable value falls back to English.
For tool progress, platforms without an override fall back to the global tool_progress value. Valid platform keys: telegram, discord, slack, signal, whatsapp, matrix, mattermost, email, sms, dingtalk, feishu, wecom, weixin, bluebubbles, qqbot, plus the name of any plugin platform (e.g. homeassistant from the Home Assistant plugin). The legacy display.tool_progress_overrides key still loads for backward compatibility but is deprecated and migrated into display.platforms on first run.
Voice Chat and Speech
The Voice chat entry picks the model that answers spoken voice-mode turns (tools included); typed turns stay on the main model. See Voice chat model.
streaming: false # live partial text while you speak (openai, xai, elevenlabs); see Voice Mode > Live transcription
model: "base" # tiny, base, small, medium, large-v3, turbo
model: "grok-voice-transcribe-2.0" # or grok-voice-transcribe-1.0; sent on every request
language: "" # per-provider override of stt.language
Recordings over the provider’s per-request upload limit (25 MB for OpenAI/Groq/DeepInfra, 500 MB for Mistral and xAI, about 5 GB for ElevenLabs, plus per-model length limits for OpenAI’s gpt-4o-transcribe family and whisper-1) are re-encoded to compact AAC and, if still too large, split at pauses and transcribed piece by piece. See Long recordings and upload limits.
STT_XAI_MODEL=grok-voice-transcribe-2.0
Web Search Backends
Backend selection: The runtime always uses the stored web.backend selection (set via hermes tools; nous routes through the managed Tool Gateway). Only if no web backend has ever been selected is one auto-detected from available API keys: if only SEARXNG_URL is set, SearXNG is used; if only EXA_API_KEY is set, Exa; if only TAVILY_API_KEY is set, Tavily; if only PERPLEXITY_API_KEY is set, Perplexity; if only PARALLEL_API_KEY is set, Parallel; if only KEENABLE_API_KEY is set, Keenable. With no selection and no credentials at all, requests rotate round-robin across the keyless free-tier ring (Exa / Parallel / Firecrawl / Keenable) with automatic next-in-line failover on rate limits (and, for search, on vendor refusals) — see the Web Search guide for details. Once a selection exists, adding a key to .env does not change the route. Selecting Tavily, Firecrawl, or Keenable in hermes tools also works without a key.
Security and Onboarding
Secret redaction and the website blocklist:
Earlier releases also had tirith_* keys for a bundled command scanner; it was removed and upgrading drops those keys. Content-level command checks are described in Content-Level Command Checks.
First-touch onboarding hints and the first-message offer:
profile_build— controls the offer on the first message ever, in the TUI, the desktop app and messaging direct messages (never in a group chat)."ask"(default) offers to build a user profile; the offer is opt-in and consent-gated — the agent asks before any lookup, never reads connected accounts silently, and saves confirmed facts to user memory. In the Microsoft Store desktop app the offer is instead one closing line that points at/initiate-setup(/initiate_setupon Telegram,/hermes initiate-setupon Slack). The offer is skipped when the first message is/initiate-setupitself, and always in the desktop app’s setup profile."off"stops only the offer; the agent still gives a plain intro. The offer fires at most once per profile./initiate-setupsends a short block of facts about the computer; see What the setup chat knows about your computer.seen— internal state. Hermes latches each shown hint here so it never fires again; the first-message offer is also recorded here once shown. Don’t hand-edit it — wipe the wholeonboardingsection if you want to re-see all hints.ssh_isolated_idle_grace_s(default900) — a Desktop-ownedhermes serve --isolatedbackend reached over SSH is detached from the SSH session on purpose, so a laptop that sleeps mid-connection cannot tear it down; each dark-wake reconnect used to leave another backend holdingstate.db. The backend now retires itself once no client WebSocket has been connected for this long and no agent turn is running (a turn keeps it alive; an unreadable turn state keeps it alive too). Set high if you rely on a detached backend finishing long work after the laptop sleeps. Such backends also send a slow WebSocket ping (60 s, 10 min timeout) so a half-open tunnel is noticed.
Drain Wait on Gateway Restart
When the gateway stops or restarts (SIGTERM, hermes update, hermes gateway stop, or the final phase of hermes gateway restart / /restart), agent.restart_drain_timeout is how many seconds it lets in-flight chat turns keep running before it interrupts them:
agent:
restart_drain_timeout: 0 # default: interrupt in-flight chat turns at once
cron_drain_timeout: 30 # floor for cron jobs and api_server (/v1) runs
restart_after_turn_timeout: 1800 # /restart and `gateway restart`: wait for turns BEFORE stopping
With the default 0, a restart interrupts a running turn immediately. The turn settles as interrupted and is never run again; the TUI and Desktop show a “Gateway restarting” (or “Gateway shutting down”) notice on that turn, and you send the message again once Hermes reconnects. Queued messages behind it are kept and run in order on the new gateway.
Set a positive value if you would rather let turns finish — restart_drain_timeout: 60, for example. A longer drain makes every restart and update wait up to that long. HERMES_RESTART_DRAIN_TIMEOUT overrides the config value. The generated systemd unit sizes TimeoutStopSec from this value (the larger of restart_drain_timeout and cron_drain_timeout + 10 s, plus 30 s of headroom, at least 60 s), so after raising restart_drain_timeout re-run hermes gateway install — otherwise systemd SIGKILLs the gateway mid-drain. launchd caps its stop timeout at 60 s, so on macOS the drain is limited to what fits inside that.
Wrapping Up
Configuration doesn’t have to be scary. With hermes config set handling the routing automatically, and hermes setup --portal offering a zero-config path, you can focus on building, not configuring. Start with the defaults, tweak as you go, and remember: hermes config check is there to catch anything you might have missed after updates.
Happy configuring!
📖 Official Docs
This article is based on the official Hermes Agent documentation:Official docs › user-guide/configuration