From 2fafcf42e8ea62987bfa47e592a0ad3b18cfca6e Mon Sep 17 00:00:00 2001 From: xwp Date: Fri, 10 Apr 2026 15:08:41 +0800 Subject: [PATCH] fix(auth): gate Claude Code credential seeding behind explicit provider config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _seed_from_singletons('anthropic') now checks is_provider_explicitly_configured('anthropic') before reading ~/.claude/.credentials.json. Without this, the auxiliary client fallback chain silently discovers and uses Claude Code tokens when the user's primary provider key is invalid — consuming their Claude Max subscription quota without consent. Follows the same gating pattern as PR #4210 (setup wizard gate) but applied to the credential pool seeding path. Co-Authored-By: Claude Opus 4.6 (1M context) --- agent/credential_pool.py | 11 +++++++++++ tests/agent/test_credential_pool.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/agent/credential_pool.py b/agent/credential_pool.py index f6c6375788e..0ce187503ce 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -1059,6 +1059,17 @@ def _seed_from_singletons(provider: str, entries: List[PooledCredential]) -> Tup auth_store = _load_auth_store() if provider == "anthropic": + # Only auto-discover external credentials (Claude Code, Hermes PKCE) + # when the user has explicitly configured anthropic as their provider. + # Without this gate, auxiliary client fallback chains silently read + # ~/.claude/.credentials.json without user consent. See PR #4210. + try: + from hermes_cli.auth import is_provider_explicitly_configured + if not is_provider_explicitly_configured("anthropic"): + return changed, active_sources + except ImportError: + pass + from agent.anthropic_adapter import read_claude_code_credentials, read_hermes_oauth_credentials for source_name, creds in ( diff --git a/tests/agent/test_credential_pool.py b/tests/agent/test_credential_pool.py index 797597dd70c..de6ffba5c57 100644 --- a/tests/agent/test_credential_pool.py +++ b/tests/agent/test_credential_pool.py @@ -567,6 +567,7 @@ def test_singleton_seed_does_not_clobber_manual_oauth_entry(tmp_path, monkeypatc monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False) monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False) + monkeypatch.setattr("hermes_cli.auth.is_provider_explicitly_configured", lambda pid: True) _write_auth_store( tmp_path, { @@ -1043,3 +1044,30 @@ def test_release_lease_decrements_counter(tmp_path, monkeypatch): pool.release_lease("cred-1") assert pool._active_leases.get("cred-1", 0) == 0 + + +def test_load_pool_does_not_seed_claude_code_when_anthropic_not_configured(tmp_path, monkeypatch): + """Claude Code credentials must not be auto-seeded when the user never selected anthropic.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) + _write_auth_store(tmp_path, {"version": 1, "credential_pool": {}}) + + # Claude Code credentials exist on disk + monkeypatch.setattr( + "agent.anthropic_adapter.read_claude_code_credentials", + lambda: {"accessToken": "sk-ant...oken", "refreshToken": "rt", "expiresAt": 9999999999999}, + ) + monkeypatch.setattr( + "agent.anthropic_adapter.read_hermes_oauth_credentials", + lambda: None, + ) + # User configured kimi-coding, NOT anthropic + monkeypatch.setattr( + "hermes_cli.auth.is_provider_explicitly_configured", + lambda pid: pid == "kimi-coding", + ) + + from agent.credential_pool import load_pool + pool = load_pool("anthropic") + + # Should NOT have seeded the claude_code entry + assert pool.entries() == []