# NousResearch/hermes-agent · Codex OAuth pool entries overwritten on add/re-auth

## Open now

- Nothing open on GitHub.

These issues and fixes concern a bug where adding or re-authenticating Codex OAuth credentials overwrites existing credential pool entries instead of keeping them distinct. The work focuses on preserving independent pool entries and their labels during sync and re-auth operations.

## TL;DR

- 12 member(s): 0 open issue(s), 3 closed issue(s), 0 open PR(s), 2 merged PR(s), 7 PR(s) closed without merging.
- A fix was merged to main in PR #12360. Verify it addresses your case.
- **Recommended:** read PR #12360 (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-42316`. 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.15`: 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.

## Closed / duplicate issues

- #11364: openai-codex credential pool: _sync_codex_entry_from_cli overwrites all entries with the same token
- #39236: \[Bug\]: openai-codex OAuth add/reauth overwrites existing credential-pool entries while preserving stale labels
- #42102: \[Bug\]: Openai-codex auth add only adds openai-codex-oauth-1 unable to add multiple

## Prior & competing PRs

- **#12360: fix(codex): Hermes owns its own Codex auth; stop touching ~/.codex/auth.json**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/credential_pool.py`, `hermes_cli/auth.py`, `tests/hermes_cli/test_auth_codex_provider.py`
  - fix hunks:
  - `agent/credential_pool.py`

    ```diff
    @@ -22,8 +22,6 @@
         _auth_store_lock,
         _codex_access_token_is_expiring,
         _decode_jwt_claims,
    -    _import_codex_cli_tokens,
    -    _write_codex_cli_tokens,
         _load_auth_store,
         _load_provider_state,
         _resolve_kimi_base_url,
    ```

    ```diff
    @@ -457,39 +455,6 @@ def _sync_anthropic_entry_from_credentials_file(self, entry: PooledCredential) -
                 logger.debug("Failed to sync from credentials file: %s", exc)
             return entry
     
    -    def _sync_codex_entry_from_cli(self, entry: PooledCredential) -> PooledCredential:
    -        """Sync an openai-codex pool entry from ~/.codex/auth.json if tokens differ.
    -
    -        OpenAI OAuth refresh tokens are single-use and rotate on every refresh.
    -        When the Codex CLI (or another Hermes profile) refreshes its token,
    -        the pool entry's refresh_token becomes stale.  This method detects that
    -        by comparing against ~/.codex/auth.json and syncing the fresh pair.
    -        """
    -        if self.provider != "openai-codex":
    -            return entry
    -        try:
    -            cli_tokens = [redacted:secret-assignment]
    -            if not cli_tokens:
    -                return entry
    -            cli_refresh = cli_tokens.get("refresh_token", "")
    -            cli_access = cli_tokens.get("access_token", "")
    -            if cli_refresh and cli_refresh != entry.refresh_token:
    -                logger.debug("Pool entry %s: syncing tokens from ~/.codex/auth.json (refresh token changed)", entry.id)
    -                updated = replace(
    -                    entry,
    -                    access_token=[redacted:secret-assignment]
    -                    refresh_token=[redacted:secret-assignment]
    -                    last_status=None,
    -                    last_status_at=None,
    -                    last_error_code=None,
    -                )
    -                self._replace_entry(entry, updated)
    -                self._persist()
    -                return updated
    -        except Exception as exc:
    -            logger.debug("Failed to sync from ~/.codex/auth.json: %s", exc)
    -        return entry
    -
         def _sync_device_code_entry_to_auth_store(self, entry: PooledCredential) -> None:
             """Write refreshed pool entry tokens back to auth.json providers.
     
    ```

    ```diff
    @@ -585,13 +550,6 @@ def _refresh_entry(self, entry: PooledCredential, *, force: bool) -> Optional[Po
                         except Exception as wexc:
                             logger.debug("Failed to write refreshed token to credentials file: %s", wexc)
                 elif self.provider == "openai-codex":
    -                # Proactively sync from ~/.codex/auth.json before refresh.
    -                # The Codex CLI (or another Hermes profile) may have already
    -                # consumed our refresh_token.  Syncing first avoids a
    -                # "refresh_token_reused" error when the CLI has a newer pair.
    -                synced = self._sync_codex_entry_from_cli(entry)
    -                if synced is not entry:
    -                    entry = synced
                     refreshed = auth_mod.refresh_codex_oauth_pure(
                         entry.access_token,
                         entry.refresh_token,
    ```

    ```diff
    @@ -677,45 +635,6 @@ def _refresh_entry(self, entry: PooledCredential, *, force: bool) -> Optional[Po
                         # Credentials file had a valid (non-expired) token — use it directly
                         logger.debug("Credentials file has valid token, using without refresh")
                         return synced
    -            # For openai-codex: the refresh_token may have been consumed by
    -            # the Codex CLI between our proactive sync and the refresh call.
    -            # Re-sync and retry once.
    -            if self.provider == "openai-codex":
    -                synced = self._sync_codex_entry_from_cli(entry)
    -                if synced.refresh_token != entry.refresh_token:
    -                    logger.debug("Retrying Codex refresh with synced token from ~/.codex/auth.json")
    -                    try:
    -                        refreshed = auth_mod.refresh_codex_oauth_pure(
    -                            synced.access_token,
    -                            synced.refresh_token,
    -                        )
    -                        updated = replace(
    -                            synced,
    -                            access_token=[redacted:secret-assignment]"access_token"],
    -                            refresh_token=[redacted:secret-assignment]"refresh_token"],
    -                            last_refresh=refreshed.get("last_refresh"),
    -                            last_status=STATUS_OK,
    -                            last_status_at=None,
    -                            last_error_code=None,
    -                        )
    -                        self._replace_entry(synced, updated)
    -                        self._persist()
    -                        self._sync_device_code_entry_to_auth_store(updated)
    -                        try:
    -                            _write_codex_cli_tokens(
    -                                updated.access_token,
    -                                updated.refresh_token,
    -                                last_refresh=updated.last_refresh,
    -                            )
    -                        except Exception as wexc:
    -                            logger.debug("Failed to write refreshed Codex tokens to CLI file (retry): %s", wexc)
    -                        return updated
    -                    except Exception as retry_exc:
    -                        logger.debug("Codex retry refresh also failed: %s", retry_exc)
    -                elif not self._entry_needs_refresh(synced):
    -                    logger.debug("Codex CLI has valid token, using without refresh")
    -                    self._sync_device_code_entry_to_auth_store(synced)
    -                    return synced
                 self._mark_exhausted(entry, None)
                 return None
     
    ```

    ```diff
    @@ -734,17 +653,6 @@ def _refresh_entry(self, entry: PooledCredential, *, force: bool) -> Optional[Po
             # _seed_from_singletons() on the next load_pool() sees fresh state
             # instead of re-seeding stale/consumed tokens.
             self._sync_device_code_entry_to_auth_store(updated)
    -        # Write refreshed tokens back to ~/.codex/auth.json so Codex CLI
    -        # and VS Code don't hit "refresh_token_reused" on their next refresh.
    -        if self.provider == "openai-codex":
    -            try:
    -                _write_codex_cli_tokens(
    -                    updated.access_token,
    -                    updated.refresh_token,
    -                    last_refresh=updated.last_refresh,
    -                )
    -            except Exception as wexc:
    -                logger.debug("Failed to write refreshed Codex tokens to CLI file: %s", wexc)
             return updated
     
         def _entry_needs_refresh(self, entry: PooledCredential) -> bool:
    ```

    ```diff
    @@ -790,16 +698,6 @@ def _available_entries(self, *, clear_expired: bool = False, refresh: bool = Fal
                     if synced is not entry:
                         entry = synced
                         cleared_any = True
    -            # For openai-codex entries, sync from ~/.codex/auth.json before
    -            # any status/refresh checks.  This picks up tokens refreshed by
    -            # the Codex CLI or another Hermes profile.
    -            if (self.provider == "openai-codex"
    -                    and entry.last_status == STATUS_EXHAUSTED
    -                    and entry.refresh_token):
    -                synced = self._sync_codex_entry_from_cli(entry)
    -                if synced is not entry:
    -                    entry = synced
    -                    cleared_any = True
                 if entry.last_status == STATUS_EXHAUSTED:
                     exhausted_until = _exhausted_until(entry)
                     if exhausted_until is not None and now < exhausted_until:
    ```

    ```diff
    @@ -1218,8 +1116,8 @@ def _seed_from_singletons(provider: str, entries: List[PooledCredential]) -> Tup
         elif provider == "openai-codex":
             # Respect user suppression — `hermes auth remove openai-codex` marks
             # the device_code source as suppressed so it won't be re-seeded from
    -        # either the Hermes auth store or ~/.codex/auth.json.  Without this
    -        # gate the removal is instantly undone on the next load_pool() call.
    +        # the Hermes auth store.  Without this gate the removal is instantly
    +        # undone on the next load_pool() call.
             codex_suppressed = False
             try:
                 from hermes_cli.auth import is_source_suppressed
    ```

    ```diff
    @@ -1231,23 +1129,12 @@ def _seed_from_singletons(provider: str, entries: List[PooledCredential]) -> Tup
     
             state = _load_provider_state(auth_store, "openai-codex")
             tokens = [redacted:secret-assignment]"tokens") if isinstance(state, dict) else None
    -        # Fallback: import from Codex CLI (~/.codex/auth.json) if Hermes auth
    -        # store has no tokens.  This mirrors resolve_codex_runtime_credentials()
    -        # so that load_pool() and list_authenticated_providers() detect tokens
    -        # that only exist in the Codex CLI shared file.
    -        if not (isinstance(tokens, dict) and tokens.get("access_token")):
    -            try:
    -                from hermes_cli.auth import _import_codex_cli_tokens, _save_codex_tokens
    -                cli_tokens = [redacted:secret-assignment]
    -                if cli_tokens:
    -                    logger.info("Importing Codex CLI tokens into Hermes auth store.")
    -                    _save_codex_tokens(cli_tokens)
    -                    # Re-read state after import
    -                    auth_store = _load_auth_store()
    -                    state = _load_provider_state(auth_store, "openai-codex")
    -                    tokens = [redacted:secret-assignment]"tokens") if isinstance(state, dict) else None
    -            except Exception as exc:
    -                logger.debug("Codex CLI token import failed: %s", exc)
    +        # Hermes owns its own Codex auth state — we do NOT auto-import from
    +        # ~/.codex/auth.json at pool-load time.  OAuth refresh tokens are
    +        # single-use, so sharing them with Codex CLI / VS Code causes
    +        # refresh_token_reused race failures.  Users who want to adopt
    +        # existing Codex CLI credentials get a one-time, explicit prompt
    +        # via `hermes auth openai-codex`.
             if isinstance(tokens, dict) and tokens.get("access_token"):
                 active_sources.add("device_code")
                 changed |= _upsert_entry(
    ```
  - `hermes_cli/auth.py`

    ```diff
    @@ -1434,49 +1434,6 @@ def _read_codex_tokens(*, _lock: bool = True) -> Dict[str, Any]:
         }
     
     
    -def _write_codex_cli_tokens(
    -    access_token: str,
    -    refresh_token: str,
    -    *,
    -    last_refresh: Optional[str] = None,
    -) -> None:
    -    """Write refreshed tokens back to ~/.codex/auth.json.
    -
    -    OpenAI OAuth refresh tokens are single-use and rotate on every refresh.
    -    When Hermes refreshes a token it consumes the old refresh_token; if we
    -    don't write the new pair back, the Codex CLI (or VS Code extension) will
    -    fail with ``refresh_token_reused`` on its next refresh attempt.
    -
    -    This mirrors the Anthropic write-back to ~/.claude/.credentials.json
    -    via ``_write_claude_code_credentials()``.
    -    """
    -    codex_home = os.getenv("CODEX_HOME", "").strip()
    -    if not codex_home:
    -        codex_home = str(Path.home() / ".codex")
    -    auth_path = Path(codex_home).expanduser() / "auth.json"
    -    try:
    -        existing: Dict[str, Any] = {}
    -        if auth_path.is_file():
    -            existing = json.loads(auth_path.read_text(encoding="utf-8"))
    -        if not isinstance(existing, dict):
    -            existing = {}
    -
    -        tokens_dict = [redacted:secret-assignment]"tokens")
    -        if not isinstance(tokens_dict, dict):
    -            tokens_dict = {}
    -        tokens_dict["access_token"] = access_token
    -        tokens_dict["refresh_token"] = refresh_token
    -        existing["tokens"] = tokens_dict
    -        if last_refresh is not None:
    -            existing["last_refresh"] = last_refresh
    -
    -        auth_path.parent.mkdir(parents=True, exist_ok=True)
    -        auth_path.write_text(json.dumps(existing, indent=2), encoding="utf-8")
    -        auth_path.chmod(0o600)
    -    except (OSError, IOError) as exc:
    -        logger.debug("Failed to write refreshed tokens to %s: %s", auth_path, exc)
    -
    -
     def _save_codex_tokens(tokens: [redacted:secret-assignment] str], last_refresh: str = None) -> None:
         """Save Codex OAuth tokens to Hermes auth store (~/.hermes/auth.json)."""
         if last_refresh is None:
    ```

    ```diff
    @@ -1604,12 +1561,6 @@ def _refresh_codex_auth_tokens(
         updated_tokens["refresh_token"] = refreshed["refresh_token"]
     
         _save_codex_tokens(updated_tokens)
    -    # Write back to ~/.codex/auth.json so Codex CLI / VS Code stay in sync.
    -    _write_codex_cli_tokens(
    -        refreshed["access_token"],
    -        refreshed["refresh_token"],
    -        last_refresh=refreshed.get("last_refresh"),
    -    )
         return updated_tokens
     
     
    ```

    ```diff
    @@ -1654,25 +1605,7 @@ def resolve_codex_runtime_credentials(
         refresh_skew_seconds: int = CODEX_ACCESS_TOKEN_REFRESH_SKEW_SECONDS,
     ) -> Dict[str, Any]:
         """Resolve runtime credentials from Hermes's own Codex token store."""
    -    try:
    -        data = _read_codex_tokens()
    -    except AuthError as orig_err:
    -        # Only attempt migration when there are NO tokens stored at all
    -        # (code == "codex_auth_missing"), not when tokens exist but are invalid.
    -        if orig_err.code != "codex_auth_missing":
    -            raise
    -
    -        # Migration: user had Codex as active provider with old storage (~/.codex/).
    -        cli_tokens = [redacted:secret-assignment]
    -        if cli_tokens:
    -            logger.info("Migrating Codex credentials from ~/.codex/ to Hermes auth store")
    -            print("⚠️  Migrating Codex credentials to Hermes's own auth store.")
    -            print("   This avoids conflicts with Codex CLI and VS Code.")
    -            print("   Run `hermes auth` to create a fully independent session.\n")
    -            _save_codex_tokens(cli_tokens)
    -            data = _read_codex_tokens()
    -        else:
    -            raise
    +    data = _read_codex_tokens()
         tokens = [redacted:secret-assignment]"tokens"])
         access_token = [redacted:secret-assignment]"access_token", "") or "").strip()
         refresh_timeout_seconds = float(os.getenv("HERMES_CODEX_REFRESH_TIMEOUT_SECONDS", "20"))
    ```
  - `tests/hermes_cli/test_auth_codex_provider.py`

    ```diff
    @@ -14,7 +14,6 @@
         PROVIDER_REGISTRY,
         _read_codex_tokens,
         _save_codex_tokens,
    -    _write_codex_cli_tokens,
         _import_codex_cli_tokens,
         get_codex_auth_status,
         get_provider_auth_state,
    ```

    ```diff
    @@ -182,98 +181,6 @@ def test_codex_tokens_not_written_to_shared_file(tmp_path, monkeypatch):
         assert data["tokens"]["access_token"] == "hermes-at"
     
     
    -def test_write_codex_cli_tokens_creates_file(tmp_path, monkeypatch):
    -    """_write_codex_cli_tokens creates ~/.codex/auth.json with refreshed tokens."""
    -    codex_home = tmp_path / "codex-cli"
    -    monkeypatch.setenv("CODEX_HOME", str(codex_home))
    -
    -    _write_codex_cli_tokens("new-access", "new-refresh", last_refresh="2026-04-12T00:00:00Z")
    -
    -    auth_path = codex_home / "auth.json"
    -    assert auth_path.exists()
    -    data = json.loads(auth_path.read_text())
    -    assert data["tokens"]["access_token"] == "new-access"
    -    assert data["tokens"]["refresh_token"] == "new-refresh"
    -    assert data["last_refresh"] == "2026-04-12T00:00:00Z"
    -    # Verify file permissions are restricted
    -    assert (auth_path.stat().st_mode & 0o777) == 0o600
    -
    -
    -def test_write_codex_cli_tokens_preserves_existing(tmp_path, monkeypatch):
    -    """_write_codex_cli_tokens preserves extra fields in existing auth.json."""
    -    codex_home = tmp_path / "codex-cli"
    -    codex_home.mkdir(parents=True, exist_ok=True)
    -    monkeypatch.setenv("CODEX_HOME", str(codex_home))
    -
    -    existing = {
    -        "tokens": {
    -            "access_token": "old-access",
    -            "refresh_token": "old-refresh",
    -            "extra_field": "preserved",
    -        },
    -        "last_refresh": "2026-01-01T00:00:00Z",
    -        "custom_key": "keep_me",
    -    }
    -    (codex_home / "auth.json").write_text(json.dumps(existing))
    -
    -    _write_codex_cli_tokens("updated-access", "updated-refresh")
    -
    -    data = json.loads((codex_home / "auth.json").read_text())
    -    assert data["tokens"]["access_token"] == "updated-access"
    -    assert data["tokens"]["refresh_token"] == "updated-refresh"
    -    assert data["tokens"]["extra_field"] == "preserved"
    -    assert data["custom_key"] == "keep_me"
    -    # last_refresh not updated since we didn't pass it
    -    assert data["last_refresh"] == "2026-01-01T00:00:00Z"
    -
    -
    -def test_write_codex_cli_tokens_handles_missing_dir(tmp_path, monkeypatch):
    -    """_write_codex_cli_tokens creates parent directories if missing."""
    -    codex_home = tmp_path / "does" / "not" / "exist"
    -    monkeypatch.setenv("CODEX_HOME", str(codex_home))
    -
    -    _write_codex_cli_tokens("at", "rt")
    -
    -    assert (codex_home / "auth.json").exists()
    -    data = json.loads((codex_home / "auth.json").read_text())
    -    assert data["tokens"]["access_token"] == "at"
    -
    -
    -def test_refresh_codex_auth_tokens_writes_back_to_cli(tmp_path, monkeypatch):
    -    """After refreshing, _refresh_codex_auth_tokens writes back to ~/.codex/auth.json."""
    -    from hermes_cli.auth import _refresh_codex_auth_tokens
    -
    -    hermes_home = tmp_path / "hermes"
    -    codex_home = tmp_path / "codex-cli"
    -    hermes_home.mkdir(parents=True, exist_ok=True)
    -    codex_home.mkdir(parents=True, exist_ok=True)
    -    (hermes_home / "auth.json").write_text(json.dumps({"version": 1, "providers": {}}))
    -    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    -    monkeypatch.setenv("CODEX_HOME", str(codex_home))
    -
    -    # Write initial CLI tokens
    -    (codex_home / "auth.json").write_text(json.dumps({
    -        "tokens": {"access_token": "old-at", "refresh_token": "old-rt"},
    -    }))
    -
    -    # Mock the pure refresh to return new tokens
    -    monkeypatch.setattr("hermes_cli.auth.refresh_codex_oauth_pure", lambda *a, **kw: {
    -        "access_token": "refreshed-at",
    -        "refresh_token": "refreshed-rt",
    -        "last_refresh": "2026-04-12T01:00:00Z",
    -    })
    -
    -    _refresh_codex_auth_tokens(
    -        {"access_token": "old-at", "refresh_token": "old-rt"},
    -        timeout_seconds=10,
    -    )
    -
    -    # Verify CLI file was updated
    -    cli_data = json.loads((codex_home / "auth.json").read_text())
    -    assert cli_data["tokens"]["access_token"] == "refreshed-at"
    -    assert cli_data["tokens"]["refresh_token"] == "refreshed-rt"
    -
    -
     def test_resolve_returns_hermes_auth_store_source(tmp_path, monkeypatch):
         hermes_home = tmp_path / "hermes"
         _setup_hermes_auth(hermes_home)
    ```
- **#42316: fix(auth): keep Codex OAuth pool accounts distinct on add + re-auth (#39236)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/credential_pool.py`, `hermes_cli/auth.py`, `hermes_cli/auth_commands.py`, `scripts/release.py`, `tests/hermes_cli/test_auth_codex_provider.py`, `tests/hermes_cli/test_auth_commands.py`
  - fix hunks:
  - `agent/credential_pool.py`

    ```diff
    @@ -91,6 +91,7 @@ def _load_config_safe() -> Optional[dict]:
     AUTH_TYPE_API_KEY = "[redacted:secret-assignment]"
     
     SOURCE_MANUAL = "manual"
    +SOURCE_MANUAL_DEVICE_CODE = f"{SOURCE_MANUAL}:device_code"
     
     STRATEGY_FILL_FIRST = "fill_first"
     STRATEGY_ROUND_ROBIN = "round_robin"
    ```
  - `hermes_cli/auth.py`

    ```diff
    @@ -1182,6 +1182,24 @@ def _store_provider_state(
             auth_store["active_provider"] = provider_id
     
     
    +def mark_provider_active_if_unset(provider_id: str) -> None:
    +    """Set ``active_provider`` to *provider_id* only when none is set yet.
    +
    +    Used by ``hermes auth add`` OAuth paths that create credential-pool
    +    entries directly (no singleton ``providers.<id>`` block). Adding the
    +    very first credential for a provider should make it the active provider
    +    so the setup wizard's ``_model_section_has_credentials()`` check (which
    +    consults ``get_active_provider()``) does not report "No inference
    +    provider configured". Subsequent adds for an already-active setup leave
    +    the user's chosen active provider untouched.
    +    """
    +    with _auth_store_lock():
    +        auth_store = _load_auth_store()
    +        if not (auth_store.get("active_provider") or "").strip():
    +            auth_store["active_provider"] = provider_id
    +            _save_auth_store(auth_store)
    +
    +
     def is_known_auth_provider(provider_id: str) -> bool:
         normalized = (provider_id or "").strip().lower()
         return normalized in PROVIDER_REGISTRY or normalized in SERVICE_PROVIDER_NAMES
    ```

    ```diff
    @@ -3355,6 +3373,7 @@ def _sync_codex_pool_entries(
         auth_store: Dict[str, Any],
         tokens: [redacted:secret-assignment] str],
         last_refresh: Optional[str],
    +    previous_singleton_tokens: [redacted:secret-assignment] str]] = None,
     ) -> None:
         """Mirror a fresh Codex re-auth into the credential_pool OAuth entries.
     
    ```

    ```diff
    @@ -3370,24 +3389,34 @@ def _sync_codex_pool_entries(
           OAuth flow when the user logged in via ``hermes setup`` / the model
           picker.  Always synced with the fresh tokens.
         * ``manual:device_code`` — entries created by ``hermes auth add openai-codex``
    -      that use the same device-code OAuth mechanism.  An interactive re-auth
    -      proves the user owns the ChatGPT account, so it is safe (and expected)
    -      to refresh these entries too.  Without this, a user who once ran the
    -      ``hermes auth add`` workaround for #33000 would silently leave that
    -      manual entry stale on every subsequent re-auth, recreating the issue
    -      reported in #33538.
    +      that use the same device-code OAuth mechanism.  ONLY synced if the
    +      entry's existing access_token matches the *previous* singleton
    +      access_token (i.e. the entry is a legacy singleton-alias from the
    +      #33000 workaround era).  Manual entries whose tokens never matched the
    +      singleton represent INDEPENDENT accounts added via
    +      ``hermes auth add openai-codex`` and must not be overwritten by a
    +      re-auth that targeted a different account (regression for #39236).
    +
    +      The original #33538 fix refreshed every ``manual:device_code`` entry
    +      unconditionally.  That worked when ``manual:device_code`` only meant
    +      "legacy alias of the singleton", but the same source string is now
    +      also produced by independent-account additions, and the broad sync
    +      silently clobbered distinct accounts with the latest-authenticated
    +      token pair.  The access_token-match check distinguishes the two cases
    +      without changing the source-string contract.
     
         What does NOT get refreshed:
     
         * ``manual:api_key`` and any other non-device-code manual sources — those
           are independent credentials (an explicit API key, a different ChatGPT
           account, etc.) and must not be overwritten by a single re-auth.
    +    * ``manual:device_code`` entries whose access_token does NOT match the
    +      previous singleton — see above; these are independent accounts.
     
    -    Error markers (``last_status``, ``last_error_*``) are also cleared on
    -    every device-code-backed entry — even those whose tokens we did not
    -    rewrite — so that an interactive re-auth gives every relevant pool entry
    -    a fresh selection chance instead of leaving them marked unhealthy from a
    -    pre-re-auth 401.
    +    Error markers (``last_status``, ``last_error_*``) are cleared ONLY on
    +    entries that actually had their tokens rewritten by this re-auth.
    +    Independent entries keep their own error state (their 401/429 markers
    +    belong to that account's own auth flow, not this re-auth).
         """
         access_token = [redacted:secret-assignment]"access_token")
         if not access_token:
    ```

    ```diff
    @@ -3399,15 +3428,34 @@ def _sync_codex_pool_entries(
         entries = pool.get("openai-codex")
         if not isinstance(entries, list):
             return
    -    # Sources whose tokens should be rewritten by a fresh Codex device-code
    -    # OAuth re-auth.  ``manual:api_key`` and unknown sources are intentionally
    -    # excluded — they represent independent credentials.
    -    REFRESHABLE_SOURCES = {"device_code", "manual:device_code"}
    +    # Previous singleton access_token (before this re-auth overwrote it) —
    +    # used to distinguish legacy singleton-aliases from independent accounts.
    +    # When None or empty, no manual entry can be treated as an alias (which
    +    # is the right default for first-ever-save or a freshly initialized
    +    # auth.json).
    +    prev_at = None
    +    if isinstance(previous_singleton_tokens, dict):
    +        prev_at = previous_singleton_tokens.get("access_token") or None
         for entry in entries:
             if not isinstance(entry, dict):
                 continue
             source = entry.get("source")
    -        if source not in REFRESHABLE_SOURCES:
    +        if source == "device_code":
    +            # Singleton-seeded mirror — always refresh.
    +            refresh_this_entry = True
    +        elif source == "manual:device_code":
    +            # Refresh only if this entry's existing access_token matches the
    +            # previous singleton access_token (i.e. it is a true alias of the
    +            # singleton from the #33000 workaround era).  An entry with its
    +            # own distinct token material is an independent account and must
    +            # be left alone (#39236).
    +            refresh_this_entry = bool(
    +                prev_at and entry.get("access_token") == prev_at
    +            )
    +        else:
    +            # ``manual:api_key`` and any future non-device-code sources.
    +            refresh_this_entry = False
    +        if not refresh_this_entry:
                 continue
             entry["access_token"] = access_token
             if refresh_token:
    ```

    ```diff
    @@ -3429,13 +3477,24 @@ def _save_codex_tokens(tokens: [redacted:secret-assignment] str], last_refresh: str = None, label:
         with _auth_store_lock():
             auth_store = _load_auth_store()
             state = _load_provider_state(auth_store, "openai-codex") or {}
    +        # Capture the previous singleton tokens BEFORE overwriting them.  The
    +        # pool-sync step uses this to distinguish legacy singleton-aliases
    +        # (which should be refreshed) from independent accounts that
    +        # ``hermes auth add openai-codex`` created (which must not be
    +        # overwritten — see #39236).
    +        previous_singleton_tokens = [redacted:secret-assignment]"tokens") if isinstance(state.get("tokens"), dict) else None
             state["tokens"] = tokens
             state["last_refresh"] = last_refresh
             state["auth_mode"] = "chatgpt"
             if label and str(label).strip():
                 state["label"] = str(label).strip()
             _save_provider_state(auth_store, "openai-codex", state)
    -        _sync_codex_pool_entries(auth_store, tokens, last_refresh)
    +        _sync_codex_pool_entries(
    +            auth_store,
    +            tokens,
    +            last_refresh,
    +            previous_singleton_tokens=[redacted:secret-assignment]
    +        )
             _save_auth_store(auth_store)
     
     
    ```
  - `hermes_cli/auth_commands.py`

    ```diff
    @@ -13,6 +13,7 @@
         AUTH_TYPE_OAUTH,
         CUSTOM_POOL_PREFIX,
         SOURCE_MANUAL,
    +    SOURCE_MANUAL_DEVICE_CODE,
         STATUS_EXHAUSTED,
         STRATEGY_FILL_FIRST,
         STRATEGY_ROUND_ROBIN,
    ```

    ```diff
    @@ -312,15 +313,35 @@ def auth_add_command(args) -> None:
                 creds["tokens"]["access_token"],
                 _oauth_default_label(provider, len(pool.entries()) + 1),
             )
    -        auth_mod._save_codex_tokens(
    -            creds["tokens"],
    -            last_refresh=creds.get("last_refresh"),
    +        # Add a distinct, self-contained pool entry per account (matching the
    +        # xai-oauth / google-gemini-cli / qwen-oauth patterns) instead of
    +        # routing through the singleton ``_save_codex_tokens`` save path.
    +        # The singleton round-trip collapsed every added account into the
    +        # latest login: a second ``hermes auth add openai-codex`` overwrote
    +        # the first account's singleton-mirrored ``device_code`` entry rather
    +        # than creating an independent one (#39236). ``manual:device_code``
    +        # entries refresh from their own token pair, so they need no singleton
    +        # shadow.
    +        entry = PooledCredential(
    +            provider=provider,
    +            id=uuid.uuid4().hex[:6],
                 label=label,
    +            auth_type=AUTH_TYPE_OAUTH,
    +            priority=0,
    +            source=SOURCE_MANUAL_DEVICE_CODE,
    +            access_token=[redacted:secret-assignment]"tokens"]["access_token"],
    +            refresh_token=[redacted:secret-assignment]"tokens"].get("refresh_token"),
    +            base_url=creds.get("base_url"),
    +            last_refresh=creds.get("last_refresh"),
             )
    -        pool = load_pool(provider)
    -        entry = next((item for item in pool.entries() if item.source == "device_code"), None)
    -        shown_label = entry.label if entry is not None else label
    -        print(f'Saved {provider} OAuth device-code credentials: "[redacted:secret-assignment]"')
    +        first_credential = not pool.entries()
    +        pool.add_entry(entry)
    +        # Adding the first Codex credential should make it the active provider
    +        # (the old singleton save path did this implicitly via
    +        # _save_provider_state). Subsequent adds leave the active provider as-is.
    +        if first_credential:
    +            auth_mod.mark_provider_active_if_unset(provider)
    +        print(f'Added {provider} OAuth credential #{len(pool.entries())}: "{entry.label}"')
             return
     
         if provider == "xai-oauth":
    ```
  - `scripts/release.py`

    ```diff
    @@ -66,6 +66,7 @@
         "[redacted:email]": "HeLLGURD",
         "[redacted:email]": "synapsesx",
         "[redacted:email]": "dirtyren",
    +    "[redacted:email]": "temalo",
         "[redacted:email]": "alarcritty",
         "[redacted:email]": "islam666",
         "[redacted:email]": "lsaether",
    ```
  - `tests/hermes_cli/test_auth_codex_provider.py`

    ```diff
    @@ -301,19 +301,23 @@ def test_save_codex_tokens_syncs_credential_pool(tmp_path, monkeypatch):
     
     
     def test_save_codex_tokens_syncs_manual_device_code_entries(tmp_path, monkeypatch):
    -    """Re-auth must also refresh ``manual:device_code`` pool entries.
    -
    -    Regression for #33538: a user who hit #33000 before the #33164 fix landed
    -    would have run ``hermes auth add openai-codex`` as a workaround, leaving
    -    a pool entry with ``source="manual:device_code"``.  On every subsequent
    -    re-auth via setup/model picker, the singleton-seeded ``device_code`` entry
    -    got refreshed but the ``manual:device_code`` entry stayed stale, recreating
    -    the same 401 token_invalidated symptom that #33164 was supposed to fix.
    -
    -    An interactive Codex device-code re-auth proves the user owns the ChatGPT
    -    account, so it is safe to refresh every device-code-backed entry in the
    -    pool — but NOT independent ``manual:api_key`` entries (separate accounts /
    -    explicit API keys).
    +    """Re-auth must refresh ``manual:device_code`` entries that are true
    +    aliases of the singleton, while leaving INDEPENDENT entries alone.
    +
    +    Original regression for #33538: a user who hit #33000 before the #33164
    +    fix landed would have run ``hermes auth add openai-codex`` as a
    +    workaround, leaving a pool entry with ``source="manual:device_code"``.
    +    On every subsequent re-auth via setup/model picker, the singleton-seeded
    +    ``device_code`` entry got refreshed but the ``manual:device_code`` entry
    +    stayed stale, recreating the same 401 token_invalidated symptom that
    +    #33164 was supposed to fix.
    +
    +    Narrowed for #39236: the original fix treated every ``manual:device_code``
    +    entry as a singleton-alias and refreshed them all, which silently
    +    clobbered independent accounts added via ``hermes auth add openai-codex``.
    +    The current behavior refreshes only entries whose access_token matches
    +    the *previous* singleton access_token (true legacy aliases), and leaves
    +    distinct-token entries alone (independent accounts).
         """
         hermes_home = tmp_path / "hermes"
         hermes_home.mkdir(parents=True, exist_ok=True)
    ```

    ```diff
    @@ -335,16 +339,30 @@ def test_save_codex_tokens_syncs_manual_device_code_entries(tmp_path, monkeypatc
                         "access_token": "old-at",
                         "refresh_token": "old-rt",
                     },
    +                # Legacy alias from the #33000 workaround era — its tokens
    +                # match the singleton, so it is a true alias and SHOULD be
    +                # refreshed (preserves #33538 behavior).
                     {
    -                    "id": "auth-add",
    +                    "id": "legacy-alias",
                         "source": "manual:device_code",
                         "auth_type": "oauth",
    -                    "access_token": "stale-manual-at",
    -                    "refresh_token": "stale-manual-rt",
    +                    "access_token": "old-at",
    +                    "refresh_token": "old-rt",
                         "last_status": "exhausted",
                         "last_error_code": 401,
                         "last_error_reason": "token_invalidated",
                     },
    +                # Independent account from `hermes auth add openai-codex` —
    +                # its tokens are distinct from the singleton.  Must NOT be
    +                # overwritten by a re-auth that targeted a different account
    +                # (#39236).
    +                {
    +                    "id": "independent",
    +                    "source": "manual:device_code",
    +                    "auth_type": "oauth",
    +                    "access_token": "independent-at",
    +                    "refresh_token": "independent-rt",
    +                },
                     {
                         "id": "api-key",
                         "source": "manual:api_key",
    ```

    ```diff
    @@ -363,18 +381,23 @@ def test_save_codex_tokens_syncs_manual_device_code_entries(tmp_path, monkeypatc
         pool = auth["credential_pool"]["openai-codex"]
     
         # Singleton-seeded device_code entry: refreshed and error markers cleared.
    -    seeded = next(e for e in pool if e["source"] == "device_code")
    +    seeded = next(e for e in pool if e["id"] == "seeded")
         assert seeded["access_token"] == "fresh-at"
         assert seeded["refresh_token"] == "fresh-rt"
     
    -    # manual:device_code entry: ALSO refreshed (the new behavior).
    -    manual_dc = next(e for e in pool if e["source"] == "manual:device_code")
    -    assert manual_dc["access_token"] == "fresh-at"
    -    assert manual_dc["refresh_token"] == "fresh-rt"
    -    assert manual_dc["last_refresh"] == "2026-05-28T00:00:00Z"
    -    assert manual_dc["last_status"] is None
    -    assert manual_dc["last_error_code"] is None
    -    assert manual_dc["last_error_reason"] is None
    +    # Legacy alias (tokens matched previous singleton): ALSO refreshed.
    +    legacy = next(e for e in pool if e["id"] == "legacy-alias")
    +    assert legacy["access_token"] == "fresh-at"
    +    assert legacy["refresh_token"] == "fresh-rt"
    +    assert legacy["last_refresh"] == "2026-05-28T00:00:00Z"
    +    assert legacy["last_status"] is None
    +    assert legacy["last_error_code"] is None
    +    assert legacy["last_error_reason"] is None
    +
    +    # Independent manual:device_code entry: NOT overwritten (#39236).
    +    independent = next(e for e in pool if e["id"] == "independent")
    +    assert independent["access_token"] == "independent-at"
    +    assert independent["refresh_token"] == "independent-rt"
     
         # manual:api_key entry: untouched — independent credential.
         api_key = [redacted:secret-assignment] for e in pool if e["source"] == "manual:api_key")
    ```

    ```diff
    @@ -382,6 +405,333 @@ def test_save_codex_tokens_syncs_manual_device_code_entries(tmp_path, monkeypatc
         assert "refresh_token" not in api_key or api_key.get("refresh_token") is None
     
     
    +def test_save_codex_tokens_does_not_overwrite_independent_manual_entries(tmp_path, monkeypatch):
    +    """Re-auth must NOT overwrite ``manual:device_code`` entries that hold
    +    independent token material (different OpenAI/ChatGPT accounts).
    +
    +    Regression for #39236: ``hermes auth add openai-codex`` for accounts B and C
    +    routes through ``_save_codex_tokens`` because the singleton path is the
    +    only Codex OAuth save flow.  The #33538 fix refreshed every
    +    ``manual:device_code`` entry on every re-auth, which works fine for the
    +    one-account/legacy-workaround case but silently overwrote distinct
    +    independent accounts with the latest-authenticated tokens (labels
    +    preserved, token material clobbered, status/quota readings then lie).
    +
    +    The safe invariant: an entry is a singleton-alias only when its current
    +    access_token matches the *previous* singleton access_token.  Manual
    +    entries whose tokens never matched the singleton are independent accounts
    +    and must be left alone.
    +    """
    +    hermes_home = tmp_path / "hermes"
    +    hermes_home.mkdir(parents=True, exist_ok=True)
    +    (hermes_home / "auth.json").write_text(json.dumps({
    +        "version": 1,
    +        "providers": {
    +            "openai-codex": {
    +                # Old singleton tokens — represent "account A" which the user
    +                # logged in with via setup originally.
    +                "tokens": {"access_token": "acctA-at", "refresh_token": "acctA-rt"},
    +                "last_refresh": "2026-01-01T00:00:00Z",
    +                "auth_mode": "chatgpt",
    +                "label": "account-A",
    +            },
    +        },
    +        "credential_pool": {
    +            "openai-codex": [
    +                # The seeded singleton mirror of account A.
    +                {
    +                    "id": "seeded",
    +                    "label": "account-A",
    +                    "source": "device_code",
    +                    "auth_type": "oauth",
    +                    "access_token": "acctA-at",
    +                    "refresh_token": "acctA-rt",
    +                },
    +                # Two INDEPENDENT manual entries added later via
    +                # ``hermes auth add openai-codex`` (account B and account C).
    +                # Each has its OWN distinct token material, unrelated to the
    +                # singleton.
    +                {
    +                    "id": "acctB",
    +                    "label": "account-B",
    +                    "source": "manual:device_code",
    +                    "auth_type": "oauth",
    +                    "access_token": "acctB-at",
    +                    "refresh_token": "acctB-rt",
    +                },
    +                {
    +                    "id": "acctC",
    +                    "label": "account-C",
    +                    "source": "manual:device_code",
    +                    "auth_type": "oauth",
    +                    "access_token": "acctC-at",
    +                    "refresh_token": "acctC-rt",
    +                },
    +            ],
    +        },
    +    }))
    +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +
    +    # User re-authenticates account A — fresh device-code login produces new
    +    # tokens.  The legitimate update is the seeded singleton mirror; the
    +    # independent acctB/acctC entries must be untouched.
    +    _save_codex_tokens(
    +        {"access_token": "acctA-new-at", "refresh_token": "acctA-new-rt"},
    +        last_refresh="2026-06-05T00:00:00Z",
    +    )
    +
    +    auth = json.loads((hermes_home / "auth.json").read_text())
    +    pool = auth["credential_pool"]["openai-codex"]
    +
    +    # Singleton-seeded entry: refreshed (legitimate sync).
    +    seeded = next(e for e in pool if e["source"] == "device_code")
    +    assert seeded["access_token"] == "acctA-new-at"
    +    assert seeded["refresh_token"] == "acctA-new-rt"
    +    assert seeded["last_refresh"] == "2026-06-05T00:00:00Z"
    +
    +    # acctB: INDEPENDENT entry — must NOT be overwritten.
    +    acctB = next(e for e in pool if e["id"] == "acctB")
    +    assert acctB["access_token"] == "acctB-at", (
    +        "acctB was clobbered by acctA re-auth (#39236 regression)"
    +    )
    +    assert acctB["refresh_token"] == "acctB-rt"
    +
    +    # acctC: INDEPENDENT entry — must NOT be overwritten.
    +    acctC = next(e for e in pool if e["id"] == "acctC")
    +    assert acctC["access_token"] == "acctC-at", (
    +        "acctC was clobbered by acctA re-auth (#39236 regression)"
    +    )
    +    assert acctC["refresh_token"] == "acctC-rt"
    +
    +
    +def test_save_codex_tokens_still_refreshes_legacy_manual_alias(tmp_path, monkeypatch):
    +    """The #33538 legacy use case must keep working.
    +
    +    A user who hit #33000 before the #33164 fix landed might have run
    +    ``hermes auth add openai-codex`` as a workaround when there was no
    +    singleton entry — that created a ``manual:device_code`` pool entry that
    +    holds the SAME token material as the (later) singleton.  This entry is a
    +    true alias of the singleton and SHOULD still be refreshed on subsequent
    +    re-auths, otherwise it goes stale and recreates the #33538 symptom.
    +
    +    The distinguishing signal: a legacy alias has access_token == previous
    +    singleton access_token; an independent account does not.
    +    """
    +    hermes_home = tmp_path / "hermes"
    +    hermes_home.mkdir(parents=True, exist_ok=True)
    +    (hermes_home / "auth.json").write_text(json.dumps({
    +        "version": 1,
    +        "providers": {
    +            "openai-codex": {
    +                "tokens": {"access_token": "shared-at", "refresh_token": "shared-rt"},
    +                "last_refresh": "2026-01-01T00:00:00Z",
    +                "auth_mode": "chatgpt",
    +            },
    +        },
    +        "credential_pool": {
    +            "openai-codex": [
    +                {
    +                    "id": "seeded",
    +                    "source": "device_code",
    +                    "auth_type": "oauth",
    +                    "access_token": "shared-at",
    +                    "refresh_token": "shared-rt",
    +                },
    +                {
    +                    "id": "legacy",
    +                    "label": "legacy-alias",
    +                    "source": "manual:device_code",
    +                    "auth_type": "oauth",
    +                    # Token material matches the singleton — this is a true
    +                    # alias from the #33000 workaround era.
    +                    "access_token": "shared-at",
    +                    "refresh_token": "shared-rt",
    +                    "last_status": "exhausted",
    +                    "last_error_code": 401,
    +                    "last_error_reason": "token_invalidated",
    +                },
    +            ],
    +        },
    +    }))
    +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +
    +    _save_codex_tokens(
    +        {"access_token": "fresh-at", "refresh_token": "fresh-rt"},
    +        last_refresh="2026-06-05T00:00:00Z",
    +    )
    +
    +    auth = json.loads((hermes_home / "auth.json").read_text())
    +    pool = auth["credential_pool"]["openai-codex"]
    +
    +    # Singleton: refreshed.
    +    seeded = next(e for e in pool if e["source"] == "device_code")
    +    assert seeded["access_token"] == "fresh-at"
    +
    +    # Legacy alias: still refreshed (preserves #33538 fix).
    +    legacy = next(e for e in pool if e["id"] == "legacy")
    +    assert legacy["access_token"] == "fresh-at"
    +    assert legacy["refresh_token"] == "fresh-rt"
    +    assert legacy["last_refresh"] == "2026-06-05T00:00:00Z"
    +    # Error markers cleared on the refreshed entry.
    +    assert legacy["last_status"] is None
    +    assert legacy["last_error_code"] is None
    +    assert legacy["last_error_reason"] is None
    +
    +
    +def test_save_codex_tokens_handles_missing_previous_singleton_tokens(tmp_path, monkeypatch):
    +    """First-ever Codex save (no prior singleton tokens) must not crash.
    +
    +    Edge case: a user has only pool entries (e.g. via direct auth.json edit
    +    or a partial state from a corrupted upgrade), no `providers.openai-codex.tokens`
    +    block at all.  The previous-singleton-tokens guard must handle missing
    +    state gracefully — fall back to "no previous tokens", which means no
    +    pool entry can be a true alias and only the singleton-seeded entry gets
    +    written.
    +    """
    +    hermes_home = tmp_path / "hermes"
    +    hermes_home.mkdir(parents=True, exist_ok=True)
    +    (hermes_home / "auth.json").write_text(json.dumps({
    +        "version": 1,
    +        "providers": {},
    +        "credential_pool": {
    +            "openai-codex": [
    +                {
    +                    "id": "preexisting",
    +                    "label": "pre-existing-manual",
    +                    "source": "manual:device_code",
    +                    "auth_type": "oauth",
    +                    "access_token": "preexisting-at",
    +                    "refresh_token": "preexisting-rt",
    +                },
    +            ],
    +        },
    +    }))
    +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +
    +    _save_codex_tokens(
    +        {"access_token": "first-at", "refresh_token": "first-rt"},
    +        last_refresh="2026-06-05T00:00:00Z",
    +    )
    +
    +    auth = json.loads((hermes_home / "auth.json").read_text())
    +    pool = auth["credential_pool"]["openai-codex"]
    +    # Pre-existing independent entry with no relationship to a (now-new)
    +    # singleton MUST be preserved.
    +    pre = next(e for e in pool if e["id"] == "preexisting")
    +    assert pre["access_token"] == "preexisting-at"
    +    assert pre["refresh_token"] == "preexisting-rt"
    +
    +
    +def test_save_codex_tokens_alias_match_uses_access_token_only(tmp_path, monkeypatch):
    +    """A manual entry counts as an alias if its access_token matches the
    +    previous singleton access_token, regardless of refresh_token presence.
    +
    +    Some legacy entries (older auth.json schemas, pre-refresh-token versions)
    +    have access_token but no refresh_token.  These should still be treated as
    +    aliases when the access_token matches.
    +    """
    +    hermes_home = tmp_path / "hermes"
    +    hermes_home.mkdir(parents=True, exist_ok=True)
    +    (hermes_home / "auth.json").write_text(json.dumps({
    +        "version": 1,
    +        "providers": {
    +            "openai-codex": {
    +                "tokens": {"access_token": "shared-at", "refresh_token": "shared-rt"},
    +                "auth_mode": "chatgpt",
    +            },
    +        },
    +        "credential_pool": {
    +            "openai-codex": [
    +                {
    +                    "id": "alias-no-refresh",
    +                    "source": "manual:device_code",
    +                    "auth_type": "oauth",
    +                    "access_token": "shared-at",
    +                    # No refresh_token at all — legacy schema.
    +                },
    +            ],
    +        },
    +    }))
    +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +
    +    _save_codex_tokens(
    +        {"access_token": "new-at", "refresh_token": "new-rt"},
    +        last_refresh="2026-06-05T00:00:00Z",
    +    )
    +
    +    auth = json.loads((hermes_home / "auth.json").read_text())
    +    pool = auth["credential_pool"]["openai-codex"]
    +    alias = next(e for e in pool if e["id"] == "alias-no-refresh")
    +    # Treated as alias → refreshed with new tokens.
    +    assert alias["access_token"] == "new-at"
    +    assert alias["refresh_token"] == "new-rt"
    +
    +
    +def test_save_codex_tokens_clears_error_markers_only_on_refreshed_entries(tmp_path, monkeypatch):
    +    """Error markers must be cleared only on entries that were actually
    +    refreshed by this re-auth.  Independent ``manual:device_code`` entries
    +    with their own stale-error markers must be left alone (their stale state
    +    is not the current re-auth's business).
    +    """
    +    hermes_home = tmp_path / "hermes"
    +    hermes_home.mkdir(parents=True, exist_ok=True)
    +    (hermes_home / "auth.json").write_text(json.dumps({
    +        "version": 1,
    +        "providers": {
    +            "openai-codex": {
    +                "tokens": {"access_token": "acctA-at", "refresh_token": "acctA-rt"},
    +                "auth_mode": "chatgpt",
    +            },
    +        },
    +        "credential_pool": {
    +            "openai-codex": [
    +                {
    +                    "id": "seeded",
    +                    "source": "device_code",
    +                    "auth_type": "oauth",
    +                    "access_token": "acctA-at",
    +                    "refresh_token": "acctA-rt",
    +                    "last_status": "exhausted",
    +                    "last_error_code": 401,
    +                },
    +                {
    +                    "id": "acctB",
    +                    "source": "manual:device_code",
    +                    "auth_type": "oauth",
    +                    "access_token": "acctB-at",
    +                    "refresh_token": "acctB-rt",
    +                    "last_status": "exhausted",
    +                    "last_error_code": 429,
    +                    "last_error_reason": "quota_exhausted",
    +                },
    +            ],
    +        },
    +    }))
    +    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    +
    +    _save_codex_tokens(
    +        {"access_token": "fresh-at", "refresh_token": "fresh-rt"},
    +        last_refresh="2026-06-05T00:00:00Z",
    +    )
    +
    +    auth = json.loads((hermes_home / "auth.json").read_text())
    +    pool = auth["credential_pool"]["openai-codex"]
    +
    +    # Singleton: refreshed AND error markers cleared.
    +    seeded = next(e for e in pool if e["id"] == "seeded")
    +    assert seeded["access_token"] == "fresh-at"
    +    assert seeded["last_status"] is None
    +    assert seeded["last_error_code"] is None
    +
    +    # Independent acctB: NOT refreshed AND error markers NOT cleared.
    +    # (Its 429 quota state belongs to acctB's own account, not acctA's re-auth.)
    +    acctB = next(e for e in pool if e["id"] == "acctB")
    +    assert acctB["access_token"] == "acctB-at"  # not overwritten
    +    assert acctB["last_status"] == "exhausted"  # not cleared
    +    assert acctB["last_error_code"] == 429
    +    assert acctB["last_error_reason"] == "quota_exhausted"
    +
    +
     def test_import_codex_cli_tokens(tmp_path, monkeypatch):
         codex_home = tmp_path / "codex-cli"
         codex_home.mkdir(parents=True, exist_ok=True)
    ```
  - `tests/hermes_cli/test_auth_commands.py`

    ```diff
    @@ -397,15 +397,92 @@ class _Args:
     
         payload = json.loads((tmp_path / "hermes" / "auth.json").read_text())
         entries = payload["credential_pool"]["openai-codex"]
    -    entry = next(item for item in entries if item["source"] == "device_code")
    +    # The add path now creates a distinct, self-contained ``manual:device_code``
    +    # pool entry per account instead of routing through the singleton save path
    +    # (which collapsed multiple accounts into the latest login — #39236).
    +    entry = next(item for item in entries if item["source"] == "manual:device_code")
         assert payload["active_provider"] == "openai-codex"
    -    assert payload["providers"]["openai-codex"]["tokens"]["access_token"] == token
    +    # No singleton ``providers.openai-codex`` block is written by the add path.
    +    assert "openai-codex" not in payload.get("providers", {})
         assert entry["label"] == "[redacted:email]"
    -    assert entry["source"] == "device_code"
    +    assert entry["source"] == "manual:device_code"
    +    assert entry["access_token"] == token
         assert entry["refresh_token"] == "refresh-token"
         assert entry["base_url"] == "https://chatgpt.com/backend-api/codex"
     
     
    +def test_auth_add_codex_oauth_keeps_distinct_pool_accounts(tmp_path, monkeypatch):
    +    """Two ``hermes auth add openai-codex`` runs for different ChatGPT
    +    accounts must produce two independent pool entries with distinct tokens.
    +
    +    Regression for #39236: the add path used to route through the singleton
    +    ``_save_codex_tokens`` save, so the second login overwrote the first
    +    account's singleton-mirrored ``device_code`` entry instead of adding a
    +    second independent one. ``hermes auth list`` showed two labels sharing
    +    one token pair, and rotation silently always used the latest account.
    +    """
    +    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
    +    _write_auth_store(tmp_path, {"version": 1, "providers": {}})
    +    first_token = [redacted:secret-assignment]"[redacted:email]")
    +    second_token = [redacted:secret-assignment]"[redacted:email]")
    +    logins = iter(
    +        [
    +            {
    +                "tokens": {
    +                    "access_token": first_token,
    +                    "refresh_token": "first-refresh-token",
    +                },
    +                "base_url": "https://chatgpt.com/backend-api/codex",
    +                "last_refresh": "2026-03-23T10:00:00Z",
    +            },
    +            {
    +                "tokens": {
    +                    "access_token": second_token,
    +                    "refresh_token": "second-refresh-token",
    +                },
    +                "base_url": "https://chatgpt.com/backend-api/codex",
    +                "last_refresh": "2026-03-23T10:05:00Z",
    +            },
    +        ]
    +    )
    +    monkeypatch.setattr("hermes_cli.auth._codex_device_code_login", lambda: next(logins))
    +
    +    from hermes_cli.auth_commands import auth_add_command
    +    from agent.credential_pool import load_pool
    +
    +    class _Args:
    +        provider = "openai-codex"
    +        auth_type = "oauth"
    +        api_key = None
    +        label = None
    +
    +    auth_add_command(_Args())
    +    auth_add_command(_Args())
    +
    +    pool = load_pool("openai-codex")
    +    entries = pool.entries()
    +
    +    assert [entry.source for entry in entries] == [
    +        "manual:device_code",
    +        "manual:device_code",
    +    ]
    +    assert [entry.label for entry in entries] == [
    +        "[redacted:email]",
    +        "[redacted:email]",
    +    ]
    +    assert [entry.access_token for entry in entries] == [first_token, second_token]
    +    assert [entry.refresh_token for entry in entries] == [
    +        "first-refresh-token",
    +        "second-refresh-token",
    +    ]
    +
    +    payload = json.loads((tmp_path / "hermes" / "auth.json").read_text())
    +    # No singleton block — the add path is now pool-only.
    +    assert "openai-codex" not in payload.get("providers", {})
    +    # First add activated the provider; second add left it as-is.
    +    assert payload["active_provider"] == "openai-codex"
    +
    +
     def test_auth_add_xai_oauth_sets_active_provider(tmp_path, monkeypatch):
         """hermes auth add xai-oauth must write providers singleton and set active_provider.
     
    ```

    ```diff
    @@ -1313,9 +1390,9 @@ class _Args:
         payload = json.loads((hermes_home / "auth.json").read_text())
         # Suppression marker must be cleared
         assert "openai-codex" not in payload.get("suppressed_sources", {})
    -    # New pool entry must be present
    +    # New pool entry must be present (distinct manual:device_code entry — #39236)
         entries = payload["credential_pool"]["openai-codex"]
    -    assert any(e["source"] == "device_code" for e in entries)
    +    assert any(e["source"] == "manual:device_code" for e in entries)
         assert payload["active_provider"] == "openai-codex"
     
     
    ```
- **#11382: fix(auth): keep manual Codex pool entries isolated**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `b02833f32d4b22d989c668a0d6cb2f1cf3b57f75`, `#12360`, `#11364`, `#11364`, `#12924`
- **#11727: fix: openai-codex credential rotation — sync guard + 429 token refresh**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `b02833f32`, `#12360`, `#11364`, `#12360`, `#12360`, `#11364`, `#12360`
- **#39243: fix(auth): preserve independent Codex pool entries on re-auth (#39236)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `761b744ab`, `#42316`
- **#40771: fix: preserve codex OAuth pool entries**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
- **#42110: fix(auth): propagate credential label to pool entries on re-auth**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
- **#42434: fix(auth): preserve independent Codex OAuth profiles**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `761b744abbc6`, `c78b3e1d3ccc`, `#42102`, `#39236`
- **#100423: fix(auth): isolate Codex singleton sync by principal**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `03fee43ca3`, `#115626`, `#115626`

## Fix status

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