The Secrets-Separation Principle

+15 Mana ✨

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 .env are masked in log output and debug dumps.
  • Environment variable substitution: config.yaml can reference .env values with ${VAR_NAME}.
  • Shareability boundary: config.yaml is meant to be shareable; .env is not.
  • .gitignore .env, version-control config.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:

text
VALUE  →  loaded into process memory  →  masked in any log or debug output
         (auto-redacted by the redactor on emit)

config.yaml lifecycle:

text
VALUE  →  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:

yaml
auxiliary:
  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

  1. Writing keys directly into config.yaml: Even briefly, for testing. The key may end up in a backup or shared session by accident.
  2. Committing .env to git: A .gitignore entry for .env should be standard in any repo that contains a Hermes setup.

Best Practices

  1. Treat ${VAR_NAME} substitution as your bridge: When a non-secret config field needs a secret value, reference it from .env rather than inlining.
  2. Add .env to .gitignore before adding any keys: This is one of those ordering decisions that costs nothing now and prevents disasters later.

Summary

  • Hermes auto-redacts .env values in logs and debug output; config.yaml values are not redacted.
  • Reference .env values from config.yaml with ${VAR_NAME} substitution.
  • A value is a secret if it grants access to a paid or third-party service.
  • .env should always be gitignored; config.yaml is meant to be shareable.

Code Examples

yaml
# 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-...
✓ Completed