fix(anthropic): OAuth token endpoint UA must be axios/, not claude-code/ (login 429, #48534) - #57922
Closed
mssteuer wants to merge 1 commit into
Closed
fix(anthropic): OAuth token endpoint UA must be axios/, not claude-code/ (login 429, #48534)#57922mssteuer wants to merge 1 commit into
mssteuer wants to merge 1 commit into
Conversation
…in 429)
hermes auth add anthropic failed 100% with HTTP 429 at token exchange while
Claude Code /login succeeded through the same client_id/redirect/scope. The
discriminator is the User-Agent on the /v1/oauth/token request.
Empirically verified against platform.claude.com (garbage code, nothing burned):
claude-code/2.1.200 (external, cli) -> 429 rate_limit (Hermes, blocked)
claude-code/2.1.200 -> 429 rate_limit
Mozilla/5.0 -> 429 rate_limit
axios/1.7.9 -> 400 invalid_grant (reached validation)
node / empty / Claude-User (...) -> 400 invalid_grant
Anthropic rate-limits token-endpoint requests whose UA starts with claude-code/
(the anti-abuse net for Max-sub-as-API-key). The real Claude Code CLI exchanges
the auth code with a bare axios client (axios/<ver>), NOT its claude-code/
inference UA. Hermes hardcoded claude-code/{ver} (external, cli) on all three
OAuth-token call sites (login exchange + two refresh paths) and tripped the net.
Fix: shared _OAUTH_TOKEN_USER_AGENT = 'axios/1.7.9' on the token endpoint only.
The INFERENCE client (build_anthropic_kwargs) KEEPS claude-code/ + x-app: cli —
that fingerprint is required there and is not throttled on /v1/messages.
Also (this commit): match Claude Code's authorize host (claude.com/cai) +
redirect_uri (platform.claude.com) + full scope; isolate two _refresh_oauth_token
tests from live ~/.claude creds; update UA regression tests to assert the
token endpoint uses a non-claude-code UA while inference still uses claude-code/.
Verified: Hermes' own request path now returns 400 (past the 429 wall) instead
of 429. 181 tests pass.
Post-update guard: carried commit + patch 0005.
mssteuer
force-pushed
the
fix/oauth-token-ua-axios-429
branch
from
July 3, 2026 20:55
ebaca44 to
77c9b9f
Compare
kshitijk4poor
added a commit
to kshitijk4poor/hermes-agent
that referenced
this pull request
Jul 4, 2026
…in isolation) Two review findings on the NousResearch#57922 salvage: 1. Stale inline comment at the login-exchange site still claimed the token endpoint uses the claude-code/ UA prefix and 404s claude-cli/ — now contradicts the axios/ fix. Repointed it at _OAUTH_TOKEN_USER_AGENT. 2. The inherited Path.home test isolation on the three TestRefreshOauthToken tests only stubbed the ~/.claude *file* source, not the macOS Keychain. _refresh_oauth_token re-reads read_claude_code_credentials() (keychain first) in its adopt-already-refreshed branch, so on any macOS dev/CI runner with real Claude Code creds the branch short-circuits and the 3 tests fail. Stub read_claude_code_credentials -> None so the tests are hermetic. (The remaining TestResolveAnthropicToken/TestResolveWithRefresh/TestRunOauthSetupToken failures on macOS are the same pre-existing keychain-leak class on origin/main, unrelated to this OAuth-UA fix, and pass in CI — left out of scope.)
kshitijk4poor
pushed a commit
that referenced
this pull request
Jul 4, 2026
…in 429, #48534) hermes auth add anthropic fails 100% at token exchange with HTTP 429 while Claude Code /login succeeds through the same client_id/redirect/scope. The discriminator is the User-Agent on the /v1/oauth/token request. Verified live against platform.claude.com (throwaway code, nothing burned): claude-code/2.1.200 (external, cli) -> 429 rate_limit (Hermes, blocked) Mozilla/5.0 -> 429 rate_limit axios/1.7.9 -> 400 invalid_grant (reached validation) node / empty / SDK-style UAs -> 400 invalid_grant Anthropic now rate-limits token-endpoint requests whose UA starts with claude-code/ (the anti-abuse net for Max-sub-as-API-key). This is the same prefix-block shape that #48534 first hit on claude-cli/, then #56263 dodged by switching to claude-code/ — which held ~2 weeks and is now blocked too. Bumping _CLAUDE_CODE_VERSION_FALLBACK cannot help; the gate is prefix-based. Fix: shared _OAUTH_TOKEN_USER_AGENT (axios/) on the token endpoint only — the two refresh POSTs (refresh_anthropic_oauth_pure) and the login exchange POST (run_hermes_oauth_login_pure). The real Claude Code CLI exchanges the auth code with a bare axios client, NOT its claude-code/ inference UA. The INFERENCE client (build_anthropic_kwargs, /v1/messages) is deliberately left on claude-code/ + x-app: cli — that fingerprint is required there and is NOT throttled on the messages API. Two endpoints, opposite UA requirements. Also isolate two _refresh_oauth_token tests from live ~/.claude creds and update the UA regression tests to assert the split (token endpoint uses a non-claude-code UA while inference keeps claude-code/). Verified E2E: Hermes' own login path now returns 400 (past the 429 wall) instead of 429, using the real _OAUTH_TOKEN_USER_AGENT constant against the live platform.claude.com token endpoint. Salvaged from #57922 (authorize-host + scope changes dropped as non-load-bearing; they only add a redirect hop back to claude.ai and the UA fix alone clears 429).
kshitijk4poor
added a commit
that referenced
this pull request
Jul 4, 2026
…in isolation) Two review findings on the #57922 salvage: 1. Stale inline comment at the login-exchange site still claimed the token endpoint uses the claude-code/ UA prefix and 404s claude-cli/ — now contradicts the axios/ fix. Repointed it at _OAUTH_TOKEN_USER_AGENT. 2. The inherited Path.home test isolation on the three TestRefreshOauthToken tests only stubbed the ~/.claude *file* source, not the macOS Keychain. _refresh_oauth_token re-reads read_claude_code_credentials() (keychain first) in its adopt-already-refreshed branch, so on any macOS dev/CI runner with real Claude Code creds the branch short-circuits and the 3 tests fail. Stub read_claude_code_credentials -> None so the tests are hermetic. (The remaining TestResolveAnthropicToken/TestResolveWithRefresh/TestRunOauthSetupToken failures on macOS are the same pre-existing keychain-leak class on origin/main, unrelated to this OAuth-UA fix, and pass in CI — left out of scope.)
Collaborator
|
Thanks @mssteuer — your UA-split diagnosis was exactly right. Salvaged and merged via #58178. I kept the load-bearing part of your fix (the
Your authorship is preserved in the merge (rebase). Follow-up commit fixed a stale inline comment and made the |
habarmc1223-sudo
pushed a commit
to habarmc1223-sudo/hermes-agent-fluxmem
that referenced
this pull request
Jul 8, 2026
…in 429, NousResearch#48534) hermes auth add anthropic fails 100% at token exchange with HTTP 429 while Claude Code /login succeeds through the same client_id/redirect/scope. The discriminator is the User-Agent on the /v1/oauth/token request. Verified live against platform.claude.com (throwaway code, nothing burned): claude-code/2.1.200 (external, cli) -> 429 rate_limit (Hermes, blocked) Mozilla/5.0 -> 429 rate_limit axios/1.7.9 -> 400 invalid_grant (reached validation) node / empty / SDK-style UAs -> 400 invalid_grant Anthropic now rate-limits token-endpoint requests whose UA starts with claude-code/ (the anti-abuse net for Max-sub-as-API-key). This is the same prefix-block shape that NousResearch#48534 first hit on claude-cli/, then NousResearch#56263 dodged by switching to claude-code/ — which held ~2 weeks and is now blocked too. Bumping _CLAUDE_CODE_VERSION_FALLBACK cannot help; the gate is prefix-based. Fix: shared _OAUTH_TOKEN_USER_AGENT (axios/) on the token endpoint only — the two refresh POSTs (refresh_anthropic_oauth_pure) and the login exchange POST (run_hermes_oauth_login_pure). The real Claude Code CLI exchanges the auth code with a bare axios client, NOT its claude-code/ inference UA. The INFERENCE client (build_anthropic_kwargs, /v1/messages) is deliberately left on claude-code/ + x-app: cli — that fingerprint is required there and is NOT throttled on the messages API. Two endpoints, opposite UA requirements. Also isolate two _refresh_oauth_token tests from live ~/.claude creds and update the UA regression tests to assert the split (token endpoint uses a non-claude-code UA while inference keeps claude-code/). Verified E2E: Hermes' own login path now returns 400 (past the 429 wall) instead of 429, using the real _OAUTH_TOKEN_USER_AGENT constant against the live platform.claude.com token endpoint. Salvaged from NousResearch#57922 (authorize-host + scope changes dropped as non-load-bearing; they only add a redirect hop back to claude.ai and the UA fix alone clears 429).
habarmc1223-sudo
pushed a commit
to habarmc1223-sudo/hermes-agent-fluxmem
that referenced
this pull request
Jul 8, 2026
…in isolation) Two review findings on the NousResearch#57922 salvage: 1. Stale inline comment at the login-exchange site still claimed the token endpoint uses the claude-code/ UA prefix and 404s claude-cli/ — now contradicts the axios/ fix. Repointed it at _OAUTH_TOKEN_USER_AGENT. 2. The inherited Path.home test isolation on the three TestRefreshOauthToken tests only stubbed the ~/.claude *file* source, not the macOS Keychain. _refresh_oauth_token re-reads read_claude_code_credentials() (keychain first) in its adopt-already-refreshed branch, so on any macOS dev/CI runner with real Claude Code creds the branch short-circuits and the 3 tests fail. Stub read_claude_code_credentials -> None so the tests are hermetic. (The remaining TestResolveAnthropicToken/TestResolveWithRefresh/TestRunOauthSetupToken failures on macOS are the same pre-existing keychain-leak class on origin/main, unrelated to this OAuth-UA fix, and pass in CI — left out of scope.)
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…in 429, NousResearch#48534) hermes auth add anthropic fails 100% at token exchange with HTTP 429 while Claude Code /login succeeds through the same client_id/redirect/scope. The discriminator is the User-Agent on the /v1/oauth/token request. Verified live against platform.claude.com (throwaway code, nothing burned): claude-code/2.1.200 (external, cli) -> 429 rate_limit (Hermes, blocked) Mozilla/5.0 -> 429 rate_limit axios/1.7.9 -> 400 invalid_grant (reached validation) node / empty / SDK-style UAs -> 400 invalid_grant Anthropic now rate-limits token-endpoint requests whose UA starts with claude-code/ (the anti-abuse net for Max-sub-as-API-key). This is the same prefix-block shape that NousResearch#48534 first hit on claude-cli/, then NousResearch#56263 dodged by switching to claude-code/ — which held ~2 weeks and is now blocked too. Bumping _CLAUDE_CODE_VERSION_FALLBACK cannot help; the gate is prefix-based. Fix: shared _OAUTH_TOKEN_USER_AGENT (axios/) on the token endpoint only — the two refresh POSTs (refresh_anthropic_oauth_pure) and the login exchange POST (run_hermes_oauth_login_pure). The real Claude Code CLI exchanges the auth code with a bare axios client, NOT its claude-code/ inference UA. The INFERENCE client (build_anthropic_kwargs, /v1/messages) is deliberately left on claude-code/ + x-app: cli — that fingerprint is required there and is NOT throttled on the messages API. Two endpoints, opposite UA requirements. Also isolate two _refresh_oauth_token tests from live ~/.claude creds and update the UA regression tests to assert the split (token endpoint uses a non-claude-code UA while inference keeps claude-code/). Verified E2E: Hermes' own login path now returns 400 (past the 429 wall) instead of 429, using the real _OAUTH_TOKEN_USER_AGENT constant against the live platform.claude.com token endpoint. Salvaged from NousResearch#57922 (authorize-host + scope changes dropped as non-load-bearing; they only add a redirect hop back to claude.ai and the UA fix alone clears 429).
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…in isolation) Two review findings on the NousResearch#57922 salvage: 1. Stale inline comment at the login-exchange site still claimed the token endpoint uses the claude-code/ UA prefix and 404s claude-cli/ — now contradicts the axios/ fix. Repointed it at _OAUTH_TOKEN_USER_AGENT. 2. The inherited Path.home test isolation on the three TestRefreshOauthToken tests only stubbed the ~/.claude *file* source, not the macOS Keychain. _refresh_oauth_token re-reads read_claude_code_credentials() (keychain first) in its adopt-already-refreshed branch, so on any macOS dev/CI runner with real Claude Code creds the branch short-circuits and the 3 tests fail. Stub read_claude_code_credentials -> None so the tests are hermetic. (The remaining TestResolveAnthropicToken/TestResolveWithRefresh/TestRunOauthSetupToken failures on macOS are the same pre-existing keychain-leak class on origin/main, unrelated to this OAuth-UA fix, and pass in CI — left out of scope.)
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…in 429, NousResearch#48534) hermes auth add anthropic fails 100% at token exchange with HTTP 429 while Claude Code /login succeeds through the same client_id/redirect/scope. The discriminator is the User-Agent on the /v1/oauth/token request. Verified live against platform.claude.com (throwaway code, nothing burned): claude-code/2.1.200 (external, cli) -> 429 rate_limit (Hermes, blocked) Mozilla/5.0 -> 429 rate_limit axios/1.7.9 -> 400 invalid_grant (reached validation) node / empty / SDK-style UAs -> 400 invalid_grant Anthropic now rate-limits token-endpoint requests whose UA starts with claude-code/ (the anti-abuse net for Max-sub-as-API-key). This is the same prefix-block shape that NousResearch#48534 first hit on claude-cli/, then NousResearch#56263 dodged by switching to claude-code/ — which held ~2 weeks and is now blocked too. Bumping _CLAUDE_CODE_VERSION_FALLBACK cannot help; the gate is prefix-based. Fix: shared _OAUTH_TOKEN_USER_AGENT (axios/) on the token endpoint only — the two refresh POSTs (refresh_anthropic_oauth_pure) and the login exchange POST (run_hermes_oauth_login_pure). The real Claude Code CLI exchanges the auth code with a bare axios client, NOT its claude-code/ inference UA. The INFERENCE client (build_anthropic_kwargs, /v1/messages) is deliberately left on claude-code/ + x-app: cli — that fingerprint is required there and is NOT throttled on the messages API. Two endpoints, opposite UA requirements. Also isolate two _refresh_oauth_token tests from live ~/.claude creds and update the UA regression tests to assert the split (token endpoint uses a non-claude-code UA while inference keeps claude-code/). Verified E2E: Hermes' own login path now returns 400 (past the 429 wall) instead of 429, using the real _OAUTH_TOKEN_USER_AGENT constant against the live platform.claude.com token endpoint. Salvaged from NousResearch#57922 (authorize-host + scope changes dropped as non-load-bearing; they only add a redirect hop back to claude.ai and the UA fix alone clears 429).
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…in isolation) Two review findings on the NousResearch#57922 salvage: 1. Stale inline comment at the login-exchange site still claimed the token endpoint uses the claude-code/ UA prefix and 404s claude-cli/ — now contradicts the axios/ fix. Repointed it at _OAUTH_TOKEN_USER_AGENT. 2. The inherited Path.home test isolation on the three TestRefreshOauthToken tests only stubbed the ~/.claude *file* source, not the macOS Keychain. _refresh_oauth_token re-reads read_claude_code_credentials() (keychain first) in its adopt-already-refreshed branch, so on any macOS dev/CI runner with real Claude Code creds the branch short-circuits and the 3 tests fail. Stub read_claude_code_credentials -> None so the tests are hermetic. (The remaining TestResolveAnthropicToken/TestResolveWithRefresh/TestRunOauthSetupToken failures on macOS are the same pre-existing keychain-leak class on origin/main, unrelated to this OAuth-UA fix, and pass in CI — left out of scope.)
This was referenced Aug 3, 2026
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…in 429, NousResearch#48534) hermes auth add anthropic fails 100% at token exchange with HTTP 429 while Claude Code /login succeeds through the same client_id/redirect/scope. The discriminator is the User-Agent on the /v1/oauth/token request. Verified live against platform.claude.com (throwaway code, nothing burned): claude-code/2.1.200 (external, cli) -> 429 rate_limit (Hermes, blocked) Mozilla/5.0 -> 429 rate_limit axios/1.7.9 -> 400 invalid_grant (reached validation) node / empty / SDK-style UAs -> 400 invalid_grant Anthropic now rate-limits token-endpoint requests whose UA starts with claude-code/ (the anti-abuse net for Max-sub-as-API-key). This is the same prefix-block shape that NousResearch#48534 first hit on claude-cli/, then NousResearch#56263 dodged by switching to claude-code/ — which held ~2 weeks and is now blocked too. Bumping _CLAUDE_CODE_VERSION_FALLBACK cannot help; the gate is prefix-based. Fix: shared _OAUTH_TOKEN_USER_AGENT (axios/) on the token endpoint only — the two refresh POSTs (refresh_anthropic_oauth_pure) and the login exchange POST (run_hermes_oauth_login_pure). The real Claude Code CLI exchanges the auth code with a bare axios client, NOT its claude-code/ inference UA. The INFERENCE client (build_anthropic_kwargs, /v1/messages) is deliberately left on claude-code/ + x-app: cli — that fingerprint is required there and is NOT throttled on the messages API. Two endpoints, opposite UA requirements. Also isolate two _refresh_oauth_token tests from live ~/.claude creds and update the UA regression tests to assert the split (token endpoint uses a non-claude-code UA while inference keeps claude-code/). Verified E2E: Hermes' own login path now returns 400 (past the 429 wall) instead of 429, using the real _OAUTH_TOKEN_USER_AGENT constant against the live platform.claude.com token endpoint. Salvaged from NousResearch#57922 (authorize-host + scope changes dropped as non-load-bearing; they only add a redirect hop back to claude.ai and the UA fix alone clears 429).
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…in isolation) Two review findings on the NousResearch#57922 salvage: 1. Stale inline comment at the login-exchange site still claimed the token endpoint uses the claude-code/ UA prefix and 404s claude-cli/ — now contradicts the axios/ fix. Repointed it at _OAUTH_TOKEN_USER_AGENT. 2. The inherited Path.home test isolation on the three TestRefreshOauthToken tests only stubbed the ~/.claude *file* source, not the macOS Keychain. _refresh_oauth_token re-reads read_claude_code_credentials() (keychain first) in its adopt-already-refreshed branch, so on any macOS dev/CI runner with real Claude Code creds the branch short-circuits and the 3 tests fail. Stub read_claude_code_credentials -> None so the tests are hermetic. (The remaining TestResolveAnthropicToken/TestResolveWithRefresh/TestRunOauthSetupToken failures on macOS are the same pre-existing keychain-leak class on origin/main, unrelated to this OAuth-UA fix, and pass in CI — left out of scope.)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #48534 (current regression). The OAuth token-exchange 429 is back: Anthropic has extended the same anti-abuse gate that once blocked
claude-cli/(404) to theclaude-code/UA prefix (429). The fix merged in #56263 (claude-cli/→claude-code/) worked for ~2 weeks and is now blocked again.hermes auth add anthropicfails 100% at token exchange on currentmain:Root cause
The three
/v1/oauth/tokencall sites inagent/anthropic_adapter.pysendUser-Agent: claude-code/{ver} (external, cli). Anthropic now rate-limits (429) any token-endpoint request whose UA starts withclaude-code/— the Max-subscription-as-API-key anti-abuse net. It's prefix-based, not version-gated, so_CLAUDE_CODE_VERSION_FALLBACKbumps can't help (same shape as the originalclaude-cli/finding in this issue).The genuine Claude Code CLI exchanges the auth code with a bare axios client (
axios/<ver>). Itsclaude-code/+x-app: clifingerprint is attached only to the inference path (/v1/messages), which is a separate code path and is not throttled there. The two endpoints have opposite UA requirements.Empirical isolation (garbage code, nothing burned)
A 429 is applied before code validation, so the exchange can be probed with a throwaway code —
400 invalid_grantmeans the request was accepted and only the fake code rejected (a real code → 200):claude-code/2.1.200 (external, cli)(currentmain)claude-code/2.1.200Mozilla/5.0axios/1.7.9node/ empty /Claude-User (claude-code/…)Fix
Introduce
_OAUTH_TOKEN_USER_AGENT = "axios/1.7.9"and apply it at all three token-endpoint call sites:refresh_anthropic_oauth_pure(refresh POST)build_anthropic_clientOAuth refresh POSTrun_hermes_oauth_login_pure(login exchange POST)The inference path (
build_anthropic_kwargs) is deliberately left untouched — it keepsuser-agent: claude-code/{ver} (external, cli)+x-app: cli, which is required on/v1/messagesand is not throttled there. Two endpoints, opposite UA requirements.Tests
tests/agent/test_anthropic_oauth_ua_prefix.pyis updated so the invariant now encodes the split instead of freezing a single UA string:claude-code/(norclaude-cli/)claude-code/Two
_refresh_oauth_tokentests that leaked the machine's live~/.claude/.credentials.json(they never patchedPath.home, so they adopted a real token instead of returningNone) are isolated withmonkeypatch.setattr("agent.anthropic_adapter.Path.home", lambda: tmp_path), matching the siblingtest_successful_refresh.Verified end-to-end
With the fix applied (inference UA untouched),
hermes auth add anthropiccompletes:Note for maintainers
This UA gate is a moving target on Anthropic's side (
claude-cli/→claude-code/→ nowaxios/needed on the token endpoint). If it flips again, the diagnostic is: fire the exchange with a throwaway code and compare the HTTP status per-UA (429 = blocked, 400 = reached validation), then match whatever the current Claude Code CLI's OAuth client actually sends.