# NousResearch/hermes-agent · Auth credential pool concurrency and sync bugs

## Open now

- Nothing open on GitHub.

These issues and fixes address race conditions and data loss in the OAuth credential pool, where concurrent writes, stale rewrites, and auth.json sync could drop or overwrite newly added or exhausted credentials.

## TL;DR

- 6 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 2 merged PR(s), 2 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-53896`. 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.29`: 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

- #6907: bug: credential pool _persist overwrites concurrent auth entries and _sync_codex_entry_from_cli rolls back exhaustion status
- #19566: \[Bug\]: OpenAI-Codex credential pool can drop newly added credential after stale auth.json rewrite during rotation

## 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)
    ```
- **#53896: fix(auth): preserve concurrently-added credentials on pool rewrite (#19566)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/credential_pool.py`, `hermes_cli/auth.py`, `scripts/release.py`, `tests/agent/test_credential_pool.py`
  - fix hunks:
  - `agent/credential_pool.py`

    ```diff
    @@ -537,10 +537,11 @@ def _replace_entry(self, old: PooledCredential, new: PooledCredential) -> None:
                     self._entries[idx] = new
                     return
     
    -    def _persist(self) -> None:
    +    def _persist(self, *, removed_ids: Optional[List[str]] = None) -> None:
             write_credential_pool(
                 self.provider,
                 [entry.to_dict() for entry in self._entries],
    +            removed_ids=removed_ids,
             )
     
         def _is_terminal_auth_failure(
    ```

    ```diff
    @@ -1124,13 +1125,17 @@ def _refresh_entry(self, entry: PooledCredential, *, force: bool) -> Optional[Po
                             logger.debug(
                                 "Failed to clear terminal xAI OAuth state: %s", clear_exc
                             )
    +                    removed_ids = [
    +                        item.id for item in self._entries
    +                        if item.source == "loopback_pkce"
    +                    ]
                         self._entries = [
                             item for item in self._entries
                             if item.source != "loopback_pkce"
                         ]
                         if self._current_id == entry.id:
                             self._current_id = None
    -                    self._persist()
    +                    self._persist(removed_ids=removed_ids)
                         return None
                 # For openai-codex: same race as xAI/nous — another Hermes process
                 # may have consumed the refresh token between our proactive sync
    ```

    ```diff
    @@ -1190,13 +1195,17 @@ def _refresh_entry(self, entry: PooledCredential, *, force: bool) -> Optional[Po
                             logger.debug(
                                 "Failed to clear terminal Codex OAuth state: %s", clear_exc
                             )
    +                    removed_ids = [
    +                        item.id for item in self._entries
    +                        if item.source == "device_code"
    +                    ]
                         self._entries = [
                             item for item in self._entries
                             if item.source != "device_code"
                         ]
                         if self._current_id == entry.id:
                             self._current_id = None
    -                    self._persist()
    +                    self._persist(removed_ids=removed_ids)
                         return None
                 # For nous: another process may have consumed the refresh token
                 # between our proactive sync and the HTTP call.  Re-sync from
    ```

    ```diff
    @@ -1253,13 +1262,17 @@ def _refresh_entry(self, entry: PooledCredential, *, force: bool) -> Optional[Po
                             auth_mod.NOUS_DEVICE_CODE_SOURCE,
                             f"manual:{auth_mod.NOUS_DEVICE_CODE_SOURCE}",
                         }
    +                    removed_ids = [
    +                        item.id for item in self._entries
    +                        if item.source in singleton_sources
    +                    ]
                         self._entries = [
                             item for item in self._entries
                             if item.source not in singleton_sources
                         ]
                         if self._current_id == entry.id:
                             self._current_id = None
    -                    self._persist()
    +                    self._persist(removed_ids=removed_ids)
                         return None
                 self._mark_exhausted(entry, None)
                 return None
    ```

    ```diff
    @@ -1421,7 +1434,7 @@ def _available_entries(self, *, clear_expired: bool = False, refresh: bool = Fal
                 pruned_ids = set(entries_to_prune)
                 self._entries = [e for e in self._entries if e.id not in pruned_ids]
             if cleared_any:
    -            self._persist()
    +            self._persist(removed_ids=entries_to_prune)
             return available
     
         def _select_unlocked(self) -> Optional[PooledCredential]:
    ```

    ```diff
    @@ -1595,7 +1608,11 @@ def remove_index(self, index: int) -> Optional[PooledCredential]:
                 replace(entry, priority=new_priority)
                 for new_priority, entry in enumerate(self._entries)
             ]
    -        self._persist()
    +        write_credential_pool(
    +            self.provider,
    +            [entry.to_dict() for entry in self._entries],
    +            removed_ids=[removed.id],
    +        )
             if self._current_id == removed.id:
                 self._current_id = None
             return removed
    ```

    ```diff
    @@ -2257,6 +2274,11 @@ def _is_suppressed(_p, _s):  # type: ignore[misc]
     def load_pool(provider: str) -> CredentialPool:
         provider = (provider or "").strip().lower()
         raw_entries = read_credential_pool(provider)
    +    disk_ids = {
    +        entry.get("id")
    +        for entry in raw_entries
    +        if isinstance(entry, dict) and entry.get("id")
    +    }
         raw_needs_sanitization = any(
             isinstance(payload, dict)
             and sanitize_borrowed_credential_payload(payload, provider) != payload
    ```

    ```diff
    @@ -2285,8 +2307,10 @@ def load_pool(provider: str) -> CredentialPool:
             changed |= _normalize_pool_priorities(provider, entries)
     
         if changed:
    +        new_ids = {entry.id for entry in entries}
             write_credential_pool(
                 provider,
                 [entry.to_dict() for entry in sorted(entries, key=lambda item: item.priority)],
    +            removed_ids=disk_ids - new_ids,
             )
         return CredentialPool(provider, entries)
    ```
  - `hermes_cli/auth.py`

    ```diff
    @@ -38,7 +38,7 @@
     from datetime import datetime, timezone
     from http.server import BaseHTTPRequestHandler, HTTPServer, ThreadingHTTPServer
     from pathlib import Path
    -from typing import Any, Callable, Dict, FrozenSet, List, Optional, Tuple
    +from typing import Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Tuple
     from urllib.parse import parse_qs, urlencode, urlparse
     
     import httpx
    ```

    ```diff
    @@ -1260,24 +1260,54 @@ def read_credential_pool(provider_id: Optional[str] = None) -> Dict[str, Any]:
         return list(global_entries) if isinstance(global_entries, list) else []
     
     
    -def write_credential_pool(provider_id: str, entries: List[Dict[str, Any]]) -> Path:
    +def write_credential_pool(
    +    provider_id: str,
    +    entries: List[Dict[str, Any]],
    +    *,
    +    removed_ids: Optional[Iterable[str]] = None,
    +) -> Path:
         """Persist one provider's credential pool under auth.json.
     
         This is the final disk-boundary guard for borrowed/reference-only
         credentials. Callers may pass raw dictionaries, so sanitize here even when
         ``PooledCredential.to_dict()`` already did the same work upstream.
    +
    +    Re-read the on-disk pool under the same lock and merge entries present on
    +    disk but missing from ``entries``. Those were added by another process after
    +    the caller loaded its in-memory snapshot; without this merge a later
    +    rotation/exhaustion rewrite drops the concurrent credential.
    +
    +    Pass ``removed_ids`` for entries the caller intentionally removed, so the
    +    merge does not resurrect them from the on-disk copy.
         """
    +    removed = {rid for rid in (removed_ids or ()) if rid}
         with _auth_store_lock():
             auth_store = _load_auth_store()
             pool = auth_store.get("credential_pool")
             if not isinstance(pool, dict):
                 pool = {}
                 auth_store["credential_pool"] = pool
    -        pool[provider_id] = [
    +        sanitized_entries = [
                 sanitize_borrowed_credential_payload(entry, provider_id)
                 if isinstance(entry, dict) else entry
                 for entry in entries
             ]
    +        existing = pool.get(provider_id)
    +        existing_list = existing if isinstance(existing, list) else []
    +        new_ids = {
    +            entry.get("id")
    +            for entry in sanitized_entries
    +            if isinstance(entry, dict) and entry.get("id")
    +        }
    +        merged: List[Dict[str, Any]] = list(sanitized_entries)
    +        for disk_entry in existing_list:
    +            if not isinstance(disk_entry, dict):
    +                continue
    +            disk_id = disk_entry.get("id")
    +            if not disk_id or disk_id in new_ids or disk_id in removed:
    +                continue
    +            merged.append(sanitize_borrowed_credential_payload(disk_entry, provider_id))
    +        pool[provider_id] = merged
             return _save_auth_store(auth_store)
     
     
    ```
  - `scripts/release.py`

    ```diff
    @@ -45,6 +45,7 @@
     
     # Auto-extracted from noreply emails + manual overrides
     AUTHOR_MAP = {
    +    "[redacted:email]": "konsisumer",  # PR #19608 salvage (read-modify-write merge in write_credential_pool to preserve concurrently-added credentials; #19566)
         "[redacted:email]": "dalenguyen",  # PR #53678 salvage (strip VIRTUAL_ENV/CONDA_PREFIX from terminal subprocess env; #23473)
         "[redacted:email]": "Blaryxoff",  # PR #32602 salvage (deep-merge PUT /api/config to preserve unrelated sections; #13396)
         "[redacted:email]": "DiamondEyesFox",  # PR #53351 salvage (rebaseline in-place compression flushes to prevent duplicate compacted rows; #9096)
    ```
  - `tests/agent/test_credential_pool.py`

    ```diff
    @@ -3045,3 +3045,104 @@ def _transient_failure(*_args, **_kwargs):
         tokens = [redacted:secret-assignment]"providers"]["openai-codex"].get("tokens", {})
         assert tokens.get("access_token") == "old-access-token"
         assert tokens.get("refresh_token") == "old-refresh-token"
    +
    +
    +def test_persist_preserves_concurrent_disk_only_entry(tmp_path, monkeypatch):
    +    """Regression for #19566: stale rotation writes keep concurrent entries."""
    +    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
    +    _write_auth_store(
    +        tmp_path,
    +        {
    +            "version": 1,
    +            "credential_pool": {
    +                "anthropic": [
    +                    {
    +                        "id": "cred-A",
    +                        "label": "primary",
    +                        "auth_type": "api_key",
    +                        "priority": 0,
    +                        "source": "manual",
    +                        "access_token": "sk-A",
    +                    },
    +                    {
    +                        "id": "cred-B",
    +                        "label": "secondary",
    +                        "auth_type": "api_key",
    +                        "priority": 1,
    +                        "source": "manual",
    +                        "access_token": "sk-B",
    +                    },
    +                ]
    +            },
    +        },
    +    )
    +
    +    from agent.credential_pool import load_pool
    +    from hermes_cli.auth import read_credential_pool, write_credential_pool
    +
    +    pool = load_pool("anthropic")
    +    assert {entry.id for entry in pool.entries()} == {"cred-A", "cred-B"}
    +
    +    disk_snapshot = read_credential_pool("anthropic")
    +    disk_snapshot.append(
    +        {
    +            "id": "cred-C",
    +            "label": "added-concurrently",
    +            "auth_type": "api_key",
    +            "priority": 2,
    +            "source": "manual",
    +            "access_token": "sk-C",
    +        }
    +    )
    +    write_credential_pool("anthropic", disk_snapshot)
    +
    +    pool.mark_exhausted_and_rotate(status_code=429)
    +
    +    final = json.loads((tmp_path / "hermes" / "auth.json").read_text())
    +    final_ids = [entry["id"] for entry in final["credential_pool"]["anthropic"]]
    +    assert set(final_ids) == {"cred-A", "cred-B", "cred-C"}
    +    persisted_a = next(
    +        entry
    +        for entry in final["credential_pool"]["anthropic"]
    +        if entry["id"] == "cred-A"
    +    )
    +    assert persisted_a["last_status"] == "exhausted"
    +
    +
    +def test_remove_index_does_not_resurrect_via_disk_merge(tmp_path, monkeypatch):
    +    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
    +    _write_auth_store(
    +        tmp_path,
    +        {
    +            "version": 1,
    +            "credential_pool": {
    +                "anthropic": [
    +                    {
    +                        "id": "cred-A",
    +                        "label": "keep",
    +                        "auth_type": "api_key",
    +                        "priority": 0,
    +                        "source": "manual",
    +                        "access_token": "sk-A",
    +                    },
    +                    {
    +                        "id": "cred-B",
    +                        "label": "drop",
    +                        "auth_type": "api_key",
    +                        "priority": 1,
    +                        "source": "manual",
    +                        "access_token": "sk-B",
    +                    },
    +                ]
    +            },
    +        },
    +    )
    +
    +    from agent.credential_pool import load_pool
    +
    +    pool = load_pool("anthropic")
    +    pool.remove_index(2)
    +
    +    final = json.loads((tmp_path / "hermes" / "auth.json").read_text())
    +    final_ids = [entry["id"] for entry in final["credential_pool"]["anthropic"]]
    +    assert final_ids == ["cred-A"]
    ```
- **#7202: fix(auth): merge credential pool entries on persist, preserve exhaustion on sync**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `8b4c29f0f`, `b02833f32`, `#12360`
- **#19608: fix(auth): preserve concurrently-added credentials on pool rewrite**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `8b4c29f0f`, `#53896`

## Fix status

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