# NousResearch/hermes-agent · Auth credential removal undermined by automatic re-seeding

## Open now

- Nothing open on GitHub.

These issues and fixes center on credential removal operations being silently reversed by automatic re-import from sources like ~/.codex/auth.json or singleton auth stores. Work spans CLI auth commands, dashboard pool deletes, and the openai-codex provider.

## TL;DR

- 9 member(s): 0 open issue(s), 4 closed issue(s), 0 open PR(s), 2 merged PR(s), 3 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-67429`. 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.17`: 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

- #9528: openai-codex provider impossible to remove — auto-restore from ~/.codex/auth.json
- #12747: auth remove openai-codex can re-seed a new device_code credential from ~/.codex/auth.json
- #13967: auth remove re-seeds manually added OAuth credentials from singleton auth store
- #55217: Bug: Dashboard credential pool delete succeeds but entries reappear on refresh

## 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)
    ```
- **#13989: fix(auth_commands): normalise manual: prefix before singleton comparisons**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `2c69b3eca`, `2c69b3eca8187013223677985c013e714e5c1d70`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#67429: fix(credentials): pool entry delete via API sticks — suppress source re-seeding (#55217)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/web_server.py`, `tests/hermes_cli/test_dashboard_admin_endpoints.py`
  - fix hunks:
  - `hermes_cli/web_server.py`

    ```diff
    @@ -12498,6 +12498,7 @@ async def add_credential_pool_entry(body: CredentialPoolAdd):
             load_pool,
             PooledCredential,
             AUTH_TYPE_API_KEY,
    +        CUSTOM_POOL_PREFIX,
             SOURCE_MANUAL,
         )
     
    ```

    ```diff
    @@ -12519,6 +12520,23 @@ async def add_credential_pool_entry(body: CredentialPoolAdd):
                 access_token=[redacted:secret-assignment]
             )
             pool.add_entry(entry)
    +        # Re-adding a credential is an explicit re-engagement signal: lift
    +        # every suppression for this provider so a source deleted earlier
    +        # (via DELETE below or `hermes auth remove`) can seed again.
    +        # Mirrors the `hermes auth add` behaviour in auth_commands.py.
    +        if not provider.startswith(CUSTOM_POOL_PREFIX):
    +            try:
    +                from hermes_cli.auth import (
    +                    _load_auth_store,
    +                    unsuppress_credential_source,
    +                )
    +                suppressed = _load_auth_store().get("suppressed_sources", {})
    +                for src in list(suppressed.get(provider, []) or []):
    +                    unsuppress_credential_source(provider, src)
    +            except Exception:
    +                _log.exception("unsuppress after pool add failed (non-fatal)")
    +    except HTTPException:
    +        raise
         except Exception as exc:
             _log.exception("POST /api/credentials/pool failed")
             raise HTTPException(status_code=400, detail=str(exc)) from exc
    ```

    ```diff
    @@ -12527,8 +12545,20 @@ async def add_credential_pool_entry(body: CredentialPoolAdd):
     
     @app.delete("/api/credentials/pool/{provider}/{index}")
     async def remove_credential_pool_entry(provider: str, index: int):
    -    """Remove a pool entry.  ``index`` is 1-based (matches the list response)."""
    +    """Remove a pool entry.  ``index`` is 1-based (matches the list response).
    +
    +    Removal must be sticky (#55217): ``load_pool()`` re-seeds entries from
    +    their backing source (.env var, OAuth singleton file, custom-provider
    +    config) on every call, so deleting only the pool row silently reverts on
    +    the next dashboard refresh.  We dispatch through the same RemovalStep
    +    registry the CLI ``hermes auth remove`` uses: each source cleans up its
    +    external state and suppresses ``(provider, source)`` so the seeders skip
    +    it.  Manual entries have no registered step — nothing external to clean,
    +    no suppression needed (they aren't re-seeded).
    +    """
         from agent.credential_pool import load_pool
    +    from agent.credential_sources import find_removal_step
    +    from hermes_cli.auth import suppress_credential_source
     
         provider = (provider or "").strip().lower()
         try:
    ```

    ```diff
    @@ -12539,7 +12569,36 @@ async def remove_credential_pool_entry(provider: str, index: int):
             raise HTTPException(status_code=400, detail=str(exc)) from exc
         if removed is None:
             raise HTTPException(status_code=404, detail="No pool entry at that index")
    -    return {"ok": True, "provider": provider, "count": len(pool.entries())}
    +
    +    cleaned: List[str] = []
    +    hints: List[str] = []
    +    step = find_removal_step(provider, removed.source or "")
    +    if step is not None:
    +        try:
    +            result = step.remove_fn(provider, removed)
    +            cleaned = list(result.cleaned)
    +            hints = list(result.hints)
    +            if result.suppress:
    +                suppress_credential_source(provider, removed.source)
    +        except Exception:
    +            # Cleanup is best-effort, but suppression is the actual bug fix —
    +            # without it the entry resurrects on the next load_pool().  Apply
    +            # it even when source-specific cleanup blew up.
    +            _log.exception(
    +                "credential source cleanup failed for %s/%s; suppressing anyway",
    +                provider, removed.source,
    +            )
    +            try:
    +                suppress_credential_source(provider, removed.source)
    +            except Exception:
    +                _log.exception("suppress_credential_source failed")
    +    return {
    +        "ok": True,
    +        "provider": provider,
    +        "count": len(pool.entries()),
    +        "cleaned": cleaned,
    +        "hints": hints,
    +    }
     
     
     # ---------------------------------------------------------------------------
    ```
  - `tests/hermes_cli/test_dashboard_admin_endpoints.py`

    ```diff
    @@ -253,6 +253,104 @@ def test_empty_body_rejected(self):
             )
             assert r.status_code == 400
     
    +    def test_env_seeded_delete_stays_deleted(self):
    +        """#55217: DELETE must suppress the source or load_pool() resurrects it.
    +
    +        load_pool() re-seeds from ~/.hermes/.env on every call, so removing
    +        just the pool row silently reverts on the next dashboard refresh.
    +        The endpoint must mirror `hermes auth remove`: clean up the backing
    +        source and suppress (provider, source).
    +        """
    +        from agent.credential_pool import load_pool
    +        from hermes_cli.auth import is_source_suppressed
    +        from hermes_cli.config import save_env_value
    +
    +        fake_key = "sk-or-" + "x" * 20  # constructed, never a real key shape
    +        save_env_value("OPENROUTER_API_KEY", fake_key)
    +
    +        entries = load_pool("openrouter").entries()
    +        assert [e.source for e in entries] == ["env:OPENROUTER_API_KEY"]
    +
    +        r = self.client.delete("/api/credentials/pool/openrouter/1")
    +        assert r.status_code == 200
    +
    +        # Suppressed exactly like the CLI removal path.
    +        assert is_source_suppressed("openrouter", "env:OPENROUTER_API_KEY")
    +
    +        # Even if the backing var comes back (shell export, another process
    +        # rewriting .env), the removal must stay sticky.
    +        save_env_value("OPENROUTER_API_KEY", fake_key)
    +        assert load_pool("openrouter").entries() == []
    +        assert self.client.get("/api/credentials/pool").json()["providers"] == []
    +
    +    def test_post_readd_lifts_suppression(self):
    +        """Re-adding via POST is an explicit re-engagement — suppressions lift.
    +
    +        Mirrors `hermes auth add`, which clears every suppression for the
    +        provider so a user who deleted a credential and re-adds one isn't
    +        silently blocked from env re-seeding.
    +        """
    +        from agent.credential_pool import load_pool
    +        from hermes_cli.auth import is_source_suppressed
    +        from hermes_cli.config import save_env_value
    +
    +        fake_key = "sk-or-" + "y" * 20
    +        save_env_value("OPENROUTER_API_KEY", fake_key)
    +        load_pool("openrouter")
    +        assert self.client.delete("/api/credentials/pool/openrouter/1").status_code == 200
    +        assert is_source_suppressed("openrouter", "env:OPENROUTER_API_KEY")
    +
    +        r = self.client.post(
    +            "/api/credentials/pool",
    +            json={"provider": "openrouter", "api_key": "sk-or-" + "z" * 20},
    +        )
    +        assert r.status_code == 200
    +        assert not is_source_suppressed("openrouter", "env:OPENROUTER_API_KEY")
    +
    +        # Key back in .env + suppression lifted → env entry seeds alongside
    +        # the manual one.
    +        save_env_value("OPENROUTER_API_KEY", fake_key)
    +        sources = sorted(e.source for e in load_pool("openrouter").entries())
    +        assert sources == ["env:OPENROUTER_API_KEY", "manual"]
    +
    +    def test_manual_delete_adds_no_suppression(self):
    +        """Manual entries aren't re-seeded — CLI parity: no suppression marker."""
    +        from hermes_cli.auth import _load_auth_store
    +
    +        self.client.post(
    +            "/api/credentials/pool",
    +            json={"provider": "openrouter", "api_key": "sk-or-" + "m" * 20},
    +        )
    +        assert self.client.delete("/api/credentials/pool/openrouter/1").status_code == 200
    +        suppressed = _load_auth_store().get("suppressed_sources", {})
    +        assert not suppressed.get("openrouter")
    +
    +        # Immediate re-add works.
    +        r = self.client.post(
    +            "/api/credentials/pool",
    +            json={"provider": "openrouter", "api_key": "sk-or-" + "n" * 20},
    +        )
    +        assert r.status_code == 200 and r.json()["count"] == 1
    +
    +    def test_delete_does_not_clobber_other_providers(self):
    +        """Deleting one provider's env entry leaves other providers' rows alone."""
    +        from agent.credential_pool import load_pool
    +        from hermes_cli.auth import _load_auth_store, read_credential_pool
    +        from hermes_cli.config import save_env_value
    +
    +        self.client.post(
    +            "/api/credentials/pool",
    +            json={"provider": "anthropic", "api_key": "sk-ant-" + "k" * 20},
    +        )
    +        save_env_value("OPENROUTER_API_KEY", "sk-or-" + "q" * 20)
    +        load_pool("openrouter")
    +
    +        assert self.client.delete("/api/credentials/pool/openrouter/1").status_code == 200
    +
    +        assert len(read_credential_pool("anthropic")) == 1
    +        suppressed = _load_auth_store().get("suppressed_sources", {})
    +        assert list(suppressed.keys()) == ["openrouter"]
    +
     
     class TestMemoryEndpoints:
         @pytest.fixture(autouse=True)
    ```
- **#13330: fix(auth): suppress codex CLI reseed on remove**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `b02833f32d`, `#12747`, `#12360`, `#12360`, `#12747`
- **#55219: fix(dashboard): suppress credential source on pool delete to prevent resurrection**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#55217`, `#67429`

## Fix status

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