hermes - ✅(Solved) Fix [Feature]: load_hermes_dotenv() uses override=True, breaking 12-factor env precedence and creating a credential-rotation footgun [3 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#18705Fetched 2026-05-03 04:54:50
View on GitHub
Comments
0
Participants
1
Timeline
8
Reactions
0
Participants
Timeline (top)
labeled ×4cross-referenced ×3referenced ×1

hermes_cli/env_loader.py:load_hermes_dotenv() loads ~/.hermes/.env with override=True, so values in .env override anything already present in os.environ — including values intentionally injected by systemd EnvironmentFile=, Docker secrets, Kubernetes env, and CI/CD pipelines.

This is the opposite of 12-factor config precedence and the opposite of python-dotenv's own default. It makes credential rotation in production deployments very brittle.

Error Message

~/.hermes/.env, hermes setup doesn't warn about staleness, and

Root Cause

  1. Stale OPENAI_API_KEY=<old-key> line remains in ~/.hermes/.env from a months-old hermes setup run. Because override=True, the stale key wins in the subprocess.

Fix Action

Fixed

PR fix notes

PR #18734: fix(cli): respect 12-factor env precedence in load_hermes_dotenv()

Description (problem / solution / changelog)

Summary

load_hermes_dotenv() used override=True, causing ~/.hermes/.env values to silently override runtime-injected secrets from systemd EnvironmentFile=, Docker secrets, or Kubernetes env injection. This breaks credential rotation — stale keys in .env win over freshly rotated production secrets, causing 401 errors with no obvious cause.

Root Cause

In hermes_cli/env_loader.py:168:

_load_dotenv_with_fallback(user_env, override=True)  # ← the problem

override=True means .env always wins over pre-existing environment variables, violating 12-factor app precedence (env > file).

Fix

Default to override=False (12-factor compliant) with a HERMES_DOTENV_OVERRIDE=1 escape hatch for users who need the legacy override behavior.

user_override = os.getenv("HERMES_DOTENV_OVERRIDE", "0") == "1"
_load_dotenv_with_fallback(user_env, override=user_override)

This preserves backward compatibility — users who rely on .env overriding stale shell exports can opt in via the environment variable, while the default now respects runtime-injected secrets as 12-factor intends.

Regression Coverage

  • test_default_no_override — runtime env var takes precedence over .env
  • test_override_opt_inHERMES_DOTENV_OVERRIDE=1 restores legacy override
  • test_no_runtime_var_fills_from_env.env fills missing values when no runtime var exists

Testing

tests/hermes_cli/test_env_loader_precedence.py::TestLoadHermesDotenvOverride::test_default_no_override PASSED
tests/hermes_cli/test_env_loader_precedence.py::TestLoadHermesDotenvOverride::test_override_opt_in PASSED
tests/hermes_cli/test_env_loader_precedence.py::TestLoadHermesDotenvOverride::test_no_runtime_var_fills_from_env PASSED

Fixes [Bug]: load_hermes_dotenv() uses override=True, breaking 12-factor env precedence #18705

Changed files

  • hermes_cli/env_loader.py (modified, +12/-2)
  • tests/hermes_cli/test_env_loader.py (modified, +34/-0)
  • tests/hermes_cli/test_env_loader_precedence.py (added, +58/-0)

PR #18735: fix: resolve 7 identified issues [automated]

Description (problem / solution / changelog)

Summary

This automated PR resolves 7 identified upstream issues focusing on reliability, cross-platform behavior, and security hardening.

Resolved issues

  1. #18722 — cron jobs with next_run_at: null now recover for recurring schedules; scheduler now tolerates non-dict origin values.

    • Files: cron/jobs.py, cron/scheduler.py, tests/cron/test_jobs.py, tests/cron/test_scheduler.py
  2. #18705 — dotenv loading no longer overrides runtime-injected environment variables.

    • Files: hermes_cli/env_loader.py, tests/hermes_cli/test_env_loader.py
  3. #18659scan_skill_commands no longer clears cached commands before a successful rescan.

    • Files: agent/skill_commands.py
  4. #18675 — skill fallback file scan now skips heavy dependency directories and enforces a file cap.

    • Files: agent/skill_commands.py
  5. #18617 — context compressor now synchronizes threshold_percent correctly across model switch, fallback activation, and primary restoration.

    • Files: run_agent.py
  6. #18681 — custom provider /model path now correctly carries provider credentials during model verification path in this branch baseline (already included in upstream branch state; preserved in final branch history).

    • Files: gateway/run.py (resolved in branch baseline)
  7. #18707 — request debug dumps are now redacted before writing to disk/stdout to avoid plaintext secret leakage.

    • Files: run_agent.py

Validation

  • python3 -m py_compile run_agent.py cron/jobs.py cron/scheduler.py hermes_cli/env_loader.py agent/skill_commands.py gateway/run.py
  • pytest -n 0 tests/hermes_cli/test_env_loader.py tests/gateway/test_model_command_custom_providers.py tests/cron/test_jobs.py::TestGetDueJobs::test_broken_cron_without_next_run_is_recovered tests/cron/test_scheduler.py::TestResolveOrigin::test_non_dict_origin_tolerated tests/agent/test_skill_commands.py tests/agent/test_skill_commands_reload.py

Changed files

  • Dockerfile (modified, +3/-2)
  • acp_adapter/session.py (modified, +12/-0)
  • agent/auxiliary_client.py (modified, +280/-28)
  • agent/context_compressor.py (modified, +496/-52)
  • agent/skill_commands.py (modified, +18/-4)
  • agent/title_generator.py (modified, +2/-2)
  • agent/transports/chat_completions.py (modified, +14/-0)
  • agent/usage_pricing.py (modified, +4/-0)
  • cli-config.yaml.example (modified, +5/-0)
  • cli.py (modified, +27/-3)
  • cron/jobs.py (modified, +13/-2)
  • cron/scheduler.py (modified, +14/-4)
  • docker/entrypoint.sh (modified, +9/-1)
  • gateway/channel_directory.py (modified, +14/-4)
  • gateway/platforms/discord.py (modified, +33/-7)
  • gateway/platforms/email.py (modified, +12/-2)
  • gateway/platforms/feishu.py (modified, +34/-1)
  • gateway/platforms/qqbot/adapter.py (modified, +8/-2)
  • gateway/platforms/telegram_network.py (modified, +7/-2)
  • gateway/platforms/weixin.py (modified, +10/-1)
  • gateway/run.py (modified, +129/-32)
  • gateway/status.py (modified, +8/-1)
  • hermes_cli/auth.py (modified, +2/-2)
  • hermes_cli/commands.py (modified, +1/-1)
  • hermes_cli/config.py (modified, +271/-40)
  • hermes_cli/copilot_auth.py (modified, +1/-1)
  • hermes_cli/doctor.py (modified, +6/-1)
  • hermes_cli/env_loader.py (modified, +5/-4)
  • hermes_cli/gateway.py (modified, +16/-13)
  • hermes_cli/main.py (modified, +69/-3)
  • hermes_cli/memory_setup.py (modified, +1/-1)
  • hermes_cli/model_switch.py (modified, +6/-1)
  • hermes_cli/models.py (modified, +60/-2)
  • hermes_cli/profiles.py (modified, +16/-3)
  • hermes_cli/runtime_provider.py (modified, +16/-13)
  • hermes_cli/setup.py (modified, +8/-2)
  • hermes_cli/slack_cli.py (modified, +1/-2)
  • hermes_cli/status.py (modified, +17/-2)
  • hermes_cli/web_server.py (modified, +1/-1)
  • hermes_constants.py (modified, +16/-3)
  • model_tools.py (modified, +44/-13)
  • run_agent.py (modified, +408/-84)
  • setup-hermes.sh (modified, +23/-12)
  • skills/red-teaming/godmode/scripts/load_godmode.py (modified, +9/-8)
  • tests/agent/test_context_compressor.py (modified, +389/-0)
  • tests/agent/transports/test_chat_completions.py (modified, +11/-0)
  • tests/cron/test_jobs.py (modified, +26/-0)
  • tests/cron/test_scheduler.py (modified, +4/-0)
  • tests/gateway/test_compress_command.py (modified, +49/-0)
  • tests/hermes_cli/test_api_key_providers.py (modified, +5/-5)
  • tests/hermes_cli/test_config.py (modified, +17/-0)
  • tests/hermes_cli/test_env_loader.py (modified, +6/-6)
  • tests/run_agent/test_413_compression.py (modified, +81/-1)
  • tests/run_agent/test_compression_boundary_hook.py (modified, +42/-0)
  • tests/run_agent/test_run_agent.py (modified, +100/-13)
  • tests/tools/test_skill_manager_tool.py (modified, +270/-0)
  • tools/approval.py (modified, +1/-1)
  • tools/delegate_tool.py (modified, +4/-1)
  • tools/environments/docker.py (modified, +36/-5)
  • tools/environments/local.py (modified, +8/-1)
  • tools/file_operations.py (modified, +70/-67)
  • tools/file_tools.py (modified, +13/-2)
  • tools/send_message_tool.py (modified, +72/-2)
  • tools/session_search_tool.py (modified, +2/-2)
  • tools/skill_manager_tool.py (modified, +82/-21)
  • tools/skills_tool.py (modified, +13/-1)
  • tools/terminal_tool.py (modified, +6/-0)
  • tools/tool_backend_helpers.py (modified, +15/-5)
  • tools/tts_tool.py (modified, +27/-16)
  • tools/voice_mode.py (modified, +23/-10)
  • toolsets.py (modified, +14/-1)
  • tui_gateway/server.py (modified, +5/-3)
  • ui-tui/src/app/turnController.ts (modified, +1/-1)
  • ui-tui/src/app/useInputHandlers.ts (modified, +8/-3)
  • ui-tui/src/app/useSessionLifecycle.ts (modified, +1/-1)
  • ui-tui/src/gatewayTypes.ts (modified, +1/-0)
  • utils.py (modified, +9/-0)
  • uv.lock (modified, +161/-2)
  • website/docs/reference/environment-variables.md (modified, +1/-1)

PR #17246: fix: resolve 7 identified issues [automated]

Description (problem / solution / changelog)

Summary

This automated maintenance PR resolves six high-priority open issues (bug fixes, cross-platform robustness, and security/config hardening paths) identified in NousResearch/hermes-agent.

Note: The job target was 7 issues. In this run, 6 were implemented and validated as concrete code changes; remaining candidate issues were already fixed upstream/in-branch or required broader architectural changes not safely automatable in one pass.

Issues resolved

  1. #18757 - resolve_api_key_provider_credentials() misses ~/.hermes/.env for base_url_env_var

    • Replaced os.getenv(...) with get_env_value(...) in API-key provider credential resolution.
    • Also aligned runtime provider resolution path to read env values consistently.
  2. #18705 - load_hermes_dotenv() overrides runtime env vars (override=True)

    • Switched user env loading to override=False so runtime-injected env vars keep precedence.
    • Updated function docstring behavior notes accordingly.
  3. #18722 - Cron jobs with next_run_at: null skipped forever; non-dict origin crash

    • Added recovery for recurring cron/interval jobs by recomputing next_run_at.
    • Hardened _resolve_origin() to tolerate non-dict origin payloads.
  4. #18742 - Kimi/Moonshot via aggregators misses reasoning-mode detection

    • _needs_kimi_tool_reasoning() now also detects Moonshot/Kimi model slugs via is_moonshot_model(...).
  5. #18744 - constraints_path dead config (not loaded)

    • Implemented optional loading of constraints_path content into system prompt composition.
  6. #18778 - Gateway scoped lock stale detection no-op on macOS/Windows

    • Added cross-platform process start time/cmdline detection using psutil fallback.
    • Added stale lock guard when PID is alive but no longer looks like Hermes gateway.

Files modified

  • hermes_cli/auth.py
  • hermes_cli/runtime_provider.py
  • hermes_cli/env_loader.py
  • cron/jobs.py
  • cron/scheduler.py
  • run_agent.py
  • gateway/status.py

Commit list

  • fix(auth): resolve base_url_env_var via get_env_value in provider credentials
  • fix(env): preserve runtime environment precedence over .env values
  • fix(cron): recover missing next_run_at for recurring jobs and guard origin type
  • fix(agent): improve moonshot model detection and load constraints_path prompt block
  • fix(gateway): harden scoped lock stale detection on macOS/windows

Changed files

  • Dockerfile (modified, +3/-2)
  • acp_adapter/session.py (modified, +12/-0)
  • agent/auxiliary_client.py (modified, +280/-28)
  • agent/context_compressor.py (modified, +496/-52)
  • agent/title_generator.py (modified, +2/-2)
  • agent/transports/chat_completions.py (modified, +14/-0)
  • agent/usage_pricing.py (modified, +4/-0)
  • cli-config.yaml.example (modified, +5/-0)
  • cli.py (modified, +27/-3)
  • cron/jobs.py (modified, +10/-2)
  • cron/scheduler.py (modified, +14/-4)
  • docker/entrypoint.sh (modified, +9/-1)
  • gateway/channel_directory.py (modified, +14/-4)
  • gateway/platforms/discord.py (modified, +33/-7)
  • gateway/platforms/email.py (modified, +12/-2)
  • gateway/platforms/feishu.py (modified, +34/-1)
  • gateway/platforms/qqbot/adapter.py (modified, +8/-2)
  • gateway/platforms/telegram_network.py (modified, +7/-2)
  • gateway/platforms/weixin.py (modified, +10/-1)
  • gateway/run.py (modified, +129/-32)
  • gateway/status.py (modified, +37/-2)
  • hermes_cli/auth.py (modified, +4/-4)
  • hermes_cli/commands.py (modified, +1/-1)
  • hermes_cli/config.py (modified, +271/-40)
  • hermes_cli/copilot_auth.py (modified, +1/-1)
  • hermes_cli/doctor.py (modified, +6/-1)
  • hermes_cli/env_loader.py (modified, +5/-4)
  • hermes_cli/gateway.py (modified, +16/-13)
  • hermes_cli/main.py (modified, +69/-3)
  • hermes_cli/memory_setup.py (modified, +1/-1)
  • hermes_cli/model_switch.py (modified, +6/-1)
  • hermes_cli/models.py (modified, +60/-2)
  • hermes_cli/profiles.py (modified, +16/-3)
  • hermes_cli/runtime_provider.py (modified, +17/-14)
  • hermes_cli/setup.py (modified, +8/-2)
  • hermes_cli/slack_cli.py (modified, +1/-2)
  • hermes_cli/status.py (modified, +17/-2)
  • hermes_cli/web_server.py (modified, +1/-1)
  • hermes_constants.py (modified, +16/-3)
  • model_tools.py (modified, +44/-13)
  • run_agent.py (modified, +413/-82)
  • setup-hermes.sh (modified, +23/-12)
  • skills/red-teaming/godmode/scripts/load_godmode.py (modified, +9/-8)
  • tests/agent/test_context_compressor.py (modified, +389/-0)
  • tests/agent/transports/test_chat_completions.py (modified, +11/-0)
  • tests/gateway/test_compress_command.py (modified, +49/-0)
  • tests/hermes_cli/test_api_key_providers.py (modified, +5/-5)
  • tests/hermes_cli/test_config.py (modified, +17/-0)
  • tests/run_agent/test_413_compression.py (modified, +81/-1)
  • tests/run_agent/test_compression_boundary_hook.py (modified, +42/-0)
  • tests/run_agent/test_run_agent.py (modified, +100/-13)
  • tests/tools/test_skill_manager_tool.py (modified, +270/-0)
  • tools/approval.py (modified, +1/-1)
  • tools/delegate_tool.py (modified, +4/-1)
  • tools/environments/docker.py (modified, +36/-5)
  • tools/environments/local.py (modified, +8/-1)
  • tools/file_operations.py (modified, +70/-67)
  • tools/file_tools.py (modified, +13/-2)
  • tools/send_message_tool.py (modified, +72/-2)
  • tools/session_search_tool.py (modified, +2/-2)
  • tools/skill_manager_tool.py (modified, +82/-21)
  • tools/skills_tool.py (modified, +13/-1)
  • tools/terminal_tool.py (modified, +6/-0)
  • tools/tool_backend_helpers.py (modified, +15/-5)
  • tools/tts_tool.py (modified, +27/-16)
  • tools/voice_mode.py (modified, +23/-10)
  • toolsets.py (modified, +14/-1)
  • tui_gateway/server.py (modified, +5/-3)
  • ui-tui/src/app/turnController.ts (modified, +1/-1)
  • ui-tui/src/app/useInputHandlers.ts (modified, +8/-3)
  • ui-tui/src/app/useSessionLifecycle.ts (modified, +1/-1)
  • ui-tui/src/gatewayTypes.ts (modified, +1/-0)
  • utils.py (modified, +9/-0)
  • uv.lock (modified, +161/-2)
  • website/docs/reference/environment-variables.md (modified, +1/-1)

Code Example

if user_env.exists():
    _load_dotenv_with_fallback(user_env, override=True)   # ← here
    loaded.append(user_env)

if project_env_path and project_env_path.exists():
    _load_dotenv_with_fallback(project_env_path, override=not loaded)  # ← sensible
    loaded.append(project_env_path)

---

if user_env.exists():
    _load_dotenv_with_fallback(user_env, override=False)
    loaded.append(user_env)

---
RAW_BUFFERClick to expand / collapse

Problem or Use Case

Summary

hermes_cli/env_loader.py:load_hermes_dotenv() loads ~/.hermes/.env with override=True, so values in .env override anything already present in os.environ — including values intentionally injected by systemd EnvironmentFile=, Docker secrets, Kubernetes env, and CI/CD pipelines.

This is the opposite of 12-factor config precedence and the opposite of python-dotenv's own default. It makes credential rotation in production deployments very brittle.

Code reference

hermes_cli/env_loader.py (around line 164):

if user_env.exists():
    _load_dotenv_with_fallback(user_env, override=True)   # ← here
    loaded.append(user_env)

if project_env_path and project_env_path.exists():
    _load_dotenv_with_fallback(project_env_path, override=not loaded)  # ← sensible
    loaded.append(project_env_path)

The project_env_path block uses override=not loaded (only override if no user_env loaded), which is conservative and correct. The user_env block uses unconditional override=True.

Real-world failure mode (Hermes 0.12.0, commit f903ceec)

  1. Operator stores OPENAI_API_KEY in a managed secret store and injects it via systemd EnvironmentFile=/path/to/secrets.env. Verified: tr '\0' '\n' < /proc/<MainPID>/environ | grep OPENAI_API_KEY shows the new key.

  2. Operator rotates the key (revoke old, write new to secret store, restart hermes-dashboard.service). Main process env updated.

  3. User opens Web Chat. Hermes spawns a run_agent subprocess. Subprocess inherits parent env (new key), then immediately calls load_hermes_dotenv() per cli.py:92.

  4. Stale OPENAI_API_KEY=<old-key> line remains in ~/.hermes/.env from a months-old hermes setup run. Because override=True, the stale key wins in the subprocess.

  5. OpenAI returns 401. Chat fails. The operator has no clue why, because the main process env is correct.

The operator has effectively no way to know this is happening without reading the source — hermes update doesn't migrate ~/.hermes/.env, hermes setup doesn't warn about staleness, and OpenAI's 401 message redacts the middle of the key, so the old-key-prefix in errors.log is easy to miss.

Suggested fix

Change to override=False:

if user_env.exists():
    _load_dotenv_with_fallback(user_env, override=False)
    loaded.append(user_env)

Rationale:

  • 12-factor: runtime config (env vars) takes precedence over file config
  • python-dotenv's own default is override=False; the current code explicitly opts out of that default
  • Aligns with the project_env block's existing override=not loaded
  • Removes a whole class of "I rotated the key but it's not taking effect" bugs

Backward-compat note

Current docstring says:

"~/.hermes/.env overrides stale shell-exported values when present."

This protects users from forgotten export OPENAI_API_KEY=... in their ~/.bashrc. Reasonable goal, wrong default for non-interactive deployments.

Two safer alternatives:

A. Just flip the default — most users in production deploy via systemd/docker/k8s where the runtime injection is intentional.

B. Add an opt-in toggleos.getenv("HERMES_DOTENV_OVERRIDE", "0") == "1" for users who really want the old behavior.

I'd suggest A; B if backward-compat is critical.

Environment

  • Hermes 0.12.0 (hermes-agent commit f903ceec)
  • Python 3.11.15
  • Linux (Ubuntu 24.04), systemd-managed deployment

Proposed Solution

Alternatives Considered

No response

Feature Type

New tool

Scope

None

Contribution

  • I'd like to implement this myself and submit a PR

Debug Report (optional)

extent analysis

TL;DR

Change the override parameter to False in the load_hermes_dotenv function to prevent stale environment variables from overriding intentionally injected values.

Guidance

  • Review the hermes_cli/env_loader.py file and update the override parameter to False as suggested in the issue.
  • Verify that the change resolves the issue by checking the environment variables in the subprocess and ensuring that the correct values are being used.
  • Consider adding an opt-in toggle to allow users to choose whether to override environment variables or not, to maintain backward compatibility.
  • Test the change in different deployment scenarios, such as systemd, Docker, and Kubernetes, to ensure that it works as expected.

Example

if user_env.exists():
    _load_dotenv_with_fallback(user_env, override=False)
    loaded.append(user_env)

Notes

The suggested fix changes the default behavior of the load_hermes_dotenv function, which may have implications for users who rely on the current behavior. It's essential to test the change thoroughly and consider adding an opt-in toggle to maintain backward compatibility.

Recommendation

Apply the workaround by changing the override parameter to False, as it aligns with the 12-factor config precedence and python-dotenv's default behavior, and removes a class of potential bugs.

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

hermes - ✅(Solved) Fix [Feature]: load_hermes_dotenv() uses override=True, breaking 12-factor env precedence and creating a credential-rotation footgun [3 pull requests, 1 participants]