Configuration System Structure and Migration in Hermes CLI: A Complete Guide
Hermes CLI uses a dual-file configuration system stored in ~/.hermes/, combining config.yaml for settings and .env for secrets, with automatic schema migration handled by migrate_config() in hermes_cli/config.py.
The NousResearch/hermes-agent project implements a sophisticated configuration management system in hermes_cli/config.py that handles everything from default value merging to versioned schema migration. This system ensures that user settings persist across updates while safely introducing new configuration options without breaking existing installations.
Configuration System Architecture
Dual-File Storage Model
The configuration system separates sensitive credentials from general settings using two distinct files. The get_hermes_home(), get_config_path(), and get_env_path() functions (lines 33-45) resolve these locations dynamically:
~/.hermes/config.yaml– Stores all non-sensitive configuration options including model selection, terminal settings, and feature toggles.~/.hermes/.env– Stores secret API keys and authentication tokens, read with UTF-8 safety on Windows and cached for performance.
Default Configuration Schema
The DEFAULT_CONFIG dictionary (lines 62-168) serves as the authoritative schema, containing every configurable option with sensible defaults. A critical field _config_version tracks schema evolution, enabling automatic migration detection.
The structure supports deeply nested configuration domains:
- Terminal – Backend selection, working directory, resource limits, and container image names.
- Compression – Enable flags, thresholds, and model/provider settings for conversation summarization.
- Auxiliary – Overrides for vision and web-extraction side-tasks.
- Display – UI tweaks including
compactmode,personalitysettings,resume_display, andtool_progress. - TTS/STT – Provider-specific sub-objects with voice and model defaults.
- Memory – Bounded curated memory limits.
Deep-Merge Loading Strategy
The load_config() function (lines 41-58) implements a non-destructive loading pattern. It begins with a deep copy of DEFAULT_CONFIG, then overlays user-provided YAML using the internal _deep_merge() helper. This ensures that adding a new sub-key does not erase user-provided siblings in nested dictionaries.
Persistence is handled by save_config() (lines 60-67) for YAML and save_env_value() for environment variables, both preserving file order and formatting.
Configuration Migration System
Version Tracking and Detection
Configuration migration relies on the _config_version field stored in both DEFAULT_CONFIG and the user's config.yaml. The check_config_version() function (lines 45-55) compares the stored version against the current default, returning the version delta to trigger migration logic.
Environment Variable Mapping
The ENV_VARS_BY_VERSION dictionary (lines 74-81) maps each schema version to the environment variables introduced at that step. This enables the system to identify which secrets must be present for a given configuration version.
The get_missing_config_options() function (lines 27-40) walks the default configuration tree recursively, recording any keys absent from the user config and returning structured objects containing the key path, default value, and description.
Stepwise Migration Logic
The migrate_config(interactive=True, quiet=False) function performs stepwise upgrades through version increments:
Migration to Version 4
Converts legacy tool-progress flags from .env variables (HERMES_TOOL_PROGRESS*) into structured YAML under display.tool_progress. If no legacy variables exist, defaults to "all".
Migration to Version 5
Adds the top-level timezone field. If the legacy HERMES_TIMEZONE environment variable exists, its value carries over; otherwise, an empty string defaults to server-local time.
Migration to Latest
After structural upgrades complete, the function checks for required environment variables via get_missing_env_vars(required_only=True). In interactive mode, it prompts the user for each missing value using getpass for secrets, then persists them via save_env_value().
Interactive Migration Interface
During interactive migration, the system prints helpful documentation URLs and captures sensitive input securely. Results track added environment variables in results["env_added"], providing transparency about configuration changes.
Practical Implementation Examples
Load and inspect the current configuration:
from hermes_cli import config
# Load the fully-merged configuration (user overrides + defaults)
cfg = config.load_config()
print("Model in use:", cfg["model"])
print("Terminal backend:", cfg["terminal"]["backend"])
Retrieve secret keys safely:
# Get a secret key (looks in OS env first, then ~/.hermes/.env)
openrouter_key = config.get_env_value("OPENROUTER_API_KEY")
print("OpenRouter key present?", bool(openrouter_key))
Run migration programmatically:
# Run migration manually (e.g., after a fresh checkout)
result = config.migrate_config(interactive=False)
print("Migration summary:", result)
CLI equivalents for daily use:
# Show current config with secrets redacted
hermes config
# Edit the YAML file directly
hermes config edit
# Run the migration wizard (prompts for missing env vars)
hermes config wizard
Summary
- Dual-file architecture separates settings (
~/.hermes/config.yaml) from secrets (~/.hermes/.env), with path resolution handled byget_hermes_home()and related functions. - Deep-merge loading via
load_config()preserves user customizations while ensuring new default keys populate automatically without overwriting existing nested values. - Version tracking through the
_config_versionfield enables automatic detection of outdated schemas viacheck_config_version(). - Stepwise migration in
migrate_config()handles structural changes (tool-progress relocation, timezone addition) and prompts for required environment variables interactively. - Secure secret management uses
get_env_value()andsave_env_value()with UTF-8 safety andgetpassfor sensitive input during migration.
Frequently Asked Questions
How does Hermes CLI handle configuration file locations across different operating systems?
The configuration system uses get_hermes_home() to resolve the base directory (~/.hermes), with get_config_path() and get_env_path() returning platform-appropriate paths for config.yaml and .env respectively. The code handles UTF-8 encoding explicitly on Windows to prevent character corruption, ensuring consistent behavior across Linux, macOS, and Windows environments.
What happens if I upgrade Hermes CLI and my configuration is outdated?
When load_config() detects a version mismatch via check_config_version(), the system automatically triggers migrate_config() to upgrade your schema stepwise. For example, configurations older than version 4 migrate tool-progress settings from environment variables into YAML structure, while pre-version 5 configs gain the timezone field. The process preserves existing user values while injecting new defaults, requiring no manual intervention unless missing required secrets need interactive input.
How are sensitive API keys separated from regular configuration settings?
The system maintains a strict separation between config.yaml (general settings) and .env (secrets). The get_env_value() function checks the operating system environment first, then falls back to the .env file with UTF-8 safety. During migration or initial setup, save_env_value() writes secrets securely, and redact_key() ensures these values display as *** when running hermes config. This architecture prevents accidental commits of API keys while keeping settings portable.
Can I programmatically trigger configuration migration without using the CLI?
Yes, the migrate_config() function in hermes_cli/config.py is fully exposed for programmatic use. Import the config module and call migrate_config(interactive=False) for silent upgrades or interactive=True to prompt for missing environment variables. The function returns a results dictionary detailing structural changes and added environment variables, allowing scripts and automated deployments to manage Hermes configuration lifecycle without manual CLI interaction.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →