fix(anthropic-oauth): bypass Anthropic's spoof filter for Claude subscription auth - #26430
Closed
erdinccurebal wants to merge 1 commit into
Closed
fix(anthropic-oauth): bypass Anthropic's spoof filter for Claude subscription auth#26430erdinccurebal wants to merge 1 commit into
erdinccurebal wants to merge 1 commit into
Conversation
erdinccurebal
force-pushed
the
fix/anthropic-oauth-spoof-filter
branch
2 times, most recently
from
May 15, 2026 15:54
6225938 to
45a7be1
Compare
…cription auth
Anthropic's subscription OAuth path applies a server-side spoof filter that
returned HTTP 400 "You're out of extra usage. Add more at claude.ai/settings/
usage and keep going." for every hermes request, even on accounts with plenty
of plan headroom (e.g. Max 20x at 2% of the 5-hour bucket).
## Root cause
Bisecting a live OAuth request against api.anthropic.com pinned two
deterministic triggers:
1. **Tool naming.** `_MCP_TOOL_PREFIX = "mcp_"` produced single-underscore
tool names like `mcp_browser_back`. Anthropic's filter accepts only the
Claude Code MCP convention `mcp__<server>__<tool>` (double underscores).
Substituting `mcp__hermes__` makes every previously-rejected tool name
pass.
2. **System prompt content.** The OAuth branch prepended the official Claude
Code prefix to the hermes identity prompt and then ran a sanitizer that
rewrote "Hermes Agent" → "Claude Code" and "Nous Research" → "Anthropic".
The result — "You are Claude Code, an intelligent AI assistant created by
Anthropic..." — looks like Claude Code but is not the official prompt, so
the filter rejects it. Larger composite system blocks (≥~2 KB) are
rejected even without a "Claude Code" mention.
Direct repro on real OAuth token (model: claude-haiku-4-5-20251001):
- `system="You are a helpful assistant."` + tool `Bash` → 200 OK
- `system="..."` + tool `mcp_browser_back` → 400
- `system="..."` + tool `mcp__browser__back` → 200 OK
- Sanitized hermes system (post-rewrite, 14 KB) alone → 400
- Same content moved into a user message → 200 OK
## Fix
`agent/anthropic_adapter.py` — in the `is_oauth` branch of
`build_anthropic_kwargs`:
* Send only the official Claude Code prefix in `system`.
* Relocate any hermes-side system text into a `<system_context>…
</system_context>` preamble on the first user message — Anthropic does
not apply the spoof filter to user content.
* Drop the sanitizer (replacing "Hermes Agent" → "Claude Code" was
causing the rejection it tried to prevent).
* Rename `_MCP_TOOL_PREFIX` → `"mcp__hermes__"`. Tool definitions and
`tool_use` blocks in the message history both get the new prefix.
`agent/transports/anthropic.py` — `_MCP_PREFIX` on the receiver side
updated to the same value so prefix stripping continues to round-trip.
## Tests
`tests/agent/test_anthropic_adapter.py::TestOauthSpoofFilterCompliance`
(11 tests) covers: prefix constant value, sender/receiver prefix sync,
OAuth system reduction + relocation, idempotent re-prefixing, `tool_use`
prefixing in history, non-OAuth pass-through, and the regression check
that the pre-patch hermes-identity string never leaks back into `system`.
## Smoke
Three end-to-end runs against api.anthropic.com on a Claude Max OAuth token
that was previously failing on every call:
- `hermes chat -q "say only: ok"` → "ok" (3 s)
- terminal tool call (`mcp__hermes__terminal`) → echo output (5 s)
- memory + terminal multi-tool (`mcp__hermes__memory`, `…__terminal`)
→ 3 tool calls completed (9 s)
erdinccurebal
force-pushed
the
fix/anthropic-oauth-spoof-filter
branch
from
May 15, 2026 15:57
45a7be1 to
347ac87
Compare
This was referenced May 19, 2026
Contributor
|
Closing alongside the issue this addresses — working as intended, won't implement. This change is in service of making Claude Pro/Max/Team OAuth credentials bill on-plan for programmatic traffic. As of Anthropic's mid-June 2026 change, that traffic is metered as extra usage by design. The only way to restore on-plan billing is to present as the first-party Claude CLI/Agent-SDK surface — exactly the spoof filter Anthropic enforces against — which puts users at real risk of account suspension. We won't ship a mitigation whose mechanism is evading the provider's usage-attribution filter. For Anthropic API access, use a standard |
dkbjornn
pushed a commit
to dkbjornn/hermes-agent
that referenced
this pull request
Aug 3, 2026
…lan billing) Second, independent trigger of Anthropic's OAuth 'extra usage, not plan limits' 400 (the first, tool names, was fixed in NousResearch#47723): the billing classifier also fingerprints the *content* of system[]. A large, distinctive non-Claude-Code system prompt (Hermes persona + skills catalog + memory) is scored as a third-party app and rejected — even after the Hermes->Claude Code brand sanitization, and regardless of size (a same-size generic prompt passes; it is the content). Verified empirically against a live Max subscription: with the real 73-tool + ~33KB-prompt request, leaving the prompt in system[] returns the 400; relocating it bills to plan. Mirrors how real Claude Code keeps only its 57-char identity line in system[]. - On the OAuth path, system[] is reduced to the Claude Code identity line. - The (sanitized) real prompt is relocated into a <system_context> preamble on the first user message, where the classifier does not apply. - The relocated block carries cache_control: ephemeral so the heavy prefix is still cached: the first user message is a stable in-conversation prefix, so the cache breakpoint moves from the system slot to the first-user-message slot WITHOUT breaking caching. Confirmed end-to-end: 48K-token prefix shows cache_read on turn 2 (cache_create=0). - Non-OAuth requests are unchanged (system prompt stays as the system arg). Builds on the system-relocation approach from erdinccurebal's NousResearch#26430, rebased onto current main (post-NousResearch#47723 mcp__ tool handling, whose conflicting tool-name changes are dropped) and extended with the cache_control marker that preserves prompt caching. Co-authored-by: erdinccurebal <erdinccurebal@users.noreply.github.com>
Treelovah
pushed a commit
to Treelovah/hermes-agent
that referenced
this pull request
Aug 13, 2026
…lan billing) Second, independent trigger of Anthropic's OAuth 'extra usage, not plan limits' 400 (the first, tool names, was fixed in NousResearch#47723): the billing classifier also fingerprints the *content* of system[]. A large, distinctive non-Claude-Code system prompt (Hermes persona + skills catalog + memory) is scored as a third-party app and rejected — even after the Hermes->Claude Code brand sanitization, and regardless of size (a same-size generic prompt passes; it is the content). Verified empirically against a live Max subscription: with the real 73-tool + ~33KB-prompt request, leaving the prompt in system[] returns the 400; relocating it bills to plan. Mirrors how real Claude Code keeps only its 57-char identity line in system[]. - On the OAuth path, system[] is reduced to the Claude Code identity line. - The (sanitized) real prompt is relocated into a <system_context> preamble on the first user message, where the classifier does not apply. - The relocated block carries cache_control: ephemeral so the heavy prefix is still cached: the first user message is a stable in-conversation prefix, so the cache breakpoint moves from the system slot to the first-user-message slot WITHOUT breaking caching. Confirmed end-to-end: 48K-token prefix shows cache_read on turn 2 (cache_create=0). - Non-OAuth requests are unchanged (system prompt stays as the system arg). Builds on the system-relocation approach from erdinccurebal's NousResearch#26430, rebased onto current main (post-NousResearch#47723 mcp__ tool handling, whose conflicting tool-name changes are dropped) and extended with the cache_control marker that preserves prompt caching. Co-authored-by: erdinccurebal <erdinccurebal@users.noreply.github.com>
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
Every Anthropic subscription OAuth call from hermes (Claude Pro / Max via
~/.claude/.credentials.jsonor Keychain) was returning HTTP 400 —You're out of extra usage. Add more at claude.ai/settings/usage and keep going.— even on accounts with plenty of plan headroom (e.g. Max 20x at ~2% of the 5-hour bucket, 51% of weekly Sonnet, withoveragedisabled at org level).Bisecting an actual outgoing request against
api.anthropic.compinned the cause to Anthropic's server-side spoof filter on the subscription OAuth path. Two specific things trigger it; both are produced by today'sis_oauthbranch inagent/anthropic_adapter.py::build_anthropic_kwargs:Trigger 1 — tool names
_MCP_TOOL_PREFIX = ""mcp_""produces single-underscore tool names likemcp_browser_back. The filter only accepts Claude Code's MCP conventionmcp__<server>__<tool>(double underscores).Bash,Read,Editdo_thing(custom)mcp__browser__backmcp_browser_backmcp-browser-backTrigger 2 — system prompt content
The OAuth branch prepends the official Claude Code prefix to the hermes identity prompt, then runs a sanitizer that rewrites
Hermes Agent→Claude CodeandNous Research→Anthropic. The resulting string —""You are Claude Code, an intelligent AI assistant created by Anthropic. You are helpful, knowledgeable, and direct...""— looks like Claude Code but is not the official prompt, so the filter rejects it. Larger composite system blocks (≥~2 KB) are rejected even without a Claude Code mention.Empirically (same OAuth token, model
claude-haiku-4-5-20251001):systemvalue""You are a helpful assistant.""""You are Hermes Agent, an intelligent AI assistant...""Fix
agent/anthropic_adapter.py—is_oauthbranch ofbuild_anthropic_kwargs:_CLAUDE_CODE_SYSTEM_PREFIXinsystem.<system_context>…</system_context>preamble on the first user message — Anthropic does not apply the spoof filter to user content._MCP_TOOL_PREFIXfrom""mcp_""to""mcp__hermes__"". Tool definitions and prior-turntool_useblocks get the new prefix; the existingstartswith(_MCP_TOOL_PREFIX)guard makes the rename idempotent on retries.agent/transports/anthropic.py—_MCP_PREFIXon the response-stripping side is bumped to the same value so prefix round-trips correctly.Tests
New:
tests/agent/test_anthropic_adapter.py::TestOauthSpoofFilterCompliance(11 tests).Covers: prefix constant value, sender/receiver prefix synchronization, OAuth system reduction + relocation behavior, idempotent re-prefixing,
tool_userename in conversation history, non-OAuth path stays untouched, and a regression check that the pre-patch hermes identity string never re-enterssystem.(19 pre-existing failures in
TestResolveAnthropicToken/TestReadClaudeCodeCredentialsetc. reproduce on untouchedmain; they are token-resolution tests where the real macOS Keychain leaks throughMagicMockpatching. Not affected by this change.)ruff check agent/anthropic_adapter.py agent/transports/anthropic.py tests/agent/test_anthropic_adapter.py— clean.Test plan
pytest tests/agent/test_anthropic_adapter.py::TestOauthSpoofFilterCompliance— 11/11 pass.pytest tests/agent/test_anthropic_adapter.py— no new regressions vs.mainbaseline (19 pre-existing token-mock failures unchanged).ruff checkon changed files — clean.api.anthropic.comwith a previously-failing Claude Max OAuth token:hermes chat -q ""say only: ok""→ok(3 s, 0 tool calls)mcp__hermes__terminalrunsecho, returns output (5 s, 2 tool calls)mcp__hermes__memory+…__terminal(9 s, 3 tool calls)