Skip to content

feat(agent): pin_anthropic_token — let a static setup-token win over a refreshable Claude Code credential - #82095

Open
adurham wants to merge 2 commits into
NousResearch:mainfrom
adurham:upstream-pr/pin-anthropic-token
Open

feat(agent): pin_anthropic_token — let a static setup-token win over a refreshable Claude Code credential#82095
adurham wants to merge 2 commits into
NousResearch:mainfrom
adurham:upstream-pr/pin-anthropic-token

Conversation

@adurham

@adurham adurham commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

resolve_anthropic_token() already prefers a refreshable Claude Code credential (~/.claude/.credentials.json or macOS Keychain) over a static ANTHROPIC_TOKEN/CLAUDE_CODE_OAUTH_TOKEN env var, so a stale persisted token never silently blocks auto-refresh. That's the right default — but it has no escape hatch for a real, common setup: a dedicated long-lived claude setup-token for Hermes, on a machine where the user also runs interactive claude CLI logins for daily-driver Claude Code work.

macOS Keychain isn't scoped by CLAUDE_CONFIG_DIR, so both credentials collide in the same Keychain slot — every interactive claude login silently overwrites Hermes's dedicated setup-token, and there was no config lever to tell resolve_anthropic_token() "keep using my static token on this machine regardless of what the interactive login just wrote."

Fix

Adds an opt-in agent.pin_anthropic_token: true config key (default false, preserving the existing safety-net behavior). When set, the resolver skips the refreshable-credential preference check for both env var sources (ANTHROPIC_TOKEN and CLAUDE_CODE_OAUTH_TOKEN) and returns the static token directly.

Test plan

  • 4 new tests in tests/agent/test_anthropic_adapter.py::TestResolveAnthropicToken:
    • pin_anthropic_token: true makes the static token win over a refreshable credential
    • pin_anthropic_token: false (default) is unaffected — regression guard confirming the opt-in doesn't change existing behavior
    • a config-load exception degrades to the safe default (prefer refresh) rather than propagating
  • The pin=true test confirmed to fail against pre-fix code via git stash.
  • ruff check clean.
  • 97/97 passed in test_anthropic_adapter.py.
  • 93/93 passed across adjacent credential-resolution test files: test_runtime_provider_resolution.py, test_credential_pool_oat_authtype.py, test_minimax_provider.py, test_anthropic_token_scope_isolation.py, test_anthropic_third_party_oauth_guard.py, test_hermetic_side_effect_guards.py.

Searched existing issues/PRs first — no existing report or competing PR for this specific gap.

…a refreshable Claude Code credential

resolve_anthropic_token() already prefers a refreshable Claude Code
credential (~/.claude/.credentials.json or macOS Keychain) over a static
ANTHROPIC_TOKEN/CLAUDE_CODE_OAUTH_TOKEN env var, so a stale persisted
token never silently blocks auto-refresh. That's the right default, but
it has no escape hatch for a real, common setup: a dedicated long-lived
`claude setup-token` for Hermes, on a machine where the user also runs
interactive `claude` CLI logins for daily-driver Claude Code work.

macOS Keychain isn't scoped by CLAUDE_CONFIG_DIR, so both credentials
collide in the same Keychain slot -- every interactive `claude` login
silently overwrites Hermes's dedicated setup-token, and there was no way
to tell resolve_anthropic_token() "no, keep using my static token on this
machine regardless of what the interactive login just wrote."

Adds an opt-in `agent.pin_anthropic_token: true` config key (default
false, preserving the existing safety-net behavior). When set, the
resolver skips the refreshable-credential preference check for both env
var sources and returns the static token directly.

4 new tests in tests/agent/test_anthropic_adapter.py::TestResolveAnthropicToken:
pin=true makes the static token win over a refreshable credential,
pin=false (default) is unaffected (regression guard), and a config-load
exception degrades to the safe default rather than propagating.

Verified: ruff clean; 97/97 in test_anthropic_adapter.py; 93/93 across
adjacent credential-resolution test files (runtime_provider_resolution,
credential_pool_oat_authtype, minimax_provider, anthropic_token_scope_isolation,
anthropic_third_party_oauth_guard, hermetic_side_effect_guards); the new
pin=true test confirmed to fail against pre-fix code via git stash.
adurham pushed a commit to adurham/hermes-agent that referenced this pull request Aug 9, 2026
Filed agent.pin_anthropic_token as PR NousResearch#82095 -- verified upstream's
resolve_anthropic_token()/_prefer_refreshable_claude_code_token() already
exist in the converged shape, no competing PR found. Checked the other
two Tier-1 candidates (trafilatura web_extract, tool_search core-toolset
deferral) and found both already have real open competing PRs upstream;
per standing rule, did not file duplicates for either.
@alt-glitch alt-glitch added type/feature New feature or request comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API area/auth Authentication, OAuth, credential pools area/config Config system, migrations, profiles P2 Medium — degraded but workaround exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 9, 2026
@spfcraze

spfcraze commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary:
resolve_anthropic_token() calls _pin_static_anthropic_token() — and through it load_config() — on every invocation, although the helper's docstring says the config read is paid only "unless the user opts in".

Problems:

  • pin_static = _pin_static_anthropic_token() runs before the env-var checks in agent/anthropic_adapter.py, so load_config() is called on every resolution, including when no static token is set and the pin cannot apply.
  • load_config() is the deepcopy variant — hermes_cli/config.py documents its cache-hit cost at ~265us per call and provides load_config_readonly() for read-only callers; the diff reads config via load_config().

Solution:
Read the pin inside the if token: / if cc_token: branches — the only places pin_static is consulted — and use load_config_readonly() for the lookup.


Checked against 636d4b8 — the tip of upstream-pr/pin-anthropic-token when this was written — and 61515e8, main at the same moment.

…y present

Per AI triage review on PR NousResearch#82095:
1. _pin_static_anthropic_token() (and its config read) was called
   unconditionally at the top of resolve_anthropic_token() on every
   invocation, even when neither ANTHROPIC_TOKEN nor
   CLAUDE_CODE_OAUTH_TOKEN is set and the pin can never apply. Moved the
   call into the #1/#2 branches where it's actually consulted, so the
   config read is skipped entirely for the common case.
2. Switched from load_config() (deepcopy variant) to
   load_config_readonly() -- this helper only reads the flag, never
   mutates it, and load_config_readonly() skips the ~265us deepcopy
   load_config() pays on every cache hit.

Updated the 3 existing pin_anthropic_token tests' mock targets to
load_config_readonly, and added 2 new regression tests: one asserting
_pin_static_anthropic_token() is never called when no static env token
is present (confirmed fails pre-fix via git stash), and one asserting
the readonly loader is used instead of the mutable one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools area/config Config system, migrations, profiles comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/anthropic Anthropic native Messages API sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants