Skip to content

Fix Anthropic OAuth stale token shadowing - #5101

Closed
mattsegura wants to merge 5 commits into
NousResearch:mainfrom
mattsegura:fix/anthropic-oauth-stale-token-shadowing
Closed

Fix Anthropic OAuth stale token shadowing#5101
mattsegura wants to merge 5 commits into
NousResearch:mainfrom
mattsegura:fix/anthropic-oauth-stale-token-shadowing

Conversation

@mattsegura

@mattsegura mattsegura commented Apr 4, 2026

Copy link
Copy Markdown

Fixes #6346

Summary

Fix Anthropic OAuth flows getting stuck behind stale ANTHROPIC_TOKEN values.

Changes

  • prefer Hermes-native PKCE in hermes model Anthropic OAuth flow
  • prefer refreshable Hermes/Claude file-backed creds over static env setup tokens
  • persist Hermes Anthropic OAuth creds when using hermes auth add anthropic
  • route status/doctor Anthropic checks through token resolution logic
  • add regression coverage for stale-token shadowing

Reproduction (before fix)

  1. Put a stale or invalid Anthropic OAuth token in your environment, for example in ~/.hermes/.env:
    ANTHROPIC_TOKEN=stale-env-token
    or export it in your shell:
    export ANTHROPIC_TOKEN=stale-env-token
  2. Re-auth Anthropic successfully through a refreshable OAuth flow so Hermes now has valid file-backed credentials.
    For example:
    hermes auth add anthropic
    or use the Anthropic login flow in hermes model.
  3. Run a command that should use the newly refreshed Anthropic credentials, such as:
    hermes status
    hermes doctor
    or start Hermes on an Anthropic model.
  4. Before this fix, the stale ANTHROPIC_TOKEN could still win during credential resolution and shadow the fresh OAuth credentials.

Observed pre-fix behavior:

  • Anthropic requests still fail with 401/unauthorized-style auth errors even after reauth
  • status/doctor can report Anthropic as broken even though valid refreshable creds exist
  • hermes auth add anthropic did not consistently persist the right credential source for later use

Why

Users could end up with an invalid or stale ANTHROPIC_TOKEN in .env, causing Anthropic 401s even after reauth. The refreshable credential stores were not consistently preferred or persisted across flows.

Validation

pytest -q -o addopts='' tests/test_anthropic_oauth_flow.py tests/test_anthropic_adapter.py tests/test_credential_pool.py tests/test_auth_commands.py tests/test_anthropic_provider_persistence.py
python3 -m compileall agent/anthropic_adapter.py hermes_cli/main.py hermes_cli/auth_commands.py hermes_cli/status.py hermes_cli/doctor.py

Result: 148 passed

@trevorgordon981

Copy link
Copy Markdown
Contributor

Good diagnosis and a clean fix. The stale-token-shadowing bug is a real footgun — users doing OAuth twice on Anthropic ending up with an expired ANTHROPIC_TOKEN wedged into .env that permanently shadows their refreshable credential file. I've hit similar auth precedence bugs on other systems; the fix here is structurally correct.

What I like:

  • _prefer_refreshable_file_backed_token loops through stores in a clear priority order (Hermes OAuth → Claude Code) and correctly returns None to fall through when no refreshable alternative exists
  • auth_add_command persists to BOTH stores, eliminating the divergence that caused this bug
  • status and doctor now route through resolve_anthropic_token() instead of raw env lookups — this fixes the confusing case where status displayed one token but requests used a different one

Three things to consider before merge:

1. except Exception swallows real failures in doctor.py:

try:
    from agent.anthropic_adapter import resolve_anthropic_token
    anthropic_key = resolve_anthropic_token()
except Exception:
    anthropic_key = os.getenv("ANTHROPIC_TOKEN") or os.getenv("ANTHROPIC_API_KEY")

If resolve_anthropic_token() ever throws — e.g., corrupted credential file, permission error — doctor silently degrades to the old behavior and the user won't know the resolution path is broken. Log the exception at logger.warning level so operators can debug. Same applies to the equivalent block in status.py.

2. Migration hint for users with stale env tokens:

After this PR, a user with ANTHROPIC_TOKEN=sk-ant-oat01-stale-value in .env plus a valid Hermes OAuth file will see authentication working again — but they won't know the env var is being silently ignored. On the next auth failure (if the credential file expires or is deleted), they'll hit the stale token and be confused why auth broke again.

Consider a one-shot info log when _prefer_refreshable_file_backed_token returns a non-env token:

logger.info(
    "Using refreshable %s credentials; your .env contains a static ANTHROPIC_TOKEN "
    "that is being ignored. You can remove it to avoid confusion.",
    label,
)

Or surface this in hermes doctor output.

3. Atomicity of dual-store writes in auth_add_command:

anthropic_mod._save_hermes_oauth_credentials(...)
anthropic_mod._write_claude_code_credentials(...)

If the first write succeeds and the second fails (disk full, permission error), the stores diverge. Not catastrophic — _prefer_refreshable_file_backed_token will pick whichever exists — but worth either try/except around each call with a warning log, or a comment acknowledging the design choice.

Test coverage question: does the test suite cover the case where both stores have expired creds AND refresh fails for both? The fallback path to ANTHROPIC_API_KEY / raw env tokens is load-bearing here.

Ship it once the doctor/status exception handling is addressed — everything else is polish.

@mattsegura

mattsegura commented Apr 5, 2026

Copy link
Copy Markdown
Author

Addressed the review blocker around the broad exception fallbacks in doctor/status.

  • added warning-level logging when resolve_anthropic_token() fails
  • preserved the existing env-var fallback path
  • added targeted tests covering the warning + fallback behavior in both commands
  • added a regression test covering the load-bearing fallback path when both refreshable Anthropic stores are expired, both refresh attempts fail, and resolution falls back to ANTHROPIC_API_KEY

Validation:
source venv/bin/activate && python -m pytest tests/test_anthropic_adapter.py tests/test_auth_commands.py tests/hermes_cli/test_doctor.py tests/hermes_cli/test_status.py tests/hermes_cli/test_status_model_provider.py -q -n 0

@ajmeese7

Copy link
Copy Markdown

@mattsegura FSA there are some merge conflicts on this branch that'll probably need to get touched up before this is hopefully reviewed/merged

@teknium1

Copy link
Copy Markdown
Contributor

Thanks @mattsegura. Closing — heavy overlap with @5park1e's #12971 which landed via #15175 (commit e110677).

#12971 addresses the same core bug (stale OAuth token → hermes model skips re-auth menu) with a narrower implementation: existing_is_stale_oauth check in _model_flow_anthropic when the only credential is an expired sk-ant- OAuth token with no cc_creds fallback. It also adds macOS Keychain support for Claude Code ≥2.1.114 credentials, which is complementary.

Your PR's 12-file scope included touches to credential_pool.py, doctor.py, status.py that weren't in #12971. If those pieces still fix bugs on current main, feel free to resubmit as focused follow-up PRs. Apologies for closing after the long wait — the Anthropic auth codebase has churned substantially since April 5 (PRs #3107, #4126, #11485, #13427) so a direct cherry-pick wasn't viable.

@teknium1 teknium1 closed this Apr 24, 2026
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround provider/anthropic Anthropic native Messages API area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard labels Apr 24, 2026
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 comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard P1 High — major feature broken, no workaround provider/anthropic Anthropic native Messages API type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Anthropic runtime still prefers stale ANTHROPIC_TOKEN over refreshable OAuth after reauth

5 participants