Skip to content

fix(anthropic-oauth): bypass Anthropic's spoof filter for Claude subscription auth - #26430

Closed
erdinccurebal wants to merge 1 commit into
NousResearch:mainfrom
erdinccurebal:fix/anthropic-oauth-spoof-filter
Closed

fix(anthropic-oauth): bypass Anthropic's spoof filter for Claude subscription auth#26430
erdinccurebal wants to merge 1 commit into
NousResearch:mainfrom
erdinccurebal:fix/anthropic-oauth-spoof-filter

Conversation

@erdinccurebal

Copy link
Copy Markdown

Summary

Every Anthropic subscription OAuth call from hermes (Claude Pro / Max via ~/.claude/.credentials.json or 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, with overage disabled at org level).

Bisecting an actual outgoing request against api.anthropic.com pinned the cause to Anthropic's server-side spoof filter on the subscription OAuth path. Two specific things trigger it; both are produced by today's is_oauth branch in agent/anthropic_adapter.py::build_anthropic_kwargs:

Trigger 1 — tool names

_MCP_TOOL_PREFIX = ""mcp_"" produces single-underscore tool names like mcp_browser_back. The filter only accepts Claude Code's MCP convention mcp__<server>__<tool> (double underscores).

Tool name Result
Bash, Read, Edit 200 OK
do_thing (custom) 200 OK
mcp__browser__back 200 OK
mcp_browser_back 400
mcp-browser-back 200 OK

Trigger 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 AgentClaude Code and Nous ResearchAnthropic. 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):

system value Result
Official prefix only 200 OK
""You are a helpful assistant."" 200 OK
""You are Hermes Agent, an intelligent AI assistant..."" 200 OK (short, ~640 B)
Sanitized hermes prompt (post-rewrite, 14 KB) 400
Same 14 KB moved into the first user message 200 OK

Fix

agent/anthropic_adapter.pyis_oauth branch of build_anthropic_kwargs:

  • Send only _CLAUDE_CODE_SYSTEM_PREFIX in system.
  • Move 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 (the substitution it performed was producing the rejection it was trying to prevent).
  • Rename _MCP_TOOL_PREFIX from ""mcp_"" to ""mcp__hermes__"". Tool definitions and prior-turn tool_use blocks get the new prefix; the existing startswith(_MCP_TOOL_PREFIX) guard makes the rename idempotent on retries.

agent/transports/anthropic.py_MCP_PREFIX on 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_use rename in conversation history, non-OAuth path stays untouched, and a regression check that the pre-patch hermes identity string never re-enters system.

tests/agent/test_anthropic_adapter.py::TestOauthSpoofFilterCompliance — 11 passed
tests/agent/test_anthropic_adapter.py (full)                          — 142 passed

(19 pre-existing failures in TestResolveAnthropicToken / TestReadClaudeCodeCredentials etc. reproduce on untouched main; they are token-resolution tests where the real macOS Keychain leaks through MagicMock patching. 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. main baseline (19 pre-existing token-mock failures unchanged).
  • ruff check on changed files — clean.
  • Live smoke tests against api.anthropic.com with a previously-failing Claude Max OAuth token:
    • hermes chat -q ""say only: ok""ok (3 s, 0 tool calls)
    • tool call: mcp__hermes__terminal runs echo, returns output (5 s, 2 tool calls)
    • memory + terminal multi-tool: mcp__hermes__memory + …__terminal (9 s, 3 tool calls)

@erdinccurebal
erdinccurebal force-pushed the fix/anthropic-oauth-spoof-filter branch 2 times, most recently from 6225938 to 45a7be1 Compare May 15, 2026 15:54
…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
erdinccurebal force-pushed the fix/anthropic-oauth-spoof-filter branch from 45a7be1 to 347ac87 Compare May 15, 2026 15:57
@daimon-nous daimon-nous Bot added type/bug Something isn't working P1 High — major feature broken, no workaround 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 labels May 15, 2026
@teknium1

Copy link
Copy Markdown
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 x-api-key API key (ANTHROPIC_API_KEY). Thanks for the work and the detailed tracing.

@teknium1 teknium1 closed this Jun 18, 2026
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>
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 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.

2 participants