hermes - ✅(Solved) Fix [Bug]: credential pool stores literal ${VAR} from .env — auxiliary requests send unexpanded variable as API key [3 pull requests, 1 comments, 2 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#20310Fetched 2026-05-06 06:37:24
View on GitHub
Comments
1
Participants
2
Timeline
8
Reactions
0
Timeline (top)
labeled ×4cross-referenced ×3commented ×1

Error Message

AuthenticationError: Invalid X-Api-Key header format

Root Cause

_seed_from_env() in agent/credential_pool.py:1381 uses _get_env_prefer_dotenv() which calls load_env() from hermes_cli/config.py:4033. load_env() parses the .env file with raw string splitting (key, _, value = line.partition('=')) — no variable interpolation.

Since _get_env_prefer_dotenv prefers the raw .env value over os.environ:

def _get_env_prefer_dotenv(key: str) -> str:
    env_file = load_env()
    val = env_file.get(key) or os.environ.get(key) or ""
    return val.strip()

The unexpanded ${OPENAI_API_KEY} wins over the correctly expanded value in os.environ.

Fix Action

Fixed

PR fix notes

PR #20319: fix(agent): skip unexpanded ${VAR} references when seeding credential pool

Description (problem / solution / changelog)

Summary

  • _get_env_prefer_dotenv() in agent/credential_pool.py uses load_env() which reads ~/.hermes/.env as raw text without variable interpolation
  • When the .env contains ANTHROPIC_API_KEY=${OPENAI_API_KEY}, the literal string ${OPENAI_API_KEY} is stored in auth.json
  • Auxiliary tasks (title generation, compression, etc.) then send that raw string as the API key, causing 401 errors
  • The main model path is unaffected because it calls resolve_anthropic_token()os.getenv(), where python-dotenv has already expanded the variable

Fix

Detect unexpanded variable patterns (${...} / $VAR) in the raw .env value and fall back to os.environ where python-dotenv has already expanded them. This preserves the existing preference for .env file values over stale shell env vars while fixing the interpolation gap.

Test plan

  • Set ANTHROPIC_API_KEY=${OPENAI_API_KEY} in ~/.hermes/.env (with OPENAI_API_KEY valid in the environment)
  • Delete ~/.hermes/auth.json and auth.lock
  • Run hermes — verify auth.json now stores the expanded key value, not the literal ${OPENAI_API_KEY}
  • Verify auxiliary tasks (e.g. title generation) succeed without 401 errors

Fixes #20310 Related: #15890 (same root cause in the cron path)

Changed files

  • agent/credential_pool.py (modified, +9/-2)

PR #20328: fix(cli): expand ${VAR} references in load_env to match python-dotenv

Description (problem / solution / changelog)

Resolve ${VAR} references in ~/.hermes/.env so the credential pool stops seeding literal ${OPENAI_API_KEY} strings into auth.json.

What changed and why

  • hermes_cli/config.pyload_env() now expands ${VAR}, $VAR, and ${VAR:-default} references using earlier .env entries first, then os.environ. Unresolvable refs are preserved verbatim so copy-paste mistakes stay visible (rather than silently becoming empty strings).
  • python-dotenv already does this when it loads .env into os.environ. The manual reader in load_env() did not, causing a divergence: os.environ['ANTHROPIC_API_KEY'] was the resolved value, but load_env()['ANTHROPIC_API_KEY'] was the literal ${OPENAI_API_KEY}. After #18755 made _get_env_prefer_dotenv prefer the .env file, that literal won — and the credential pool persisted it into auth.json, breaking auxiliary requests (title generation, compression) with Invalid X-Api-Key header format.
  • Tests: 8 unit tests for the new interpolation + 1 integration test in test_credential_pool_env_fallback.py reproducing the exact ANTHROPIC_API_KEY=${OPENAI_API_KEY} scenario from the issue.

How to test

  1. In ~/.hermes/.env, set ANTHROPIC_API_KEY=${OPENAI_API_KEY} with a valid OPENAI_API_KEY exported in the shell.
  2. Delete ~/.hermes/auth.json and ~/.hermes/auth.lock.
  3. Run hermes and trigger an auxiliary task (e.g. session title generation).
  4. Confirm auth.json contains the resolved key, not the literal ${OPENAI_API_KEY}.
  5. pytest tests/hermes_cli/test_load_env_var_expansion.py tests/tools/test_credential_pool_env_fallback.py — all green.

What platforms tested on

  • macOS on darwin-arm64 (local)

Fixes #20310

<!-- autocontrib:worker-id=issue-new-ce040990 kind=pr-open -->

Changed files

  • hermes_cli/config.py (modified, +49/-4)
  • tests/hermes_cli/test_load_env_var_expansion.py (added, +138/-0)
  • tests/tools/test_credential_pool_env_fallback.py (modified, +25/-0)

PR #20541: fix(auth): expand dotenv refs before credential pool seeding

Description (problem / solution / changelog)

Summary

  • expand $VAR / ${VAR} references from ~/.hermes/.env before seeding env credentials into the credential pool
  • preserve existing .env-over-shell precedence, including references to other values in the same .env
  • skip unresolved references instead of persisting raw ${VAR} strings to auth.json

Fixes #20310.

Verification

  • scripts/run_tests.sh tests/agent/test_credential_pool.py -k "dotenv_reference or prefers_dotenv_over_stale_os_environ or falls_back_to_os_environ" -> 5 passed
  • scripts/run_tests.sh tests/agent/test_credential_pool.py -> 44 passed
  • git diff --check

Non-goals

  • Does not change the general load_env() parser contract outside credential-pool seeding.

Changed files

  • agent/credential_pool.py (modified, +25/-2)
  • tests/agent/test_credential_pool.py (modified, +72/-0)

Code Example

ANTHROPIC_API_KEY=${OPENAI_API_KEY}

---

"anthropic": [{
     "access_token": "${OPENAI_API_KEY}",
     "source": "env:ANTHROPIC_API_KEY"
   }]

---

AuthenticationError: Invalid X-Api-Key header format

---

def _get_env_prefer_dotenv(key: str) -> str:
    env_file = load_env()
    val = env_file.get(key) or os.environ.get(key) or ""
    return val.strip()
RAW_BUFFERClick to expand / collapse

Bug Description

When ~/.hermes/.env uses variable references like ANTHROPIC_API_KEY=${OPENAI_API_KEY}, the credential pool seeds the literal string ${OPENAI_API_KEY} into auth.json instead of the expanded value. Auxiliary tasks (title generation, compression, etc.) then send that raw string as the x-api-key header, causing 401 errors.

The main model path works fine because it calls resolve_anthropic_token()os.getenv(), where python-dotenv has already expanded the variable. But the credential pool path goes through _get_env_prefer_dotenv()load_env(), which reads the .env file as raw text without interpolation — and prefers that raw value over os.environ.

Steps to Reproduce

  1. Set up .env with a variable reference:

    ANTHROPIC_API_KEY=${OPENAI_API_KEY}

    (where OPENAI_API_KEY is a valid key in the environment)

  2. Delete ~/.hermes/auth.json and auth.lock

  3. Run hermes — auth.json is recreated with:

    "anthropic": [{
      "access_token": "${OPENAI_API_KEY}",
      "source": "env:ANTHROPIC_API_KEY"
    }]
  4. Any auxiliary task (e.g. title generation) fails:

    AuthenticationError: Invalid X-Api-Key header format

Root Cause

_seed_from_env() in agent/credential_pool.py:1381 uses _get_env_prefer_dotenv() which calls load_env() from hermes_cli/config.py:4033. load_env() parses the .env file with raw string splitting (key, _, value = line.partition('=')) — no variable interpolation.

Since _get_env_prefer_dotenv prefers the raw .env value over os.environ:

def _get_env_prefer_dotenv(key: str) -> str:
    env_file = load_env()
    val = env_file.get(key) or os.environ.get(key) or ""
    return val.strip()

The unexpanded ${OPENAI_API_KEY} wins over the correctly expanded value in os.environ.

Expected Behavior

The credential pool should store the resolved/expanded value, not the literal ${VAR} string. Either:

  1. load_env() should expand ${VAR} references (consistent with python-dotenv behavior), or
  2. _get_env_prefer_dotenv() should detect unexpanded variable references and fall back to os.environ, or
  3. _get_env_prefer_dotenv() should prefer os.environ over raw file parsing (reversing the current priority)

Related

#15890 — same root cause (load_env() not expanding variables) but in the cron path. The credential pool is a more impactful surface since it silently persists the bad value to auth.json and breaks all auxiliary tasks.

Environment

  • Hermes version: latest main
  • OS: macOS
  • Python: 3.13.5

extent analysis

TL;DR

Modify _get_env_prefer_dotenv() to prefer os.environ over raw .env file parsing or implement variable expansion in load_env().

Guidance

  • Review the _get_env_prefer_dotenv() function to understand how it prioritizes environment variable sources and consider reversing the priority to prefer os.environ over the raw .env file value.
  • Investigate implementing variable expansion in load_env() similar to python-dotenv to ensure consistent behavior across different paths.
  • Verify that the auth.json file is correctly updated with the expanded value after making changes to _get_env_prefer_dotenv() or load_env().
  • Test auxiliary tasks to confirm they no longer produce 401 errors due to incorrect x-api-key headers.

Example

def _get_env_prefer_dotenv(key: str) -> str:
    # Prefer os.environ over raw .env file value
    val = os.environ.get(key) or load_env().get(key) or ""
    return val.strip()

Notes

The current implementation of load_env() and _get_env_prefer_dotenv() leads to inconsistent behavior between the main model path and auxiliary tasks. Resolving this inconsistency is crucial for ensuring that all tasks use the correct, expanded environment variable values.

Recommendation

Apply a workaround by modifying _get_env_prefer_dotenv() to prefer os.environ over the raw .env file parsing until a more comprehensive solution, such as expanding variables in load_env(), can be implemented. This approach ensures that auxiliary tasks use the correct environment variable values without waiting for a full fix.

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