Skip to content

fix(credentials): prefer ~/.hermes/.env over stale os.environ on key rotation#20602

Open
0xDevNinja wants to merge 1 commit into
NousResearch:mainfrom
0xDevNinja:fix/20591-resolve-prefers-dotenv
Open

fix(credentials): prefer ~/.hermes/.env over stale os.environ on key rotation#20602
0xDevNinja wants to merge 1 commit into
NousResearch:mainfrom
0xDevNinja:fix/20591-resolve-prefers-dotenv

Conversation

@0xDevNinja

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes a credential-pool / auth-resolve bug where a deliberate key rotation in ~/.hermes/.env is shadowed by a stale shell export inherited from the parent process (Codex CLI, login profile, test runner). The result was persistent 401s after rotation: the user edits .env, but os.environ still wins and the live request path keeps using the dead key.

The issue's root-cause analysis pointed at _seed_from_env in agent/credential_pool.py, but the real divergence was on the auth resolve path_resolve_api_key_provider_secret was calling get_env_value, which checks os.environ first. So even though the pool's inline _get_env_prefer_dotenv had the right intent, the live request resolution didn't share that policy. Symmetric fix on both sides.

Related Issue

Fixes #20591

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • hermes_cli/config.py: add get_env_value_prefer_dotenv(key) — checks ~/.hermes/.env first, falls back to os.environ. Existing get_env_value is unchanged so generic env reads keep os.environ-first semantics.
  • hermes_cli/auth.py::_resolve_api_key_provider_secret: route Hermes-managed credential lookup through the new helper so the resolve path matches the pool seeding policy.
  • agent/credential_pool.py::_seed_from_env: collapse the inline _get_env_prefer_dotenv onto the shared helper (single source of truth).
  • tests/tools/test_credential_pool_env_fallback.py:
    • flip test_os_environ_still_wins_over_dotenvtest_dotenv_wins_over_stale_os_environ (the prior expectation encoded the bug — it asserted stale shell export wins, which is exactly what users hit on rotation).
    • add test_dotenv_wins_over_stale_os_environ_on_resolve covering the auth resolve path so the pool/auth symmetry is locked in by tests.

How to Test

  1. pytest tests/tools/test_credential_pool_env_fallback.py tests/agent/test_credential_pool.py -q → 51 passed.
  2. Reproduce the original bug manually:
    • export OPENROUTER_API_KEY=sk-or-stale-from-shell
    • edit ~/.hermes/.envOPENROUTER_API_KEY=sk-or-fresh-rotated
    • run hermes — pre-fix the request used the stale shell key (401), post-fix it uses the rotated .env value.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(credentials): …)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes
  • I've tested on my platform: macOS 14.2 (Darwin 23.2.0)

Documentation & Housekeeping

  • I've updated relevant documentation — N/A (internal helper, behavior preserved for get_env_value)
  • I've updated cli-config.yaml.example — N/A (no config keys changed)
  • I've updated CONTRIBUTING.md / AGENTS.md — N/A
  • I've considered cross-platform impact — yes, fix is pure Python over os.environ + dotenv parsing
  • I've updated tool descriptions/schemas — N/A

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent loop, run_agent.py, prompt builder area/auth Authentication, OAuth, credential pools labels May 6, 2026
@0xDevNinja 0xDevNinja force-pushed the fix/20591-resolve-prefers-dotenv branch from ef6eeaa to d8a6b4a Compare May 7, 2026 18:33
…rotation

A deliberate edit to ~/.hermes/.env must win over a value inherited from
the parent shell (Codex CLI, login profile, test runner). Previously the
auth resolve path called get_env_value, which checks os.environ first,
so rotating a key in .env mid-session left both the live request path
and the credential pool seeded with the stale shell export — producing
persistent 401s after rotation.

Add get_env_value_prefer_dotenv in hermes_cli/config.py and route the
two Hermes-managed credential paths through it:

  - agent/credential_pool.py::_seed_from_env (was already inline; now
    centralised)
  - hermes_cli/auth.py::_resolve_api_key_provider_secret

Existing get_env_value is unchanged — generic env reads keep their
os.environ-first behaviour.

Tests:
  - flip test_os_environ_still_wins_over_dotenv to
    test_dotenv_wins_over_stale_os_environ (the prior expectation
    encoded the bug)
  - add test_dotenv_wins_over_stale_os_environ_on_resolve covering the
    auth resolve path symmetric with the pool seeding rule

Fixes NousResearch#20591
@0xDevNinja 0xDevNinja force-pushed the fix/20591-resolve-prefers-dotenv branch from d8a6b4a to da0158f Compare May 11, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent loop, run_agent.py, prompt builder P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: credential pool reads stale os.environ instead of fresh .env — test documents correct behavior but code doesn't match

2 participants