# Configuration System Structure and Migration in Hermes CLI: A Complete Guide

> Explore the Hermes CLI configuration system structure in hermes_cli/config.py. Learn how settings and secrets are managed and discover the automatic config migration process.

- Repository: [Nous Research/hermes-agent](https://github.com/NousResearch/hermes-agent)
- Tags: how-to-guide
- Published: 2026-03-09

---

**Hermes CLI uses a dual-file configuration system stored in `~/.hermes/`, combining [`config.yaml`](https://github.com/NousResearch/hermes-agent/blob/main/config.yaml) for settings and `.env` for secrets, with automatic schema migration handled by `migrate_config()` in [`hermes_cli/config.py`](https://github.com/NousResearch/hermes-agent/blob/main/hermes_cli/config.py).**

The NousResearch/hermes-agent project implements a sophisticated configuration management system in [`hermes_cli/config.py`](https://github.com/NousResearch/hermes-agent/blob/main/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 `compact` mode, `personality` settings, `resume_display`, and `tool_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`](https://github.com/NousResearch/hermes-agent/blob/main/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:

```python
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:

```python

# 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:

```python

# Run migration manually (e.g., after a fresh checkout)

result = config.migrate_config(interactive=False)
print("Migration summary:", result)

```

CLI equivalents for daily use:

```bash

# 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 by `get_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_version` field enables automatic detection of outdated schemas via `check_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()` and `save_env_value()` with UTF-8 safety and `getpass` for 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`](https://github.com/NousResearch/hermes-agent/blob/main/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`](https://github.com/NousResearch/hermes-agent/blob/main/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`](https://github.com/NousResearch/hermes-agent/blob/main/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.