Introduction
The most important rule in Hermes configuration is also the simplest: secrets go in .env, everything else goes in config.yaml. The reason is not stylistic. Hermes specifically auto-redacts values from .env in logs and tool output. Anything that lives in config.yaml is treated as shareable, reproducible state and may surface in places you do not want a key to be.
Key Concepts
- Auto-redaction: Values loaded from
.envare masked in log output and debug dumps. - Environment variable substitution:
config.yamlcan reference.envvalues with${VAR_NAME}. - Shareability boundary:
config.yamlis meant to be shareable;.envis not. .gitignore.env, version-controlconfig.yaml: The standard pattern.
Real World Context
A developer tails the Hermes log file to debug a stuck tool call. The log includes everything the agent saw and emitted. Because their API keys live in .env, the keys appear as [REDACTED] in the log, even though they were referenced multiple times during the session. The same log file is safe to paste into a support thread. If the keys had been in config.yaml, they would have been plain text.
Deep Dive
Hermes treats .env and config.yaml as two slots that look superficially similar (key/value pairs) but have very different lifecycles.
.env lifecycle:
textVALUE → loaded into process memory → masked in any log or debug output (auto-redacted by the redactor on emit)
config.yaml lifecycle:
textVALUE → loaded into runtime config → visible in /config, /dump, backups, shared sessions
The redactor is the key piece. It scans output streams for any value that matches a .env entry and replaces it with [REDACTED]. That mechanism only protects values it can identify. A key written into config.yaml is just another setting; the redactor does not know it is supposed to be sensitive.
Linking the two via environment substitution
You can reference .env values inside config.yaml using ${VAR_NAME} syntax:
yamlauxiliary: vision: api_key: ${GOOGLE_VISION_API_KEY} # comes from .env, not stored in config.yaml base_url: ${CUSTOM_VISION_URL}
When Hermes loads the config, it substitutes the value from the environment (loaded from .env). The config.yaml file itself stores only the placeholder, so it remains shareable. If a referenced variable is undefined, the placeholder remains verbatim, which is intentional: you would rather see ${MISSING_KEY} in your config than have an empty string silently substituted.
Only the ${VAR} syntax is supported. Bare $VAR is not expanded, by design, to avoid accidental substitutions in YAML strings.
What counts as a secret
The rule of thumb: if a value gives access to a paid account or a third-party service, it is a secret. That covers:
- API keys (Anthropic, OpenAI, OpenRouter, Google, etc.)
- Bot tokens (Telegram, Slack, Discord)
- Passwords (for self-hosted endpoints with auth)
- Webhook signing secrets
- Cloud provider credentials
Things that are not secrets:
- The model name (
anthropic/claude-sonnet-4-20250514) - The terminal backend (
docker,local) - The toolset list
- Display preferences
- File paths to local resources
Common Pitfalls
- Writing keys directly into
config.yaml: Even briefly, for testing. The key may end up in a backup or shared session by accident. - Committing
.envto git: A.gitignoreentry for.envshould be standard in any repo that contains a Hermes setup.
Best Practices
- Treat
${VAR_NAME}substitution as your bridge: When a non-secret config field needs a secret value, reference it from.envrather than inlining. - Add
.envto.gitignorebefore adding any keys: This is one of those ordering decisions that costs nothing now and prevents disasters later.
Summary
- Hermes auto-redacts
.envvalues in logs and debug output;config.yamlvalues are not redacted. - Reference
.envvalues fromconfig.yamlwith${VAR_NAME}substitution. - A value is a secret if it grants access to a paid or third-party service.
.envshould always be gitignored;config.yamlis meant to be shareable.
Code Examples
# config.yaml references secrets without storing them
auxiliary:
vision:
api_key: ${GOOGLE_VISION_API_KEY}
base_url: ${CUSTOM_VISION_URL}
providers:
openrouter:
api_key: ${OPENROUTER_API_KEY}
# The actual values live next door in .env (which is gitignored):
# GOOGLE_VISION_API_KEY=AIza...
# CUSTOM_VISION_URL=https://api.vision.local
# OPENROUTER_API_KEY=sk-or-...