fix(terminal): stop stripping CLAUDE_CODE_OAUTH_TOKEN from spawned subprocesses (#56935)

CLAUDE_CODE_OAUTH_TOKEN is set and owned by the user's Claude Code
install (subscription OAuth), not a Hermes-managed inference
credential — Claude subscription auth is not a working Hermes provider
path. Blocklisting it broke agent-spawned claude CLIs: with no token in
the child env, claude fell through to the shared macOS Keychain /
~/.claude/.credentials.json store and, on auth failure, cleared it —
logging the user out of their interactive Claude sessions and the
desktop app.

Exempt it from _HERMES_PROVIDER_ENV_BLOCKLIST (it arrives via the
anthropic registry entry, so discard explicitly with rationale).
ANTHROPIC_API_KEY / ANTHROPIC_TOKEN and every other provider credential
remain stripped, and the GHSA-rhgp-j443-p4rf fail-closed passthrough
guard is unchanged for everything still on the blocklist.

Fixes #55878
This commit is contained in:
Teknium 2026-07-02 02:13:30 -07:00 committed by GitHub
parent 8b1ad38ecb
commit 14639ded77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 29 additions and 6 deletions

View file

@ -80,7 +80,6 @@ class TestProviderEnvBlocklist:
must also be blocked not just the hand-written extras."""
registry_vars = {
"ANTHROPIC_TOKEN": "ant-tok",
"CLAUDE_CODE_OAUTH_TOKEN": "cc-tok",
"ZAI_API_KEY": "zai-key",
"Z_AI_API_KEY": "z-ai-key",
"GLM_API_KEY": "glm-key",
@ -326,11 +325,18 @@ class TestBlocklistCoverage:
def test_registry_vars_are_in_blocklist(self):
"""Every api_key_env_var and base_url_env_var from PROVIDER_REGISTRY
must appear in the blocklist ensures no drift."""
must appear in the blocklist ensures no drift.
CLAUDE_CODE_OAUTH_TOKEN is the one deliberate exemption: it is owned
by the user's Claude Code install, not Hermes (#55878).
"""
from hermes_cli.auth import PROVIDER_REGISTRY
exempt = {"CLAUDE_CODE_OAUTH_TOKEN"}
for pconfig in PROVIDER_REGISTRY.values():
for var in pconfig.api_key_env_vars:
if var in exempt:
continue
assert var in _HERMES_PROVIDER_ENV_BLOCKLIST, (
f"Registry var {var} (provider={pconfig.id}) missing from blocklist"
)
@ -371,11 +377,19 @@ class TestBlocklistCoverage:
)
def test_extra_auth_vars_covered(self):
"""Non-registry auth vars (ANTHROPIC_TOKEN, CLAUDE_CODE_OAUTH_TOKEN)
must also be in the blocklist."""
extras = {"ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"}
"""Non-registry auth vars (ANTHROPIC_TOKEN) must also be in the
blocklist."""
extras = {"ANTHROPIC_TOKEN"}
assert extras.issubset(_HERMES_PROVIDER_ENV_BLOCKLIST)
def test_claude_code_oauth_token_is_inheritable(self):
"""CLAUDE_CODE_OAUTH_TOKEN is owned by the user's Claude Code install
(subscription OAuth), not a Hermes inference credential. Stripping it
made agent-spawned ``claude`` fall through to the shared Keychain /
~/.claude credential store and clobber the user's interactive login
on auth failure (#55878). It must stay inheritable."""
assert "CLAUDE_CODE_OAUTH_TOKEN" not in _HERMES_PROVIDER_ENV_BLOCKLIST
def test_non_registry_provider_vars_are_in_blocklist(self):
extras = {
"GOOGLE_API_KEY",

View file

@ -152,7 +152,6 @@ def _build_provider_env_blocklist() -> frozenset:
"ANTHROPIC_BASE_URL",
"ANTHROPIC_API_KEY",
"ANTHROPIC_TOKEN",
"CLAUDE_CODE_OAUTH_TOKEN",
"LLM_MODEL",
"GOOGLE_API_KEY",
# Path to a GCP service-account JSON, not a bare key, so
@ -212,6 +211,16 @@ def _build_provider_env_blocklist() -> frozenset:
"GATEWAY_RELAY_SECRET",
"GATEWAY_RELAY_DELIVERY_KEY",
})
# CLAUDE_CODE_OAUTH_TOKEN is deliberately NOT stripped. It is set and
# owned by the user's Claude Code install (subscription OAuth), not a
# Hermes-managed inference credential — Claude subscription auth is not a
# working Hermes provider path. Stripping it broke agent-spawned
# ``claude`` CLIs: the child fell through to the shared macOS Keychain /
# ``~/.claude/.credentials.json`` store and, on auth failure, cleared it,
# logging the user out of their interactive Claude sessions (#55878).
# It arrives via the registry loop above (anthropic api_key_env_vars),
# so remove it explicitly.
blocked.discard("CLAUDE_CODE_OAUTH_TOKEN")
return frozenset(blocked)