# NousResearch/hermes-agent · Credential pool not checked across CLI auth flows

## Open now

- 4 issues, 3 PRs open on GitHub.
- #20675: hermes debug: credential check should include credential_pool OAuth tokens, not just env vars
- #42269: hermes auth add deepseek: credential stored to pool but provider resolution ignores it at runtime
- #50737: bug(auth/profile): OpenRouter pool state can override or leak into selected profile readiness
- #60106: hermes status reports OpenRouter not set when credential exists only in auth pool
- PR #42386: fix(auth): surface pool base_url and log errors in _resolve_api_key_p…
- PR #60135: fix: hermes status checks credential pool when env var is absent (#60106)
- PR #60401: fix(debug): hermes debug shows OAuth providers as 'not set' when authenticated (Fixes #20675)

A cluster of bugs and fixes around Hermes CLI tools (debug, doctor, model wizard, status, fallback chain) failing to consult the credential_pool or auth.json when checking for API keys, OAuth tokens, and provider configuration. Multiple commands incorrectly reported credentials as missing or providers as unconfigured when valid pooled credentials existed.

## TL;DR

- 37 member(s): 4 open issue(s), 7 closed issue(s), 3 open PR(s), 4 merged PR(s), 19 PR(s) closed without merging.
- A fix was merged to main in PR #16101. Verify it addresses your case.
- **Recommended:** read PR #16101 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-16101,pr-73509,pr-73883`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.05`: the share of member 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.

## Open related issues

- #20675: hermes debug: credential check should include credential_pool OAuth tokens, not just env vars
- #42269: hermes auth add deepseek: credential stored to pool but provider resolution ignores it at runtime
- #50737: bug(auth/profile): OpenRouter pool state can override or leak into selected profile readiness
- #60106: hermes status reports OpenRouter not set when credential exists only in auth pool

## Closed / duplicate issues

- #5807: fix: hermes doctor reports Nous Portal as 'not logged in' when credentials exist in pool
- #15914: \[Bug\]: Fallback chain collapses when API key unavailable in os.environ but present in credential_pool
- #15932: bug(auth): API keys in ~/.hermes/.env not resolved by credential_pool._seed_from_env
- #26428: I need to reshare this because I ran into an issue with the Hermes update. \[Bug\]: hermes doctor does not flag missing credentials for an active openrouter provider
- #26436: \[Bug\]: hermes doctor does not flag missing credentials for an active openrouter provider
- #65977: \[Bug\]: \`hermes model\` wizard says "not configured" for providers that already have a working credential in \`auth.json\`'s pool
- #68003: \[Bug\]: STT/TTS tools bypass credential pool — get_env_value() ignores auth store

## Prior & competing PRs

- **#5808: fix: check credential pool in get_nous_auth_status() fallback**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#5860`, `#5860`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#16101: fix(auth): resolve API keys from ~/.hermes/.env and credential_pool**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/credential_pool.py`, `hermes_cli/auth.py`, `tests/tools/test_credential_pool_env_fallback.py`
  - fix hunks:
  - `agent/credential_pool.py`

    ```diff
    @@ -14,6 +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
     import hermes_cli.auth as auth_mod
     from hermes_cli.auth import (
         CODEX_ACCESS_TOKEN_REFRESH_SKEW_SECONDS,
    ```

    ```diff
    @@ -1273,7 +1274,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":
    -        token = [redacted:secret-assignment]"OPENROUTER_API_KEY", "").strip()
    +        # Check both os.environ and ~/.hermes/.env file
    +        token = [redacted:secret-assignment]"OPENROUTER_API_KEY") or "").strip()
             if token:
                 source = "env:OPENROUTER_API_KEY"
                 if _is_source_suppressed(provider, source):
    ```

    ```diff
    @@ -1299,7 +1301,7 @@ def _is_source_suppressed(_p, _s):  # type: ignore[misc]
     
         env_url = ""
         if pconfig.base_url_env_var:
    -        env_url = os.getenv(pconfig.base_url_env_var, "").strip().rstrip("/")
    +        env_url = (get_env_value(pconfig.base_url_env_var) or "").strip().rstrip("/")
     
         env_vars = list(pconfig.api_key_env_vars)
         if provider == "anthropic":
    ```

    ```diff
    @@ -1310,7 +1312,8 @@ def _is_source_suppressed(_p, _s):  # type: ignore[misc]
             ]
     
         for env_var in env_vars:
    -        token = [redacted:secret-assignment] "").strip()
    +        # Check both os.environ and ~/.hermes/.env file
    +        token = [redacted:secret-assignment] or "").strip()
             if not token:
                 continue
             source = f"env:{env_var}"
    ```
  - `hermes_cli/auth.py`

    ```diff
    @@ -467,11 +467,27 @@ def _resolve_api_key_provider_secret(
                 pass
             return "", ""
     
    +    from hermes_cli.config import get_env_value
         for env_var in pconfig.api_key_env_vars:
    -        val = os.getenv(env_var, "").strip()
    +        # Check both os.environ and ~/.hermes/.env file
    +        val = (get_env_value(env_var) or "").strip()
             if has_usable_secret(val):
                 return val, env_var
     
    +    # Fallback: try credential pool (e.g. zai key stored via auth.json)
    +    try:
    +        from agent.credential_pool import load_pool
    +        pool = load_pool(provider_id)
    +        if pool and pool.has_credentials():
    +            entry = pool.peek()
    +            if entry:
    +                key = getattr(entry, "access_token", "") or getattr(entry, "runtime_api_key", "")
    +                key = str(key).strip()
    +                if has_usable_secret(key):
    +                    return key, f"credential_pool:[redacted:secret-assignment]"
    +    except Exception:
    +        pass
    +
         return "", ""
     
     
    ```
  - `tests/tools/test_credential_pool_env_fallback.py`

    ```diff
    @@ -0,0 +1,210 @@
    +"""Tests for credential_pool .env fallback and auth credential_pool lookup.
    +
    +Covers the fix from #15914 / PR #15920:
    +- _seed_from_env reads API keys from ~/.hermes/.env when not in os.environ
    +- _resolve_api_key_provider_secret falls back to credential_pool when env vars are empty
    +- env vars take priority over .env file (handled by get_env_value itself)
    +- env vars take priority over credential pool (fallback only kicks in when env is empty)
    +"""
    +
    +import os
    +from pathlib import Path
    +from unittest.mock import MagicMock, patch
    +
    +import pytest
    +
    +
    +def _make_pconfig(provider_id="deepseek", env_vars=None):
    +    """Create a minimal ProviderConfig for testing.
    +
    +    Default provider_id is 'deepseek' because it's a real api_key provider
    +    in PROVIDER_REGISTRY (needed for _seed_from_env's generic path).
    +    """
    +    from hermes_cli.auth import ProviderConfig
    +    return ProviderConfig(
    +        id=provider_id,
    +        name=provider_id.title(),
    +        auth_type="api_key",
    +        api_key_env_vars=[redacted:secret-assignment] or [f"{provider_id.upper()}_API_KEY"]),
    +    )
    +
    +
    [redacted:email]
    +def isolated_hermes_home(tmp_path, monkeypatch):
    +    """Point HERMES_HOME at a temp dir and clear known API key env vars.
    +
    +    Also invalidates any cached get_env_value state by patching Path.home().
    +    """
    +    home = tmp_path / ".hermes"
    +    home.mkdir()
    +    monkeypatch.setattr(Path, "home", lambda: tmp_path)
    +    monkeypatch.setenv("HERMES_HOME", str(home))
    +
    +    # Clear all known API key env vars so get_env_value falls through to .env
    +    for key in [
    +        "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENROUTER_API_KEY",
    +        "ZAI_API_KEY", "DEEPSEEK_API_KEY", "ANTHROPIC_TOKEN",
    +        "CLAUDE_CODE_OAUTH_TOKEN", "OPENAI_BASE_URL",
    +    ]:
    +        monkeypatch.delenv(key, raising=False)
    +
    +    return home
    +
    +
    +def _write_env_file(home: Path, **kwargs) -> None:
    +    """Write key=value pairs to ~/.hermes/.env."""
    +    lines = [f"{k}={v}" for k, v in kwargs.items()]
    +    (home / ".env").write_text("\n".join(lines) + "\n")
    +
    +
    +class TestCredentialPoolSeedsFromDotEnv:
    +    """_seed_from_env must read keys from ~/.hermes/.env, not just os.environ.
    +
    +    This is the load-bearing behaviour for the fix: when a user adds a key to
    +    .env mid-session or via a non-CLI entry point that doesn't run
    +    load_hermes_dotenv, the credential pool must still discover it.
    +    """
    +
    +    def test_deepseek_key_from_dotenv_only(self, isolated_hermes_home):
    +        """Key in .env but not os.environ → _seed_from_env adds a pool entry."""
    +        _write_env_file(isolated_hermes_home, DEEPSEEK_API_KEY="[redacted:secret-assignment]")
    +        assert "DEEPSEEK_API_KEY" not in os.environ
    +
    +        from agent.credential_pool import _seed_from_env
    +        entries = []
    +        changed, active_sources = _seed_from_env("deepseek", entries)
    +
    +        assert changed is True
    +        assert "env:DEEPSEEK_API_KEY" in active_sources
    +        assert any(
    +            e.access_token == "sk-dotenv-only-12345"
    +            and e.source == "env:DEEPSEEK_API_KEY"
    +            for e in entries
    +        ), f"Expected seeded entry with dotenv key, got: {[(e.source, e.access_token) for e in entries]}"
    +
    +    def test_openrouter_key_from_dotenv_only(self, isolated_hermes_home):
    +        """OpenRouter path has its own branch — verify it also reads .env."""
    +        _write_env_file(isolated_hermes_home, OPENROUTER_API_KEY="[redacted:secret-assignment]")
    +        assert "OPENROUTER_API_KEY" not in os.environ
    +
    +        from agent.credential_pool import _seed_from_env
    +        entries = []
    +        changed, active_sources = _seed_from_env("openrouter", entries)
    +
    +        assert changed is True
    +        assert "env:OPENROUTER_API_KEY" in active_sources
    +        assert any(
    +            e.access_token == "sk-or-dotenv-abc" for e in entries
    +        )
    +
    +    def test_empty_dotenv_no_entries(self, isolated_hermes_home):
    +        """No .env file, no env vars → no entries seeded (and no crash)."""
    +        from agent.credential_pool import _seed_from_env
    +        entries = []
    +        changed, active_sources = _seed_from_env("deepseek", entries)
    +        assert changed is False
    +        assert active_sources == set()
    +        assert entries == []
    +
    +    def test_os_environ_still_wins_over_dotenv(self, isolated_hermes_home, monkeypatch):
    +        """get_env_value checks os.environ first — verify seeding picks that up."""
    +        _write_env_file(isolated_hermes_home, DEEPSEEK_API_KEY="[redacted:secret-assignment]")
    +        monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-env-fresh-xyz")
    +
    +        from agent.credential_pool import _seed_from_env
    +        entries = []
    +        changed, _ = _seed_from_env("deepseek", entries)
    +
    +        assert changed is True
    +        seeded = [e for e in entries if e.source == "env:DEEPSEEK_API_KEY"]
    +        assert len(seeded) == 1
    +        assert seeded[0].access_token == "sk-env-fresh-xyz"
    +
    +
    +class TestAuthResolvesFromDotEnv:
    +    """_resolve_api_key_provider_secret must also read from ~/.hermes/.env."""
    +
    +    def test_key_from_dotenv_only(self, isolated_hermes_home):
    +        """Key in .env but not os.environ → _resolve returns it with the env var source."""
    +        _write_env_file(isolated_hermes_home, DEEPSEEK_API_KEY="[redacted:secret-assignment]")
    +        assert "DEEPSEEK_API_KEY" not in os.environ
    +
    +        from hermes_cli.auth import _resolve_api_key_provider_secret
    +        key, source = _resolve_api_key_provider_secret(
    +            provider_id="deepseek",
    +            pconfig=_make_pconfig(),
    +        )
    +        assert key == "[redacted:openai-key]"
    +        assert source == "DEEPSEEK_API_KEY"
    +
    +
    +class TestAuthCredentialPoolFallback:
    +    """_resolve_api_key_provider_secret falls back to credential pool when env + dotenv are empty."""
    +
    +    def test_credential_pool_fallback_structure(self, isolated_hermes_home):
    +        """Empty env + empty .env → auth falls back to credential pool."""
    +        mock_entry = MagicMock()
    +        mock_entry.access_token = "[redacted:secret-assignment]"
    +        mock_entry.runtime_api_key = ""
    +
    +        mock_pool = MagicMock()
    +        mock_pool.has_credentials.return_value = True
    +        mock_pool.peek.return_value = mock_entry
    +
    +        from hermes_cli.auth import _resolve_api_key_provider_secret
    +        with patch("agent.credential_pool.load_pool", return_value=mock_pool):
    +            key, source = _resolve_api_key_provider_secret(
    +                provider_id="deepseek",
    +                pconfig=_make_pconfig(),
    +            )
    +        assert "test-pool-key-12345" in key
    +        assert "credential_pool" in source
    +
    +    def test_credential_pool_empty_returns_empty(self, isolated_hermes_home):
    +        """Empty env + empty .env + empty pool → empty string."""
    +        mock_pool = MagicMock()
    +        mock_pool.has_credentials.return_value = False
    +
    +        from hermes_cli.auth import _resolve_api_key_provider_secret
    +        with patch("agent.credential_pool.load_pool", return_value=mock_pool):
    +            key, source = _resolve_api_key_provider_secret(
    +                provider_id="deepseek",
    +                pconfig=_make_pconfig(),
    +            )
    +        assert key == ""
    +
    +    def test_env_var_takes_priority_over_pool(self, isolated_hermes_home, monkeypatch):
    +        """os.environ key wins — credential pool is NEVER consulted."""
    +        monkeypatch.setenv("DEEPSEEK_API_KEY", "[redacted:openai-key]")
    +
    +        mock_pool = MagicMock()
    +        mock_pool.has_credentials.return_value = True
    +
    +        from hermes_cli.auth import _resolve_api_key_provider_secret
    +        with patch("agent.credential_pool.load_pool", return_value=mock_pool) as mp:
    +            key, source = _resolve_api_key_provider_secret(
    +                provider_id="deepseek",
    +                pconfig=_make_pconfig(),
    +            )
    +        assert key == "[redacted:openai-key]"
    +        assert source == "DEEPSEEK_API_KEY"
    +        # Pool should not even have been loaded — env var satisfied the request first
    +        mp.assert_not_called()
    +
    +    def test_dotenv_takes_priority_over_pool(self, isolated_hermes_home):
    +        """Key in .env beats credential pool — pool only fires when both env sources are empty."""
    +        _write_env_file(isolated_hermes_home, DEEPSEEK_API_KEY="[redacted:secret-assignment]")
    +        assert "DEEPSEEK_API_KEY" not in os.environ
    +
    +        mock_pool = MagicMock()
    +        mock_pool.has_credentials.return_value = True
    +
    +        from hermes_cli.auth import _resolve_api_key_provider_secret
    +        with patch("agent.credential_pool.load_pool", return_value=mock_pool) as mp:
    +            key, source = _resolve_api_key_provider_secret(
    +                provider_id="deepseek",
    +                pconfig=_make_pconfig(),
    +            )
    +        assert key == "[redacted:openai-key]"
    +        assert source == "DEEPSEEK_API_KEY"
    +        mp.assert_not_called()
    ```
- **#27561: fix(doctor): flag missing credentials for active openrouter provider (salvage #26453)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `Dockerfile`, `hermes_cli/doctor.py`, `scripts/release.py`, `tests/hermes_cli/test_doctor.py`, `tests/tools/test_dockerfile_pid1_reaping.py`
  - fix hunks:
  - `Dockerfile`

    ```diff
    @@ -66,9 +66,11 @@ RUN npm install --prefer-offline --no-audit && \
     # frontend stats the readme path during dep resolution, so we `touch` an
     # empty placeholder — the real README is restored by `COPY . .` below.
     #
    -# `uv sync --frozen --no-install-project --extra all` installs only the
    -# deps reachable through the composite `[all]` extra (handpicked set
    -# intended for the production image).  We do NOT use `--all-extras`:
    +# `uv sync --frozen --no-install-project --extra all --extra messaging`
    +# installs the deps reachable through the composite `[all]` extra
    +# (handpicked set intended for the production image), plus gateway
    +# messaging adapters that should work in the published image without a
    +# first-boot lazy install.  We do NOT use `--all-extras`:
     # that would pull in `[rl]` (atroposlib + tinker + torch + wandb from
     # git), `[yc-bench]` (another git dep), and `[termux-all]` (Android
     # redundancy), none of which belong in the published container.
    ```

    ```diff
    @@ -76,7 +78,7 @@ RUN npm install --prefer-offline --no-audit && \
     # The editable link is created after the source copy below.
     COPY pyproject.toml uv.lock ./
     RUN touch ./README.md
    -RUN uv sync --frozen --no-install-project --extra all
    +RUN uv sync --frozen --no-install-project --extra all --extra messaging
     
     # ---------- Source code ----------
     # .dockerignore excludes node_modules, so the installs above survive.
    ```

    ```diff
    @@ -94,10 +96,10 @@ RUN cd web && npm run build && \
     # hermes_cli/main.py succeeds (see #18800). /opt/hermes/web is build-time
     # only (HERMES_WEB_DIST points at hermes_cli/web_dist) and is intentionally
     # not chowned here.
    -# The .venv MUST be hermes-writable so lazy_deps.py can install platform
    -# packages (discord.py, telegram, slack, etc.) at first gateway boot.
    -# Without this, `uv pip install` fails with EACCES and all messaging
    -# adapters silently fail to load.  See tools/lazy_deps.py.
    +# The .venv MUST remain hermes-writable so lazy_deps.py can install
    +# remaining optional platform packages and future pin bumps at first use.
    +# Without this, `uv pip install` fails with EACCES and adapters silently
    +# fail to load.  See tools/lazy_deps.py.
     USER root
     RUN chmod -R a+rX /opt/hermes && \
         chown -R hermes:hermes /opt/hermes/.venv /opt/hermes/ui-tui /opt/hermes/node_modules
    ```
  - `hermes_cli/doctor.py`

    ```diff
    @@ -651,31 +651,41 @@ def run_doctor(args):
     
                 # Check credentials for the configured provider.
                 # Limit to API-key providers in PROVIDER_REGISTRY — other provider
    -            # types (OAuth, SDK, openrouter/anthropic/custom/auto) have their
    -            # own env-var checks elsewhere in doctor, and get_auth_status()
    -            # returns a bare {logged_in: False} for anything it doesn't
    -            # explicitly dispatch, which would produce false positives.
    -            if runtime_provider and runtime_provider not in {"auto", "custom", "openrouter"}:
    +            # types (OAuth, SDK, anthropic/custom/auto) have their own env-var
    +            # checks elsewhere in doctor, and get_auth_status() returns a bare
    +            # {logged_in: False} for anything it doesn't explicitly dispatch,
    +            # which would produce false positives.
    +            if runtime_provider and runtime_provider not in ("auto", "custom"):
                     try:
    -                    from hermes_cli.auth import PROVIDER_REGISTRY, get_auth_status
    -                    pconfig = PROVIDER_REGISTRY.get(runtime_provider)
    -                    if pconfig and getattr(pconfig, "auth_type", "") == "api_key":
    -                        status = get_auth_status(runtime_provider) or {}
    +                    if runtime_provider == "openrouter":
    +                        from hermes_cli.config import get_env_value
    +
                             configured = bool(
    -                            status.get("configured")
    -                            or status.get("logged_in")
    -                            or status.get("api_key")
    +                            str(get_env_value("OPENROUTER_API_KEY") or "").strip()
    +                            or str(get_env_value("OPENAI_API_KEY") or "").strip()
                             )
    -                        if not configured:
    -                            check_fail(
    -                                f"model.provider '{runtime_provider}' is set but no API key is configured",
    -                                "(check ~/.hermes/.env or run 'hermes setup')",
    -                            )
    -                            issues.append(
    -                                f"No credentials found for provider '{runtime_provider}'. "
    -                                f"Run 'hermes setup' or set the provider's API key in {_DHH}/.env, "
    -                                f"or switch providers with 'hermes config set model.provider <name>'"
    +                    else:
    +                        from hermes_cli.auth import PROVIDER_REGISTRY, get_auth_status
    +
    +                        pconfig = PROVIDER_REGISTRY.get(runtime_provider)
    +                        configured = True
    +                        if pconfig and getattr(pconfig, "auth_type", "") == "api_key":
    +                            status = get_auth_status(runtime_provider) or {}
    +                            configured = bool(
    +                                status.get("configured")
    +                                or status.get("logged_in")
    +                                or status.get("api_key")
                                 )
    +                    if not configured:
    +                        check_fail(
    +                            f"model.provider '{runtime_provider}' is set but no API key is configured",
    +                            "(check ~/.hermes/.env or run 'hermes setup')",
    +                        )
    +                        issues.append(
    +                            f"No credentials found for provider '{runtime_provider}'. "
    +                            f"Run 'hermes setup' or set the provider's API key in {_DHH}/.env, "
    +                            f"or switch providers with 'hermes config set model.provider <name>'"
    +                        )
                     except Exception:
                         pass
     
    ```
  - `scripts/release.py`

    ```diff
    @@ -1161,6 +1161,7 @@
         "[redacted:email]": "davidcampbelldc",
         "[redacted:email]": "hehehe0803",  # PR #26212 salvage (codex kanban writable root)
         "[redacted:email]": "hehehe0803",
    +    "[redacted:email]": "vaddisrinivas",  # PR #26394 salvage (Docker messaging extra)
     }
     
     
    ```
  - `tests/hermes_cli/test_doctor.py`

    ```diff
    @@ -477,6 +477,48 @@ def test_run_doctor_accepts_bare_custom_provider(monkeypatch, tmp_path):
         assert "model.provider 'custom' is not a recognised provider" not in out
     
     
    +def test_run_doctor_flags_missing_credentials_for_active_openrouter_provider(monkeypatch, tmp_path):
    +    home = tmp_path / ".hermes"
    +    home.mkdir(parents=True, exist_ok=True)
    +    (home / "config.yaml").write_text(
    +        "model:\n"
    +        "  provider: openrouter\n"
    +        "  default: openai/gpt-4.1-mini\n",
    +        encoding="utf-8",
    +    )
    +
    +    monkeypatch.setattr(doctor_mod, "HERMES_HOME", home)
    +    monkeypatch.setattr(doctor_mod, "PROJECT_ROOT", tmp_path / "project")
    +    monkeypatch.setattr(doctor_mod, "_DHH", str(home))
    +    (tmp_path / "project").mkdir(exist_ok=True)
    +
    +    fake_model_tools = types.SimpleNamespace(
    +        check_tool_availability=lambda *a, **kw: ([], []),
    +        TOOLSET_REQUIREMENTS={},
    +    )
    +    monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools)
    +    monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
    +    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
    +
    +    try:
    +        from hermes_cli import auth as _auth_mod
    +
    +        monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {})
    +        monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
    +        monkeypatch.setattr(_auth_mod, "get_gemini_oauth_auth_status", lambda: {})
    +        monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: {})
    +    except Exception:
    +        pass
    +
    +    buf = io.StringIO()
    +    with contextlib.redirect_stdout(buf):
    +        doctor_mod.run_doctor(Namespace(fix=False))
    +
    +    out = buf.getvalue()
    +    assert "model.provider 'openrouter' is set but no API key is configured" in out
    +    assert "No credentials found for provider 'openrouter'." in out
    +
    +
     @pytest.mark.parametrize(
         ("provider", "default_model"),
         [
    ```
  - `tests/tools/test_dockerfile_pid1_reaping.py`

    ```diff
    @@ -121,6 +121,20 @@ def test_dockerfile_installs_tui_dependencies(dockerfile_text):
         )
     
     
    +def test_dockerfile_preinstalls_gateway_messaging_dependencies(dockerfile_text):
    +    sync_steps = [
    +        step for step in _run_steps(dockerfile_text)
    +        if "uv sync" in step and "--no-install-project" in step
    +    ]
    +
    +    assert sync_steps, "Dockerfile must install Python dependencies with uv sync"
    +    assert any("--extra messaging" in step for step in sync_steps), (
    +        "Published Docker images must preload the [messaging] extra so "
    +        "Telegram/Discord gateway adapters do not depend on first-boot "
    +        "lazy installation (#24698)."
    +    )
    +
    +
     def test_dockerfile_builds_tui_assets(dockerfile_text):
         assert any(
             "ui-tui" in step and "npm" in step and "run build" in step
    ```
- **#68011: fix(tools): resolve TTS/STT provider keys through credential pool (fixes #68003)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#68509`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#73509: fix(tools): STT/TTS provider keys resolve through the credential pool via one scoped resolver**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `tests/tools/test_tool_backend_helpers.py`, `tests/tools/test_voice_credential_pool_resolution.py`, `tools/tool_backend_helpers.py`, `tools/transcription_tools.py`, `tools/tts_tool.py`, `tools/xai_http.py`
- **#73883: fix(cli): honor pooled credentials in model wizard (#65977)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `contributors/emails/[redacted:email]`, `hermes_cli/main.py`, `hermes_cli/model_setup_flows.py`, `tests/hermes_cli/test_model_flow_pooled_credentials.py`, `tests/hermes_cli/test_prompt_api_key.py`
- **#5860: fix(auth): check credential pool in get_nous_auth_status**. closed without merge
  - maintainer direction: category `volume_quota`; implication `active`
- **#15920: fix(auth): resolve API keys from ~/.hermes/.env and credential_pool**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#16101`
- **#15944: fix(auth): add credential_pool fallback when env var is absent**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#16101`, `#15920`, `#15914`
- **#20698: fix(cli): hermes debug now sees credential_pool OAuth tokens**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`
- **#20761: fix(cli): hermes debug reports OAuth providers as 'not set' when logged in**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#20698`
- **#26448: fix(doctor): flag missing OpenRouter credentials**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
- **#26453: Fix doctor summary for active OpenRouter without credentials**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `73df32921`, `#27561`, `#26436`
- **#26776: fix(doctor): flag missing OpenRouter credentials**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `73df329214a89eddbd45b0fa84ee99aefa8aea30`, `#26448`, `#26453`, `#26436`
- **#42386: fix(auth): surface pool base_url and log errors in _resolve_api_key_p…**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `hermes_cli/auth.py`, `tests/hermes_cli/test_deepseek_auth_pool_resolution.py`
- **#42425: fix(auth): honour pool entry base_url in provider credential resolution (#42269)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`
- **#60135: fix: hermes status checks credential pool when env var is absent (#60106)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#60214`
  - files: `hermes_cli/status.py`
- **#60214: fix(cli): check credential pool for OpenRouter in hermes status (#60106)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#60135`, `#60106`, `#60135`
- **#60392: feat(debug): check credential_pool OAuth tokens in API key status**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`
- **#60401: fix(debug): hermes debug shows OAuth providers as 'not set' when authenticated (Fixes #20675)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `hermes_cli/config.py`, `tests/hermes_cli/test_config_show_oauth.py`
- **#65988: fix(cli): honor pooled credentials in model wizard 🩷**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#73883`
- **#66007: fix(cli): model wizard recognises auth.json credential-pool keys (#65977)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#65988`
- **#66041: fix: hermes model wizard recognises keys from credential pool (#65977)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#73883`
- **#68509: fix(tools): resolve TTS/STT provider keys through credential pool**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `c136400c9e`, `8c7ba05437`, `#73509`
- **#68518: fix(config): check credential pool in get_env_value (#68003)**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#68509`, `#69469`, `#68509`, `#69469`, `#68003`
- **#108333: fix(cli): count credential-pool entries as a configured API key in doctor, status and config show**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#60135`, `#60106`, `#62467`

## Fix status

- Merged to main in PR #16101 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-29`.
- Subsystem: `agent`
