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
- 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.
Next step: read #18755, then check whether your report is the one still open.
Read #18755Pick up a Lab taskThese 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
-
#18755 fix: prefer ~/.hermes/.env over stale os.environ when seeding credential pool (salvage #18256)
files:
agent/credential_pool.py,scripts/release.py,tests/agent/test_credential_pool.pyShow 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.00means 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 LabAt a glance
- Subsystem
- hermes cli
- Merged PRs
- #18755
- Closed issues
- 0
- Attempts
- 1 PR closed without merging
- Fix link checked
- 2 May 2026