fix(anthropic-oauth): refresh Claude Code wire fingerprint to 2.1.112 - #3
Merged
Merged
Conversation
The Claude Code 2.1.112 native binary ships a noticeably different
request shape than what we were emitting. Captured via mitmproxy under
~/.hermes/reference/oauth-audit-2026-04-16/ and diffed. Anthropic's
billing classifier appears to weigh the cumulative drift score; a
sufficiently stale set of signals tips the request into the API / Extra
Usage billing lane instead of the Max weekly subscription lane.
Wire-level changes (each one observed on the live CC 2.1.112 wire):
* Add system[0] x-anthropic-billing-header text block with cc_version
("major.minor.patch.build"), cc_entrypoint=sdk-cli, and a per-call
cch=<5-hex> opaque checksum. This is the single highest-signal item
the audit found.
* Update _CLAUDE_CODE_SYSTEM_PREFIX to the new identity phrasing
"You are a Claude agent, built on Anthropic's Claude Agent SDK."
Old wording ("Claude Code, Anthropic's official CLI") is no longer
what the real CLI sends.
* Bump anthropic-beta set to the current 9-entry list:
context-1m-2025-08-07, context-management-2025-06-27,
prompt-caching-scope-2026-01-05, advisor-tool-2026-03-01,
advanced-tool-use-2025-11-20, effort-2025-11-24
(in addition to the existing claude-code, oauth, interleaved-thinking).
Drop fine-grained-tool-streaming from the OAuth set — CC 2.1.112
no longer sends it on opus calls.
* Add ?beta=true query string to /v1/messages calls (extra_query).
* Add per-request X-Claude-Code-Session-Id (stable per process)
and x-client-request-id (per call) UUID headers.
* Add anthropic-dangerous-direct-browser-access: true to default headers.
* Bump _CLAUDE_CODE_VERSION_FALLBACK to 2.1.112 and pair with
_CLAUDE_CODE_BUILD_FALLBACK = 148.
* Switch User-Agent to capital case (was sending duplicate "User-Agent"
+ "user-agent" pair); switch entrypoint label cli -> sdk-cli to match
what CC actually sends.
* Add metadata.user_id with {device_id, account_uuid, session_id}
populated from ~/.claude.json. When that file is missing we omit
the field rather than fake it.
* Upgrade cache_control on the largest user-supplied system block to
{type: ephemeral, ttl: '1h', scope: 'global'} to match CC's form.
Coverage:
152 adapter tests pass, including a new TestClaudeCodeFingerprintParity
class that locks in each of the above items individually so a future
upstream refactor can't silently break parity. Pre-existing 4 failing
tests (TestReadCodexAccessToken / test_explicit_reset_timestamp) are
unrelated and fail on main as well.
Out of scope (deferred to a follow-up if needed):
X-Stainless-Lang / X-Stainless-Runtime / X-Stainless-Package-Version
still betray the Python SDK identity. Spoofing those requires either
patching the Anthropic SDK's headers at construction or building a
bespoke httpx transport for OAuth requests. We'll evaluate whether
this set of fixes is sufficient before going there.
mssteuer
force-pushed
the
fix/oauth-fingerprint-refresh-2026-04-16
branch
from
April 16, 2026 23:36
aa03dc4 to
ae1fb14
Compare
…table The previous table rewrote "Hermes*" and "OpenClaw" tokens to "Claude Code" / "claude-code" in outbound traffic to Anthropic. This shifted 'content fingerprinting' onto the legitimate repo/file names the agent itself operates on, producing ghost rewrites for every PR title, URL, and tool_result that referenced the real repo name. The observable symptom: gh CLI, git remotes, and file paths all appeared renamed to the masked form in tool output, making the repo impossible to locate from the agent side. PR #3's wire-fingerprint refresh (billing header, identity phrase, metadata.user_id, cache_control ttl, session headers) covers the structural signals the classifier actually keys on, so we can drop the content-level brand masking without reopening meaningful classification risk. Tradeoff is documented; remaining drift is the Stainless SDK telemetry (python/CPython vs js/node), which requires a separate fix.
The 2026-04-16 cleanup dropped 8 brand-masking RHS entries that were causing ghost rewrites of legitimate inbound content. Three tests were asserting the old behaviour (brand tokens get rewritten to generic forms) and now correctly fail. - test_text_helper_replaces_branding is renamed to test_text_helper_branding_tokens_pass_through and inverted: brand tokens MUST pass through unchanged, while the upstream-owner fingerprint is still swapped. - test_text_helper_two_word_replaced_before_bare is renamed to test_text_helper_longest_first_ordering_cron_family. The original invariant guarded an entry pair that no longer exists; the cron/task family carries the longest-first invariant now, so we verify the table ORDER there explicitly plus an idempotence round-trip. - test_build_kwargs_sanitizes_system switches the dirty owner token to the actual LHS of the surviving owner-swap rule and verifies the brand token Hermes passes through the sanitizer untouched.
mssteuer
added a commit
that referenced
this pull request
Apr 17, 2026
….112
Captured Claude Code 2.1.112 sending these fields on opus-4-7 requests
(reference: ~/.hermes/reference/oauth-audit-2026-04-17-verify/):
thinking: {"type":"adaptive"}
output_config: {"effort":"xhigh"}
context_management: {"edits":[{"type":"clear_thinking_20251015",
"keep":"all"}]}
temperature: (absent)
tool_choice: (absent when caller doesn't request one)
Hermes was sending:
thinking: {"type":"enabled","budget_tokens":4000}
temperature: 1
tool_choice: {"type":"auto"}
(no output_config, no context_management)
Root cause: `_supports_adaptive_thinking` only matched "4-6" / "4.6", so
opus-4-7 fell through to the legacy manual-thinking branch written for
4.5 and earlier. opus-4-7 now rejects `temperature` with a 400
"temperature is deprecated for this model" — caught in journalctl after
PR #3 deploy but not blocking as errors were retried. More importantly,
the broken shape was a fingerprint-drift signal the Anthropic billing
classifier reads, keeping OAuth traffic in the Extra Usage lane instead
of the Max weekly lane.
Changes (agent/anthropic_adapter.py):
- `_supports_adaptive_thinking`: match "4-7" / "4.7" in addition to 4.6.
Lowercase input for case-insensitive match.
- `ADAPTIVE_EFFORT_MAP`: "xhigh" → "xhigh" (identity). Was "xhigh" →
"max" — CC sends "xhigh" literally.
- `_ANTHROPIC_OUTPUT_LIMITS`: explicit "claude-opus-4-7" → 128_000 entry
so the output ceiling doesn't fall through the substring table to
"claude-opus-4" → 32_000.
- Adaptive branch now emits `context_management` with the
clear_thinking_20251015 cleanup edit. Routed via `extra_body` because
anthropic Python SDK 0.92.0 doesn't accept it as a native kwarg yet.
- Adaptive branch no longer sets `temperature`. Legacy branch still
sets `temperature=1` for manual-thinking models (4.5 and earlier).
- `tool_choice`: emit only when caller explicitly passed a value.
Previously emitted `{"type":"auto"}` unconditionally. CC never sends
`tool_choice` for default (auto) behavior.
Tests:
New `TestOpus47BodyShapeParity` class (14 tests) locks down each
parity item + the legacy-branch invariant. Existing
`test_reasoning_config_maps_xhigh_to_max_effort_for_4_6_models`
renamed and rewritten to assert the corrected identity mapping.
172 adapter tests pass (was 158, +14 new, 0 regressions).
Verified end-to-end: built real OAuth client, routed via mitmproxy,
fired real /v1/messages call on opus-4-7. Anthropic returned 200,
captured request body matches CC byte-for-byte on all 5 deltas. Only
remaining difference is `stream` (present on CC; absent in the
non-streaming smoke test — gateway streams in production).
mssteuer
added a commit
that referenced
this pull request
Apr 17, 2026
….112 (#5) Captured Claude Code 2.1.112 sending these fields on opus-4-7 requests (reference: ~/.hermes/reference/oauth-audit-2026-04-17-verify/): thinking: {"type":"adaptive"} output_config: {"effort":"xhigh"} context_management: {"edits":[{"type":"clear_thinking_20251015", "keep":"all"}]} temperature: (absent) tool_choice: (absent when caller doesn't request one) Hermes was sending: thinking: {"type":"enabled","budget_tokens":4000} temperature: 1 tool_choice: {"type":"auto"} (no output_config, no context_management) Root cause: `_supports_adaptive_thinking` only matched "4-6" / "4.6", so opus-4-7 fell through to the legacy manual-thinking branch written for 4.5 and earlier. opus-4-7 now rejects `temperature` with a 400 "temperature is deprecated for this model" — caught in journalctl after PR #3 deploy but not blocking as errors were retried. More importantly, the broken shape was a fingerprint-drift signal the Anthropic billing classifier reads, keeping OAuth traffic in the Extra Usage lane instead of the Max weekly lane. Changes (agent/anthropic_adapter.py): - `_supports_adaptive_thinking`: match "4-7" / "4.7" in addition to 4.6. Lowercase input for case-insensitive match. - `ADAPTIVE_EFFORT_MAP`: "xhigh" → "xhigh" (identity). Was "xhigh" → "max" — CC sends "xhigh" literally. - `_ANTHROPIC_OUTPUT_LIMITS`: explicit "claude-opus-4-7" → 128_000 entry so the output ceiling doesn't fall through the substring table to "claude-opus-4" → 32_000. - Adaptive branch now emits `context_management` with the clear_thinking_20251015 cleanup edit. Routed via `extra_body` because anthropic Python SDK 0.92.0 doesn't accept it as a native kwarg yet. - Adaptive branch no longer sets `temperature`. Legacy branch still sets `temperature=1` for manual-thinking models (4.5 and earlier). - `tool_choice`: emit only when caller explicitly passed a value. Previously emitted `{"type":"auto"}` unconditionally. CC never sends `tool_choice` for default (auto) behavior. Tests: New `TestOpus47BodyShapeParity` class (14 tests) locks down each parity item + the legacy-branch invariant. Existing `test_reasoning_config_maps_xhigh_to_max_effort_for_4_6_models` renamed and rewritten to assert the corrected identity mapping. 172 adapter tests pass (was 158, +14 new, 0 regressions). Verified end-to-end: built real OAuth client, routed via mitmproxy, fired real /v1/messages call on opus-4-7. Anthropic returned 200, captured request body matches CC byte-for-byte on all 5 deltas. Only remaining difference is `stream` (present on CC; absent in the non-streaming smoke test — gateway streams in production).
mssteuer
pushed a commit
that referenced
this pull request
Apr 17, 2026
…dentity sanitizer Root cause of residual Extra Usage billing drift: Hermes sent its own persona (Jean Clawd / harness instructions) in system[2]/system[3]. Anthropic's billing classifier reads system-block content, so any non-CC persona was a fingerprint miss even with wire headers (#3), Stainless/JS spoof (#4), and opus-4-7 body shape (#5) all correct. Changes: - Ship verbatim CC 2.1.112 captures of system[2] (persona) and system[3] (tool/output rules) under agent/_cc_parity_assets/, loaded at request build time and cached. - Drop context-1m-2025-08-07 from _OAUTH_ONLY_BETAS — CC no longer sends it on opus-4-7 (captured 2026-04-17). - Retire the _OAUTH_SANITIZE_REPLACEMENTS table. With full system-prompt spoofing the per-phrase replacements are vestigial noise that kept leaking identity mappings (#Nous Research → Anthropic, etc.) into the wire. Skip the TestOAuthSanitizer class; keep the fingerprint-scrub assertions at the build_anthropic_kwargs boundary. - Package the _cc_parity_assets directory via setuptools package-data so it ships with the wheel. Refresh procedure for CC version bumps: see agent/_cc_parity_assets/README.md.
mssteuer
pushed a commit
that referenced
this pull request
Apr 19, 2026
…lls/ - Rename skill to touchdesigner-mcp (matches blender-mcp convention) - Move from skills/creative/ to optional-skills/creative/ - Fix duplicate pitfall numbering (#3 appeared twice) - Update SKILL.md cross-references for renumbered pitfalls - Update setup.sh path for new directory location
mssteuer
pushed a commit
that referenced
this pull request
Jul 21, 2026
mssteuer
pushed a commit
that referenced
this pull request
Jul 21, 2026
Blocking #1 — gateway-connecting-overlay.tsx reduced-motion regression: the top `if (reduce) setPhase('gone')` fired unconditionally on mount whenever reduce-motion was on, so every OS reduced-motion user lost the CONNECTING overlay during cold boot entirely (jumped to 'gone' before the gateway was even open). The intent was to skip the exit *choreography*, not to skip showing the overlay. Removed the unconditional top block and the redundant nested preview block; kept only the third branch (`gatewayState === 'open' && shownRef.current` → `reduce ? 'gone' : 'text-out'`) which correctly gates the short-circuit on connect. Also fixed `if(reduce)` missing-space, 6-space misindent, and the same 3-line comment pasted three times. Nit #1 — tsconfig excludes e2e, so specs were never typechecked in CI. Added tsconfig.e2e.json (extends base, includes e2e/ + playwright.config.ts, adds @playwright/test types) and wired it into the typecheck script. This surfaced three latent type errors that are fixed in the same commit: - fix-electron-tracing.ts: `app._context` and `electron._playwright` are private APIs — added `as any` on the access before the existing cast. - playwright.config.ts: `reducedMotion: 'reduce'` directly under `use:` is not a valid UseOptions property in playwright 1.58; it's a BrowserContextOption accessed via `contextOptions: { reducedMotion: 'reduce' }`. The old form was silently ignored at runtime, so reduced-motion emulation wasn't actually active — screenshots could catch overlays mid-fade (exactly what the comment warned about). Nit #2 — fix-electron-tracing.ts reaches into Playwright internals (_playwright, _allContexts, _context) with no public contract. Added a header comment calling out the `@playwright/test` exact pin (=1.58.2) so a future bump knows to re-verify the private symbols still exist. Nit #3 — main.ts TEST_WORKER_INDEX block had stray 6-space indentation. Verified: tsc -p . && tsconfig.electron && tsconfig.e2e → 0 errors; vitest boot-failure-overlay (3/3) + boot-failure-reauth (21/21) pass; npm run build clean; playwright e2e/boot-failure.spec.ts 2/2 pass.
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.
What
Refreshes our OAuth wire fingerprint to byte-for-byte parity with what Claude Code 2.1.112 (current native binary) sends on
/v1/messagesrequests. Anthropic's billing classifier appears to weigh the cumulative drift score across many small signals — when too many are stale, the request gets routed into the API / Extra Usage billing lane instead of the Max weekly subscription lane.Why now
Captured a live diff of CC 2.1.112 vs Hermes via mitmproxy on the same OAuth account, same model. Audit notes and raw captures live under
~/.hermes/reference/oauth-audit-2026-04-16/(local; can share if useful). Found 9+ distinct drift items, each documented inline.Changes
Single commit, 2 files changed, +449/-20:
Per-message body
x-anthropic-billing-header: cc_version=...; cc_entrypoint=sdk-cli; cch=...;block — by far the highest-signal item the audit found"You are Claude Code, Anthropic's official CLI for Claude."→"You are a Claude agent, built on Anthropic's Claude Agent SDK."cache_control={type: ephemeral, ttl: '1h', scope: 'global'}(was bare{type: ephemeral})metadata.user_idJSON blob with{device_id, account_uuid, session_id}populated from~/.claude.json. Omitted (not faked) if that file isn't presentHeaders
User-Agent: claude-cli/<version> (external, sdk-cli)(was double-sending capital + lowercase, with old(external, cli)label)anthropic-dangerous-direct-browser-access: trueX-Claude-Code-Session-Id(stable per process) andx-client-request-id(per call) UUIDsBeta flags
context-1m-2025-08-07,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advisor-tool-2026-03-01,advanced-tool-use-2025-11-20,effort-2025-11-24fine-grained-tool-streaming-2025-05-14from the OAuth set (CC 2.1.112 stopped sending this on opus calls)URL
?beta=truequery string added viaextra_queryto match CC's pathConstants
_CLAUDE_CODE_VERSION_FALLBACK2.1.74 → 2.1.112_CLAUDE_CODE_BUILD_FALLBACK = 148(the.148build suffix incc_version=2.1.112.148)Tests
152 adapter tests pass (was 142). New
TestClaudeCodeFingerprintParityclass with 12 tests locks down each parity item individually, so a future refactor can't silently break any one of them.Pre-existing 4 failing tests (
TestReadCodexAccessToken::*,test_explicit_reset_timestamp_overrides_default_429_ttl) fail on main too — unrelated to this PR.Verified end-to-end
Re-ran a real
hermes chatsession through the same mitmproxy after the patch and confirmed the outbound request shape matches CC 2.1.112 on every diffed item.Out of scope (follow-up if needed)
The
X-Stainless-Lang: python/X-Stainless-Runtime: CPython/X-Stainless-Package-Version: 0.92.0headers still identify us as the Anthropic Python SDK rather than the JS SDK that CC uses. Spoofing those requires either monkey-patching the SDK at construction or writing a bespoke httpx transport for OAuth requests. Holding off on that until we see whether this PR's parity is sufficient on the dashboard.Verification next steps for reviewer