Skip to content

fix(anthropic-oauth): refresh Claude Code wire fingerprint to 2.1.112 - #3

Merged
mssteuer merged 4 commits into
mainfrom
fix/oauth-fingerprint-refresh-2026-04-16
Apr 17, 2026
Merged

fix(anthropic-oauth): refresh Claude Code wire fingerprint to 2.1.112#3
mssteuer merged 4 commits into
mainfrom
fix/oauth-fingerprint-refresh-2026-04-16

Conversation

@mssteuer

Copy link
Copy Markdown
Owner

What

Refreshes our OAuth wire fingerprint to byte-for-byte parity with what Claude Code 2.1.112 (current native binary) sends on /v1/messages requests. 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

  • New system[0] x-anthropic-billing-header: cc_version=...; cc_entrypoint=sdk-cli; cch=...; block — by far the highest-signal item the audit found
  • system[1] identity prefix updated: "You are Claude Code, Anthropic's official CLI for Claude.""You are a Claude agent, built on Anthropic's Claude Agent SDK."
  • Largest user system block now gets cache_control={type: ephemeral, ttl: '1h', scope: 'global'} (was bare {type: ephemeral})
  • New metadata.user_id JSON blob with {device_id, account_uuid, session_id} populated from ~/.claude.json. Omitted (not faked) if that file isn't present

Headers

  • Single canonical User-Agent: claude-cli/<version> (external, sdk-cli) (was double-sending capital + lowercase, with old (external, cli) label)
  • New anthropic-dangerous-direct-browser-access: true
  • New per-request X-Claude-Code-Session-Id (stable per process) and x-client-request-id (per call) UUIDs

Beta flags

  • Added: 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
  • Removed: fine-grained-tool-streaming-2025-05-14 from the OAuth set (CC 2.1.112 stopped sending this on opus calls)
  • Dedup pass added so the merged set never has duplicates

URL

  • ?beta=true query string added via extra_query to match CC's path

Constants

  • _CLAUDE_CODE_VERSION_FALLBACK 2.1.74 → 2.1.112
  • New _CLAUDE_CODE_BUILD_FALLBACK = 148 (the .148 build suffix in cc_version=2.1.112.148)

Tests

152 adapter tests pass (was 142). New TestClaudeCodeFingerprintParity class 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 chat session 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.0 headers 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

  1. Merge / deploy.
  2. Trigger a representative OAuth-billed workload (chat or cron).
  3. Wait ~30 min for the Claude.ai usage dashboard to refresh.
  4. Diff Extra Usage growth before vs after — should flatten if the drift was the cause.

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
mssteuer force-pushed the fix/oauth-fingerprint-refresh-2026-04-16 branch from aa03dc4 to ae1fb14 Compare April 16, 2026 23:36
…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
mssteuer merged commit f5ed076 into main Apr 17, 2026
4 of 5 checks passed
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
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant