NousResearch/hermes-agent

OpenAI Codex auth state cleanup on logout

Cluster 15013. Data as of

Nothing open on GitHub. Fix merged in #15061 on 24 Apr 2026. Check it covers your case.

The short answer

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

These items concern a bug where logging out of OpenAI Codex can leave stale provider state pinned, and related fixes to clear that state on logout.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

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

  • #15061 fix(auth): clear stale OpenAI Codex provider state on logout (salvage #15018)

    merged to main

    files: hermes_cli/auth.py, scripts/release.py, tests/hermes_cli/test_auth_commands.py

    Show the diff
    • hermes_cli/auth.py
      @@ -955,10 +955,12 @@ def clear_provider_auth(provider_id: Optional[str] = None) -> bool:
                   del pool[target]
                   cleared = True
       
      -        if not cleared:
      -            return False
               if auth_store.get("active_provider") == target:
                   auth_store["active_provider"] = None
      +            cleared = True
      +
      +        if not cleared:
      +            return False
               _save_auth_store(auth_store)
           return True
       
      @@ -2826,6 +2828,46 @@ def _update_config_for_provider(
           return config_path
       
       
      +def _get_config_provider() -> Optional[str]:
      +    """Return model.provider from config.yaml, normalized, if present."""
      +    try:
      +        config = read_raw_config()
      +    except Exception:
      +        return None
      +    if not config:
      +        return None
      +    model = config.get("model")
      +    if not isinstance(model, dict):
      +        return None
      +    provider = model.get("provider")
      +    if not isinstance(provider, str):
      +        return None
      +    provider = provider.strip().lower()
      +    return provider or None
      +
      +
      +def _config_provider_matches(provider_id: Optional[str]) -> bool:
      +    """Return True when config.yaml currently selects *provider_id*."""
      +    if not provider_id:
      +        return False
      +    return _get_config_provider() == provider_id.strip().lower()
      +
      +
      +def _logout_default_provider_from_config() -> Optional[str]:
      +    """Fallback logout target when auth.json has no active provider.
      +
      +    `hermes logout` historically keyed off auth.json.active_provider only.
      +    That left users stuck when auth state had already been cleared but
      +    config.yaml still selected an OAuth provider such as openai-codex for the
      +    agent model: there was no active auth provider to target, so logout printed
      +    "No provider is currently logged in" and never reset model.provider.
      +    """
      +    provider = _get_config_provider()
      +    if provider in {"nous", "openai-codex"}:
      +        return provider
      +    return None
      +
      +
       def _reset_config_provider() -> Path:
           """Reset config.yaml provider back to auto after logout."""
           config_path = get_config_path()
      @@ -3524,15 +3566,16 @@ def logout_command(args) -> None:
               raise SystemExit(1)
       
           active = get_active_provider()
      -    target = provider_id or active
      +    target = provider_id or active or _logout_default_provider_from_config()
       
           if not target:
               print("No provider is currently logged in.")
               return
       
           provider_name = PROVIDER_REGISTRY[target].name if target in PROVIDER_REGISTRY else target
      +    config_matches = _config_provider_matches(target)
       
      -    if clear_provider_auth(target):
      +    if clear_provider_auth(target) or config_matches:
               _reset_config_provider()
               print(f"Logged out of {provider_name}.")
               if os.getenv("OPENROUTER_API_KEY"):
    • scripts/release.py
      @@ -189,6 +189,7 @@
           "[redacted:email]": "christomitov",
           "[redacted:email]": "NousResearch",
           "[redacted:email]": "ReginaldasR",
      +    "[redacted:email]": "0xharryriddle",
           "[redacted:email]": "benbarclay",
           "[redacted:email]": "ChimingLiu",
           "[redacted:email]": "openclaw",
    • tests/hermes_cli/test_auth_commands.py
      @@ -504,6 +504,91 @@ def test_clear_provider_auth_removes_provider_pool_entries(tmp_path, monkeypatch
           assert "openrouter" in payload.get("credential_pool", {})
       
       
      +def test_logout_resets_codex_config_when_auth_state_already_cleared(tmp_path, monkeypatch, capsys):
      +    """`hermes logout --provider openai-codex` must still clear model.provider.
      +
      +    Users can end up with auth.json already cleared but config.yaml still set to
      +    openai-codex.  Previously logout reported no auth state and left the agent
      +    pinned to the Codex provider.
      +    """
      +    hermes_home = tmp_path / "hermes"
      +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
      +    _write_auth_store(tmp_path, {"version": 1, "providers": {}, "credential_pool": {}})
      +    (hermes_home / "config.yaml").write_text(
      +        "model:\n"
      +        "  default: gpt-5.3-codex\n"
      +        "  provider: openai-codex\n"
      +        "  base_url: https://chatgpt.com/backend-api/codex\n"
      +    )
      +
      +    from types import SimpleNamespace
      +    from hermes_cli.auth import logout_command
      +
      +    logout_command(SimpleNamespace(provider="openai-codex"))
      +
      +    out = capsys.readouterr().out
      +    assert "Logged out of OpenAI Codex." in out
      +    config_text = (hermes_home / "config.yaml").read_text()
      +    assert "provider: auto" in config_text
      +    assert "base_url: https://openrouter.ai/api/v1" in config_text
      +
      +
      +def test_logout_defaults_to_configured_codex_when_no_active_provider(tmp_path, monkeypatch, capsys):
      +    """Bare `hermes logout` should target configured Codex if auth has no active provider."""
      +    hermes_home = tmp_path / "hermes"
      +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
      +    _write_auth_store(tmp_path, {"version": 1, "providers": {}, "credential_pool": {}})
      +    (hermes_home / "config.yaml").write_text(
      +        "model:\n"
      +        "  default: gpt-5.3-codex\n"
      +        "  provider: openai-codex\n"
      +        "  base_url: https://chatgpt.com/backend-api/codex\n"
      +    )
      +
      +    from types import SimpleNamespace
      +    from hermes_cli.auth import logout_command
      +
      +    logout_command(SimpleNamespace(provider=None))
      +
      +    out = capsys.readouterr().out
      +    assert "Logged out of OpenAI Codex." in out
      +    config_text = (hermes_home / "config.yaml").read_text()
      +    assert "provider: auto" in config_text
      +
      +
      +def test_logout_clears_stale_active_codex_without_provider_credentials(tmp_path, monkeypatch, capsys):
      +    """Logout must clear active_provider even when provider credential payloads are gone."""
      +    hermes_home = tmp_path / "hermes"
      +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
      +    _write_auth_store(
      +        tmp_path,
      +        {
      +            "version": 1,
      +            "active_provider": "openai-codex",
      +            "providers": {},
      +            "credential_pool": {},
      +        },
      +    )
      +    (hermes_home / "config.yaml").write_text(
      +        "model:\n"
      +        "  default: gpt-5.3-codex\n"
      +        "  provider: openai-codex\n"
      +        "  base_url: https://chatgpt.com/backend-api/codex\n"
      +    )
      +
      +    from types import SimpleNamespace
      +    from hermes_cli.auth import logout_command
      +
      +    logout_command(SimpleNamespace(provider=None))
      +
      +    out = capsys.readouterr().out
      +    assert "Logged out of OpenAI Codex." in out
      +    auth_payload = json.loads((hermes_home / "auth.json").read_text())
      +    assert auth_payload.get("active_provider") is None
      +    config_text = (hermes_home / "config.yaml").read_text()
      +    assert "provider: auto" in config_text
      +
      +
       def test_auth_list_does_not_call_mutating_select(monkeypatch, capsys):
           from hermes_cli.auth_commands import auth_list_command
       

1 PR closed without merging

From the close comments: merged via another change 1

  • #15018 fix(auth): clear stale OpenAI Codex provider state on logout · merged via another change → #15061
Closed issues · 1
  • Closed#15013[Bug]: OpenAI Codex logout can leave Hermes pinned to openai-codex when auth state is already cleared
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.67: 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

Merged PRs
#15061
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
24 Apr 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.