hermes - ✅(Solved) Fix Non-secret gateway settings in .env silently override config.yaml [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#13582Fetched 2026-04-22 08:05:35
View on GitHub
Comments
0
Participants
1
Timeline
5
Reactions
0
Author
Participants
Timeline (top)
labeled ×3cross-referenced ×1referenced ×1

Error Message

  • warn clearly when a non-secret config.yaml key is being ignored because of .env

Root Cause

This is very easy to miss because users reasonably expect config.yaml to be the authoritative place for non-secret behavior config.

Fix Action

Fixed

PR fix notes

PR #13629: fix(gateway-config): warn when non-secret settings conflict between config.yaml and .env (#13582)

Description (problem / solution / changelog)

Fixes #13582.

TL;DR

Stale non-secret entries in ~/.hermes/.env silently override their config.yaml counterparts. Reporter hit it with DISCORD_REPLY_TO_MODE=off in .env winning over discord.reply_to_mode: first in config.yaml, with no diagnostic.

Fix: add a non-breaking warning pass that surfaces these conflicts. Env still wins (behavior unchanged); users just see which setting got overridden so they can clean up their .env.

Approach

The issue proposed two options:

  • Option A: make config.yaml authoritative for non-secrets (breaking change)
  • Option B: warn when both sources specify the same setting (non-breaking)

This PR is Option B. A stricter Option A — flipping precedence — is intentionally left for a separate, larger-scope discussion.

Fix

A small static allow-list of non-secret settings known to have both YAML and env-var forms:

_YAML_ENV_NON_SECRET_MAPPINGS: tuple[tuple[str, str], ...] = (
    # Discord
    ("discord.reply_to_mode",           "DISCORD_REPLY_TO_MODE"),  # reporter's case
    ("discord.require_mention",         "DISCORD_REQUIRE_MENTION"),
    ("discord.auto_thread",             "DISCORD_AUTO_THREAD"),
    ("discord.reactions",               "DISCORD_REACTIONS"),
    ("discord.free_response_channels",  "DISCORD_FREE_RESPONSE_CHANNELS"),
    ("discord.ignored_channels",        "DISCORD_IGNORED_CHANNELS"),
    ("discord.allowed_channels",        "DISCORD_ALLOWED_CHANNELS"),
    ("discord.no_thread_channels",      "DISCORD_NO_THREAD_CHANNELS"),
    # Telegram
    ("telegram.reply_to_mode",          "TELEGRAM_REPLY_TO_MODE"),
    ("telegram.require_mention",        "TELEGRAM_REQUIRE_MENTION"),
    ("telegram.reactions",              "TELEGRAM_REACTIONS"),
    ("telegram.free_response_chats",    "TELEGRAM_FREE_RESPONSE_CHATS"),
    ("telegram.ignored_threads",        "TELEGRAM_IGNORED_THREADS"),
    ("telegram.proxy_url",              "TELEGRAM_PROXY"),
    # Slack
    ("slack.require_mention",           "SLACK_REQUIRE_MENTION"),
    ("slack.allow_bots",                "SLACK_ALLOW_BOTS"),
    ("slack.free_response_channels",    "SLACK_FREE_RESPONSE_CHANNELS"),
)

And a single helper called once near the top of load_gateway_config:

def _warn_yaml_env_conflicts(yaml_cfg, config_yaml_path):
    for dotted_key, env_var in _YAML_ENV_NON_SECRET_MAPPINGS:
        # walk dotted path, skip if YAML key absent
        # skip if env var absent or empty
        # skip if values match (case-insensitive, same bool/list serialization)
        logger.warning(
            "Gateway config conflict: %s in %s is %r but %s=%r is set in "
            "your environment (typically ~/.hermes/.env). The env var takes "
            "priority by design, so the config.yaml value will have no effect. "
            "Remove %s from .env to make config.yaml authoritative, or update "
            "the .env value to match. (#13582)",
            dotted_key, config_yaml_path.name, yaml_val,
            env_var, env_val, env_var,
        )

Behaviour matrix

ScenarioBeforeAfter
YAML and env both set, values differsilent override (bug)warning + override (env still wins)
YAML and env both set, values matchsilent (consistent)silent (no user surprise)
YAML onlyYAML winsYAML wins (unchanged)
Env onlyenv winsenv wins (unchanged)
Token (DISCORD_BOT_TOKEN) in bothsilentsilent (tokens deliberately excluded from allow-list)
YAML has list, env has comma-joined string that matchessilentsilent (serialization handled: [a, b]a,b)
YAML missing parent key, env setsilentsilent (walker doesn't raise)

Narrow scope — explicitly not changed

  • Precedence order. Env still wins. This PR does NOT implement the reporter's Option A (YAML authoritative) because that would break existing deployments relying on .env overrides.
  • Tokens / credentials / home-channel IDs. Deliberately absent from the allow-list. Secrets are env-first by convention. Pinned by test_tokens_not_covered.
  • YAML → env bridges. The existing bridges (and not os.getenv(...)) are untouched — the warning runs before them and doesn't interfere.
  • Output level. WARNING is the right severity: actionable, not fatal. Users can silence it with log config if intentional.

Regression coverage

tests/gateway/test_config.py::TestYamlEnvConflictWarnings12 new cases:

Bug-surfacing (5):

  • test_reporter_repro_discord_reply_to_mode_warns — reporter's exact scenario
  • test_boolean_yaml_compared_as_lowercased_string — bool YAML vs "true"/"false" env
  • test_list_yaml_compared_as_comma_joined — list YAML vs comma-joined env
  • test_multiple_conflicts_each_warn_once — independent warnings per setting
  • test_slack_mappings_covered — non-Discord mappings also fire

Preserved-behaviour canaries (5):

  • test_no_warning_when_yaml_only
  • test_no_warning_when_env_only
  • test_no_warning_when_values_match
  • test_value_match_is_case_insensitive
  • test_empty_env_value_treated_as_unset

Narrow-scope canaries (2):

  • test_tokens_not_coveredDISCORD_BOT_TOKEN etc. must NOT trigger
  • test_nested_missing_path_no_warning — walker doesn't raise

5 of 12 fail on clean origin/main (04f9ffb7) with assert 0 == 1 (expected 1 warning, got 0) — the scan isn't there yet. The 7 remaining pin preserved behaviour.

Validation

source venv/bin/activate
python -m pytest tests/gateway/test_config.py::TestYamlEnvConflictWarnings -q
# 12 passed

Broader config-related suites (test_config.py, test_config_cwd_bridge.py, test_display_config.py, test_session_env.py, test_stt_config.py) → 102 passed, 0 regressions.

Pre-empted review questions

Q. Why a static allow-list instead of introspecting the schema? The YAML→env bridges in this file are hand-written per setting — there's no unified schema to derive from. A static allow-list is the minimal, auditable surface that matches reality. If future refactors unify the bridges into a schema, the allow-list can be replaced by that schema.

Q. Why not make this an error at startup? Env is legitimately authoritative in many deployment patterns (Docker, Kubernetes secrets). A warning surfaces the conflict without blocking startup for setups that know what they're doing.

Q. What if a user sets ANTHROPIC_API_KEY in both places? Not covered — tokens/credentials are intentionally absent from the allow-list. Secrets belong in env; duplicating them in YAML is a different class of bug (and a security problem in itself). Separate fix if anyone cares about that.

Q. Could the warning mention which file the env var came from specifically (.env vs shell vs Docker)? The Python process can't reliably distinguish — os.getenv returns a merged view. The warning says "typically ~/.hermes/.env" because that's the most common source; the env var name is named explicitly so the user can grep for it wherever.


<sub>Co-authored via LLM assistance; I've reviewed every line and am responsible for correctness.</sub>

Changed files

  • gateway/config.py (modified, +125/-0)
  • tests/gateway/test_config.py (modified, +254/-0)
RAW_BUFFERClick to expand / collapse

Bug Description

Non-secret gateway settings in ~/.hermes/.env silently override the same settings in ~/.hermes/config.yaml, which makes config.yaml edits appear ineffective after restart.

I hit this with Discord reply_to_mode:

  • ~/.hermes/config.yaml had discord.reply_to_mode: first
  • ~/.hermes/.env still had DISCORD_REPLY_TO_MODE=off
  • after hermes gateway restart, runtime behavior stayed off

This is very easy to miss because users reasonably expect config.yaml to be the authoritative place for non-secret behavior config.

Current Behavior

gateway/config.py documents merge priority as:

  1. Environment variables
  2. ~/.hermes/config.yaml
  3. ~/.hermes/gateway.json
  4. Built-in defaults

And for Discord specifically, config bridging only happens when the env var is absent:

  • gateway/config.py:435-439
  • gateway/config.py:578-605
  • gateway/config.py:744-799

So once a stale DISCORD_* value exists in .env, config.yaml can no longer change that behavior.

Why this is a problem

This makes .env act as a hidden high-priority shadow config for non-secret settings such as:

  • reply_to_mode
  • require_mention
  • free_response_channels
  • allowed_channels
  • auto_thread
  • reactions

In practice, users edit config.yaml, restart, see no effect, and have no obvious signal that .env won.

Expected Behavior

One of these should be true:

Option A

Treat .env as secrets / credentials only, and make config.yaml authoritative for non-secret gateway behavior config.

Option B

If keeping env overrides is intentional, Hermes should at least:

  • warn clearly when a non-secret config.yaml key is being ignored because of .env
  • provide a migration / cleanup path to remove duplicated non-secret keys from .env

Suggested Fix

My preference is:

  1. keep credentials/tokens in .env
  2. move non-secret behavior config authority to config.yaml
  3. stop letting stale DISCORD_* / TELEGRAM_* behavior vars in .env silently override config.yaml

If a breaking change is a concern, even adding startup warnings for duplicated keys would already remove a lot of confusion.

Repro

  1. Put discord.reply_to_mode: first in ~/.hermes/config.yaml
  2. Put DISCORD_REPLY_TO_MODE=off in ~/.hermes/.env
  3. Restart gateway
  4. Observe runtime still behaves as off

Environment

  • Hermes repo: NousResearch/hermes-agent
  • Platform: Discord gateway
  • OS: macOS

extent analysis

TL;DR

To fix the issue, consider moving non-secret behavior config authority to config.yaml and stopping stale environment variables from silently overriding it.

Guidance

  • Review ~/.hermes/.env for any non-secret settings that may be overriding ~/.hermes/config.yaml and remove or update them accordingly.
  • Verify that config.yaml edits are effective by checking the runtime behavior after restarting the gateway.
  • Consider adding startup warnings for duplicated keys to remove confusion, as a temporary solution.
  • Evaluate the trade-offs between treating .env as secrets-only and keeping env overrides, and choose the approach that best fits your use case.

Example

No code snippet is provided as the issue is more related to configuration and environment variable management.

Notes

The suggested fix involves changing the priority of configuration sources, which may have implications for existing workflows and user expectations. It's essential to weigh the benefits of making config.yaml authoritative for non-secret gateway behavior config against potential breaking changes.

Recommendation

Apply a workaround by removing non-secret settings from ~/.hermes/.env and relying on ~/.hermes/config.yaml for configuration, as this approach aligns with the expected behavior of having config.yaml as the primary configuration source.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING