Skip to content

[Bug]: Anthropic provider OAuth integration has multiple issues with credential management and subscription routing #12905

Description

@cryanskl

Bug Description

Summary

After installing Hermes v0.10.0 and configuring the built-in anthropic provider via OAuth (Claude Code credential reuse), I encountered a series of issues that prevent a fresh Max-subscription user from successfully sending any message. Some are clear bugs (Bearer None, token duplication, stale .env state); others are documentation/design gaps around OAuth subscription routing.

This report documents the full user journey from fresh install to working session, with evidence from request dumps.

Environment

  • Hermes Agent: v0.10.0 (2026.4.16) · upstream commit 5157f542
  • Install method: source clone + ./setup-hermes.sh
  • OS: macOS (Apple Silicon, M4 Pro)
  • Python: 3.11.14 (venv from setup-hermes.sh)
  • Claude Code: v2.1.114 (authenticated, working — claude -p "reply with OK" returns OK)
  • Subscription: Claude Max ($200/mo), 20% session / 15% weekly quota used (healthy)
  • Baseline verified: Provider openai-codex works perfectly on the same install — "你好" gets an immediate reply. Only the anthropic provider path exhibits these issues.

Observed Issues (chronological)

Issue 1 — Requests sent to OpenAI-style endpoint with Bearer None auth

On first use with provider: anthropic and no valid token configured, requests are dispatched to https://api.anthropic.com/chat/completions (OpenAI-style path, does not exist on Anthropic) with Authorization: Bearer None — the API key resolver returns None and it gets stringified into the header.

Evidence (from ~/.hermes/sessions/request_dump_20260420_145922_*.json):

{
  "request": {
    "url": "https://api.anthropic.com/chat/completions",
    "headers": {
      "Authorization": "Bearer None",
      "Content-Type": "application/json"
    },
    "body": { "model": "claude-opus-4-6", "messages": [...], "tools": [...] }
  },
  "error": {
    "type": "BadRequestError",
    "status_code": 400,
    "body": "",
    "response_text": ""
  }
}

The empty response body is consistent with the URL path not existing on Anthropic's API. Tested with both claude-opus-4-7 and claude-opus-4-6 — same failure.

Expected: For provider: anthropic, requests should go to https://api.anthropic.com/v1/messages with x-api-key + anthropic-version headers (per Anthropic Messages API convention).

Issue 2 — .env retains malformed token after failed OAuth flow

After hermes model → selecting Anthropic (OAuth), the ~/.hermes/.env was found to contain a duplicated/concatenated token value:
ANTHROPIC_TOKEN=sk-ant-oat01-sk-ant-oat01-<SAME_TOKEN>

The same OAuth token appeared twice, concatenated without a separator. This indicates the credential-write path has no idempotency or deduplication guard.

Issue 3 — hermes model selection does not trigger a real OAuth flow

After cleaning .env and re-running hermes modelAnthropic:

  • Total elapsed time: 36 seconds
  • No browser was opened
  • No "Opening browser for OAuth..." prompt
  • Final output: Default model set to: claude-opus-4-7 (via Anthropic)

The .env then contained a token (sk-ant-oat01-eBkG5faXeSMfo9299...), but that token was the stale value from the previous failed attempt — not a freshly-obtained one. Subsequent message attempts returned:
HTTP 401: Invalid bearer token
{'type': 'error', 'error': {'type': 'authentication_error',
'message': 'Invalid bearer token'}}

This is consistent with hermes model taking a fast path that reuses a cached/stale token instead of initiating a real OAuth flow when the user selects Anthropic.

Issue 4 — Hermes does not read Claude Code's Keychain credentials

Documentation states (providers docs):

"When you choose Anthropic OAuth through hermes model, Hermes prefers Claude Code's own credential store over copying the token into ~/.hermes/.env."

In practice, Claude Code 2.1.114 on macOS stores credentials in the Keychain as a JSON structure:

$ security find-generic-password -s "Claude Code-credentials" -w
{"claudeAiOauth":{"accessToken":"...","refreshToken":"...",...}}

Hermes reads ANTHROPIC_TOKEN as a plain string from ~/.hermes/.env and never queries the Keychain entry. After claude logout + claude /login on the CLI (which updates the Keychain), Hermes continues using the old token in .env, producing 401s until the user manually re-runs setup.

Issue 5 — Error messages reveal known "legacy state" problems

When the 401 occurs, Hermes prints actionable hints that indicate maintainers are aware of the state-management pitfalls:

Legacy cleanup: hermes config set ANTHROPIC_TOKEN ""
Clear stale keys: hermes config set ANTHROPIC_API_KEY ""

Surfacing this as runtime advice — rather than preventing the stale-state situation — suggests the credential lifecycle isn't self-healing. A fresh user hits this on day 1.

Issue 6 — OAuth via subscription does not actually cover Hermes usage

Even after finally getting a valid OAuth token through Hermes, the first real message produced:
HTTP 400: You're out of extra usage.
Add more at claude.ai/settings/usage and keep going.

…despite the Max subscription showing 20% session / 15% weekly / 1% Sonnet-only usage (plenty of quota remaining per Claude Code's own /status view).

Anthropic appears to route third-party OAuth client requests to a separate extra_usage billing pool, which is effectively empty for most users. This isn't a Hermes code bug — but Hermes documentation implies OAuth reuse of Claude Code credentials will leverage the subscription, which is misleading.

Suggestion: Documentation should explicitly state that OAuth credential discovery does not imply subscription billing reuse, and recommend API keys for meaningful Anthropic direct usage.

Suggested Fix Directions

  1. Routing: When provider == "anthropic", route through agent/anthropic_adapter.py with /v1/messages endpoint and x-api-key headers — not the OpenAI-style /chat/completions path.
  2. Null-check at auth boundary: Never emit Authorization: Bearer None. Fail fast with a clear message pointing to hermes setup when no credential resolves.
  3. Credential write idempotency: When updating ANTHROPIC_TOKEN in .env, overwrite rather than append — prevent duplicated/concatenated values.
  4. Real OAuth flow in hermes model: When user selects Anthropic (OAuth) and no valid token exists, open a browser and run the actual OAuth flow. Do not fast-path to "default set" on stale state.
  5. Keychain integration: For Claude Code 2.1.x+, read tokens via security find-generic-password -s "Claude Code-credentials" (on macOS) and parse the JSON claudeAiOauth.accessToken. Do not assume the token is a plain string in .env.
  6. Docs correction: Clarify that Anthropic OAuth does not route through the Max subscription quota; users wanting to use their subscription should be directed to providers that do (Copilot, etc.) or to API keys for direct Anthropic access.

Related Issues

Reproduction (full sequence)

  1. Fresh clone + ./setup-hermes.sh on macOS M4.
  2. hermes setup → choose anthropic provider.
  3. Start hermes, send "hello" → Issue 1 (HTTP 400, empty response).
  4. hermes modelAnthropic (OAuth) → Issue 2/3 (malformed .env, no real OAuth).
  5. Manually clean .env, re-run hermes model → still fast-completes without browser.
  6. First real send after obtaining valid token → Issue 6 (extra usage error despite healthy subscription).

Happy to provide additional dumps or test candidate fixes locally. Let me know which file(s) would be the right place to start if this is something I could help with.

Steps to Reproduce

  1. Fresh clone + ./setup-hermes.sh on macOS (Apple Silicon, M4 Pro).
  2. Run hermes setup and select anthropic as the provider.
  3. Start hermes interactively, send any message (e.g. "hello").
  4. Observe HTTP 400 with empty response body.
  5. cat ~/.hermes/sessions/request_dump_*.json shows the request was dispatched to https://api.anthropic.com/chat/completions (not /v1/messages) with Authorization: Bearer None.
  6. Attempt recovery: clear .env and re-run hermes modelAnthropic (OAuth). Flow completes in ~36 seconds with no browser interaction. .env ends up with either a stale or duplicated token.
  7. Next message returns HTTP 401 "Invalid bearer token" (stale token) or, after setup is rerun enough times to produce a valid token, HTTP 400 "You're out of extra usage" — despite an active Claude Max subscription with 99% quota remaining (per claude /status).

Full reproduction transcript available in the main body below.

Expected Behavior

For a user with Claude Code already authenticated on macOS:

  1. hermes setup → selecting anthropic (OAuth) should trigger a real OAuth flow (open browser, sign in, callback) and write a fresh valid token to credential storage.
  2. First message should succeed without HTTP 400 or 401.
  3. The built-in anthropic provider should dispatch requests to https://api.anthropic.com/v1/messages with x-api-key / anthropic-version headers — not to /chat/completions with OpenAI-style Authorization: Bearer.
  4. When ANTHROPIC_TOKEN is unset or invalid, Hermes should fail fast with a clear, actionable error — not silently emit Authorization: Bearer None.
  5. If Claude Code credentials exist in macOS Keychain (v2.1.x stores them as JSON under Claude Code-credentials), Hermes should parse and use them — as implied by existing documentation.
  6. Documentation should state clearly that OAuth credential discovery does not automatically route requests through the Max subscription quota (Anthropic routes third-party OAuth clients to a separate extra_usage billing pool).

Actual Behavior

Six distinct issues observed during one debugging session (2026-04-20 afternoon, macOS M4 Pro, Hermes v0.10.0 upstream 5157f542):

A. Wrong URL + null-stringified auth. Fresh install with provider: anthropic dispatches to https://api.anthropic.com/chat/completions (OpenAI-style path, does not exist on Anthropic) with Authorization: Bearer None. Response body is empty, status 400.

Evidence from ~/.hermes/sessions/request_dump_20260420_145922_*.json:

{
  "request": {
    "url": "https://api.anthropic.com/chat/completions",
    "headers": {
      "Authorization": "Bearer None",
      "Content-Type": "application/json"
    },
    "body": { "model": "claude-opus-4-6", "messages": [...], "tools": [...] }
  },
  "error": { "status_code": 400, "body": "", "response_text": "" }
}

B. Duplicated token in .env after failed OAuth. After hermes model → Anthropic (OAuth), .env contained the same OAuth token concatenated twice: ANTHROPIC_TOKEN=sk-ant-oat01-<TOKEN>sk-ant-oat01-<SAME_TOKEN>. No separator, no idempotency check on the write.

C. Fast-path "OAuth" without real login. hermes modelAnthropic (OAuth) completed in 36 seconds with no browser, no prompt, just Default model set to: claude-opus-4-7 (via Anthropic). The token written to .env was a stale value, causing HTTP 401 "Invalid bearer token" on next send.

D. Keychain credentials not read. Claude Code 2.1.114 stores tokens in macOS Keychain as JSON (security find-generic-password -s "Claude Code-credentials" -w returns {"claudeAiOauth":{"accessToken":"..."}}). Hermes only reads ANTHROPIC_TOKEN as a plain string from .env, never touches Keychain. Consequence: claude logout + claude /login on the CLI updates Keychain but does not update Hermes — stale token remains, 401s continue.

E. Runtime hints reveal known legacy-state problem. When the 401 fires, Hermes prints:

Legacy cleanup: hermes config set ANTHROPIC_TOKEN ""
Clear stale keys: hermes config set ANTHROPIC_API_KEY ""

This indicates maintainers are aware of the stale-credential problem but surface it as manual remediation advice rather than preventing the state.

F. Docs/behavior mismatch on subscription routing. Once a valid OAuth token was finally obtained, the first real message returned:
HTTP 400: You're out of extra usage. Add more at claude.ai/settings/usage
Even though my Claude Max subscription showed 20% session / 15% weekly / 1% Sonnet-only usage (plenty of quota). Anthropic routes third-party OAuth clients to a separate extra_usage pool that is effectively $0 for most users — not covered by the Max subscription. This is arguably not a Hermes bug in code, but the documentation at https://hermes-agent.nousresearch.com/docs/integrations/providers/ implies OAuth credential reuse means subscription reuse, which is misleading.

Baseline verified: on the same install, switching to provider: openai-codex produces an immediate successful response to "hello". The issues are localized to the anthropic provider code path.

G. Silent auto-detect probes burn quota before first message.

From agent.log, a fresh hermes invocation with provider: anthropic fires 5+ auxiliary API probes before the user sends anything:

```
14:53:31 Vision auto-detect: using main provider anthropic (claude-opus-4-7)
14:53:37 Vision auto-detect: using main provider anthropic (claude-opus-4-7)
14:53:37 Vision auto-detect: using main provider anthropic (claude-opus-4-7)
14:54:50 Vision auto-detect: using main provider anthropic (claude-opus-4-7)
14:54:54 Auxiliary auto-detect: using main provider anthropic (claude-opus-4-7)
```

For OAuth users on a metered "extra usage" tier, this means burning quota before the user types anything. These probes should be cached across restarts.

H. Credential pool continues requests after exhaustion.

When the OAuth token is marked exhausted (401), the credential pool logs no available entries but subsequent code paths still attempt resolve_provider_client: anthropic and produce cascading WARNING lines:

```
15:37:23 credential pool: no available entries (all exhausted or empty)
15:37:23 WARNING: resolve_provider_client: anthropic requested but no Anthropic credentials found
15:37:24 (same pattern, x4)
15:37:27 (same pattern)
```

At this point Hermes should halt and prompt the user to re-authenticate, not continue probing with no credentials.

Affected Component

Agent Core (conversation loop, context compression, memory), Configuration (config.yaml, .env, hermes setup)

Messaging Platform (if gateway-related)

No response

Debug Report

Debug report (sensitive values pre-redacted by `hermes debug share`, manually reviewed — no API keys or tokens exposed):

- Report: https://paste.rs/kj6YN
- agent.log: https://paste.rs/QZVKo
- gateway.log: not available (gateway not running)

Operating System

macOS tahoe 26.3

Python Version

3.11.14

Hermes Version

0.10.0

Additional Logs / Traceback (optional)

Root Cause Analysis (optional)

Multiple related root causes in the Anthropic provider code path:

  1. Provider routing: provider: anthropic appears to resolve through an OpenAI-compatible code path rather than the Anthropic Messages adapter (agent/anthropic_adapter.py). The outgoing URL /chat/completions and Authorization: Bearer header are OpenAI conventions — Anthropic expects /v1/messages + x-api-key + anthropic-version.

  2. Credential null-check: When ANTHROPIC_TOKEN and ANTHROPIC_API_KEY are both unset or empty, the auth layer produces the string "None" (Python None interpolated via f-string) and ships it as Authorization: Bearer None. No guard before the request leaves the process.

  3. Credential write idempotency: The code path that writes ANTHROPIC_TOKEN to .env during OAuth completion appears to append rather than overwrite, producing duplicated concatenated tokens on repeat runs.

  4. OAuth flow completeness in hermes model: Selecting Anthropic (OAuth) in hermes model can complete in ~36 seconds without opening a browser. This suggests it short-circuits to a "token already present" path even when that token is stale/invalid — skipping the actual OAuth refresh.

  5. Credential discovery on macOS: The documented behavior is that Hermes prefers the Claude Code credential store. In Claude Code 2.1.114 that store is the macOS Keychain under service name Claude Code-credentials, containing a JSON blob with claudeAiOauth.accessToken. Hermes reads ANTHROPIC_TOKEN as a plain string from .env instead and never queries the Keychain.

  6. Billing-routing transparency: Anthropic's separate extra_usage billing pool for third-party OAuth clients is an external constraint, but Hermes documentation doesn't surface it — users with $200/month Max subscriptions reasonably expect their subscription to cover Hermes usage and are surprised to hit "out of extra usage" on their first message.

I don't have read access to the full codebase beyond what's in the repo, but I'd guess the files to audit are: agent/anthropic_adapter.py, hermes_cli/auth.py, hermes_cli/setup.py, hermes_cli/model_switch.py. Related issue #7366 also points into anthropic_adapter.py.

Proposed Fix (optional)

Suggested direction, in order of safety:

  1. Null-check at auth boundary (easiest, smallest diff). Before constructing the Authorization header, assert the resolved token is non-None and non-empty. If it is, raise a clear error ("ANTHROPIC_TOKEN not set — run hermes setup") instead of emitting Bearer None. This alone would have turned my confusing HTTP 400 into an actionable error.

  2. Route provider: anthropic through the Messages API adapter. When no custom base_url is set, provider: anthropic should unconditionally use /v1/messages with x-api-key / anthropic-version headers — not the OpenAI-compatible code path.

  3. Make ANTHROPIC_TOKEN writes overwrite-not-append. Deduplicate on write; reject invalid shapes (e.g. concatenated OAuth tokens) at parse time.

  4. Actually run OAuth when the selected provider is anthropic and the current token is invalid. Don't fast-path on presence of a stale .env value.

  5. Add Claude Code Keychain reader for macOS. Parse security find-generic-password -s "Claude Code-credentials" -w output as JSON and use claudeAiOauth.accessToken. Fall back to .env only if Keychain doesn't have an entry.

  6. Docs: add a prominent note in providers.md that OAuth credential sharing does not imply subscription billing reuse; users who want their Max subscription to cover Hermes usage should be directed to Copilot provider (which bills against the Copilot subscription) or API keys for direct Anthropic usage.

Happy to take any of these as a first PR if you point me at the right files. I have a working local dev environment on M4 Pro and a baseline on openai-codex for diffing behavior.

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions