NousResearch/hermes-agent

Auth sync feature for detecting credential drift

Cluster 18359 · hermes cli. Data as of

2 open items. Fix merged in #18755 on 2 May 2026. Check it covers your case.

The short answer

1open issue
1open PR
4items grouped
Known?
Yes. 4 related items grouped here, 0 issues already closed.
Fix event
A fix was merged to main in PR #18755. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
1 issue and 1 PR.

These members concern a proposed or in-progress 'hermes auth sync' feature aimed at detecting and fixing credential drift across .env files, auth.json, and a credential pool. There is also a related fix for preferring ~/.hermes/.env over stale environment variables when seeding the credential pool.

Open issues1

  • Issue#18359[Feature]: hermes auth sync — detect and fix credential drift across .env files and credential pool

Open PRs1

  • PR#52300feat(auth): hermes auth sync — detect and fix credential drift across profile .env files

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #18755 fix: prefer ~/.hermes/.env over stale os.environ when seeding credential pool (salvage #18256)

    merged to main

    files: agent/credential_pool.py, scripts/release.py, tests/agent/test_credential_pool.py

    Show the diff
    • agent/credential_pool.py
      @@ -3,6 +3,7 @@
       from __future__ import annotations
       
       import logging
      +import os
       import random
       import threading
       import time
      @@ -13,7 +14,7 @@
       from typing import Any, Dict, List, Optional, Set, Tuple
       
       from hermes_constants import OPENROUTER_BASE_URL
      -from hermes_cli.config import get_env_value
      +from hermes_cli.config import get_env_value, load_env
       import hermes_cli.auth as auth_mod
       from hermes_cli.auth import (
           CODEX_ACCESS_TOKEN_REFRESH_SKEW_SECONDS,
      @@ -1380,6 +1381,16 @@ def _is_suppressed(_p, _s):  # type: ignore[misc]
       def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool, Set[str]]:
           changed = False
           active_sources: Set[str] = set()
      +
      +    # Prefer ~/.hermes/.env over os.environ — the user's config file is the
      +    # authoritative source for Hermes credentials. Stale env vars from parent
      +    # processes (Codex CLI, test scripts, etc.) should not override deliberate
      +    # changes to the .env file.
      +    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()
      +
           # Honour user suppression — `hermes auth remove <provider> <N>` for an
           # env-seeded credential marks the env:<VAR> source as suppressed so it
           # won't be re-seeded from the user's shell environment or ~/.hermes/.env.
      @@ -1391,8 +1402,8 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool
               def _is_source_suppressed(_p, _s):  # type: ignore[misc]
                   return False
           if provider == "openrouter":
      -        # Check both os.environ and ~/.hermes/.env file
      -        token = [redacted:secret-assignment]"OPENROUTER_API_KEY") or "").strip()
      +        # Prefer ~/.hermes/.env over os.environ
      +        token = _get_env_prefer_dotenv("OPENROUTER_API_KEY")
               if token:
                   source = "env:OPENROUTER_API_KEY"
                   if _is_source_suppressed(provider, source):
      @@ -1418,7 +1429,7 @@ def _is_source_suppressed(_p, _s):  # type: ignore[misc]
       
           env_url = ""
           if pconfig.base_url_env_var:
      -        env_url = (get_env_value(pconfig.base_url_env_var) or "").strip().rstrip("/")
      +        env_url = _get_env_prefer_dotenv(pconfig.base_url_env_var).rstrip("/")
       
           env_vars = list(pconfig.api_key_env_vars)
           if provider == "anthropic":
      @@ -1429,8 +1440,8 @@ def _is_source_suppressed(_p, _s):  # type: ignore[misc]
               ]
       
           for env_var in env_vars:
      -        # Check both os.environ and ~/.hermes/.env file
      -        token = [redacted:secret-assignment] or "").strip()
      +        # Prefer ~/.hermes/.env over os.environ
      +        token = [redacted:secret-assignment]
               if not token:
                   continue
               source = f"env:{env_var}"
    • scripts/release.py
      @@ -371,6 +371,7 @@
           "[redacted:email]": "zons-zhaozhy",
           "[redacted:email]": "Es1la",
           "[redacted:email]": "CoreyNoDream",
      +    "[redacted:email]": "franksong2702",
           # ── bulk addition: 75 emails resolved via API, PR salvage bodies, noreply
           #    crossref, and GH contributor list matching (April 2026 audit) ──
           "[redacted:email]": "aaronagent",
    • tests/agent/test_credential_pool.py
      @@ -348,6 +348,64 @@ def test_load_pool_seeds_env_api_key(tmp_path, monkeypatch):
           assert entry.access_token == "sk-or-seeded"
       
       
      +
      +def test_load_pool_prefers_dotenv_over_stale_os_environ(tmp_path, monkeypatch):
      +    """Regression for #18254: stale OPENROUTER_API_KEY in os.environ (inherited
      +    from a parent shell) must NOT shadow the fresh key in ~/.hermes/.env when
      +    seeding the credential pool. Before the fix, `get_env_value()` preferred
      +    os.environ and silently wrote the stale value into auth.json, causing
      +    persistent 401 errors after key rotation.
      +    """
      +    hermes_home = tmp_path / "hermes"
      +    hermes_home.mkdir()
      +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
      +
      +    # Simulate the bug: parent shell exported a stale test key
      +    monkeypatch.setenv("OPENROUTER_API_KEY", "[redacted:openai-key]")
      +
      +    # User edited ~/.hermes/.env with the fresh key
      +    (hermes_home / ".env").write_text(
      +        "OPENROUTER_API_KEY=[redacted:openai-key]\n"
      +    )
      +
      +    _write_auth_store(tmp_path, {"version": 1, "providers": {}})
      +
      +    from agent.credential_pool import load_pool
      +    pool = load_pool("openrouter")
      +    entry = pool.select()
      +
      +    assert entry is not None
      +    assert entry.source == "env:OPENROUTER_API_KEY"
      +    # The fresh key from .env must win over the stale shell export
      +    assert entry.access_token == "[redacted:openai-key]", (
      +        f"Expected .env to win, got {entry.access_token!r}"
      +    )
      +
      +
      +def test_load_pool_falls_back_to_os_environ_when_dotenv_empty(tmp_path, monkeypatch):
      +    """When ~/.hermes/.env does not define OPENROUTER_API_KEY (typical Docker /
      +    K8s / systemd deployment), seeding must still pick up the key from
      +    os.environ. Guards against regressions that would break production
      +    deployments relying on runtime-injected env vars.
      +    """
      +    hermes_home = tmp_path / "hermes"
      +    hermes_home.mkdir()
      +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
      +    monkeypatch.setenv("OPENROUTER_API_KEY", "[redacted:openai-key]")
      +
      +    # .env exists but does not define OPENROUTER_API_KEY
      +    (hermes_home / ".env").write_text("SOME_OTHER_VAR=unrelated\n")
      +
      +    _write_auth_store(tmp_path, {"version": 1, "providers": {}})
      +
      +    from agent.credential_pool import load_pool
      +    pool = load_pool("openrouter")
      +    entry = pool.select()
      +
      +    assert entry is not None
      +    assert entry.access_token == "[redacted:openai-key]"
      +
      +
       def test_load_pool_removes_stale_seeded_env_entry(tmp_path, monkeypatch):
           monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
           monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)

1 PR closed without merging

From the close comments: superseded by another PR 1

  • #18361 feat(auth): hermes auth sync — detect and fix credential drift across .env files and auth.json · superseded by another PR → #18755, #18254
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
hermes cli
Merged PRs
#18755
Closed issues
0
Attempts
1 PR closed without merging
Fix link checked
2 May 2026

For your agent

The same facts, typed.