Unofficial complete specification for implementing a proxy that handles Claude Code's OAuth authentication flow and API requests. Captured from Claude Code CLI v2.1.77 on 2026-03-17.
- Architecture Overview
- Hosts & Endpoints
- OAuth Flow
- Post-Auth Initialisation Sequence
- Messages API (Inference)
- Telemetry & Non-Essential Endpoints
- Authentication Patterns
- Rate Limit Headers
- SSE Streaming Response Format
- MCP Server Proxying
- Version Check
- Full Request Body Reference
Claude Code communicates with seven distinct hosts:
| Host | Purpose | Auth Method |
|---|---|---|
claude.ai |
OAuth authorization (browser-based) | None (browser redirect) |
platform.claude.com |
OAuth token exchange | None (uses auth code + PKCE) |
api.anthropic.com |
All API calls (profile, messages, telemetry, feature flags, MCP servers) | Authorization: Bearer <access_token> |
mcp-proxy.anthropic.com |
Remote MCP server communication | Authorization: Bearer <access_token> |
storage.googleapis.com |
Version check | None |
raw.githubusercontent.com |
Plugin security list & changelog | None |
github.com |
Git operations (cert-pinned, cannot be MITM proxied) | None |
Optional external telemetry hosts (can be blocked):
| Host | Purpose |
|---|---|
http-intake.logs.us5.datadoghq.com |
Datadog logging |
api.segment.io |
Segment analytics |
A proxy must handle traffic to claude.ai, platform.claude.com, and api.anthropic.com to fully support Claude Code.
| Method | Path | Purpose |
|---|---|---|
| GET | /oauth/authorize |
Initiate OAuth authorization |
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/oauth/token |
Exchange authorization code for tokens / refresh tokens |
| Method | Path | Purpose | Required |
|---|---|---|---|
| GET | /api/oauth/profile |
User profile & subscription info | Yes |
| GET | /api/oauth/claude_cli/roles |
Organization & workspace roles | Yes |
| GET | /api/oauth/account/settings |
Account feature flags & preferences | Yes |
| GET | /api/oauth/usage |
Current usage / rate limit utilization | Yes |
| GET | /api/oauth/claude_cli/client_data |
Client-specific configuration | Yes |
| GET | /api/claude_code_grove |
Grove (session sharing) status | Yes |
| GET | /api/claude_code_penguin_mode |
Penguin mode (extended thinking) status | Yes |
| POST | /api/eval/sdk-{hash} |
GrowthBook feature flag evaluation | Yes |
| GET | /v1/mcp_servers?limit=1000 |
List remote MCP servers | Yes |
| POST | /v1/messages?beta=true |
Inference (Messages API) | Yes |
| POST | /api/event_logging/v2/batch |
Telemetry events (v2) | No (can be stubbed) |
| POST | /api/event_logging/batch |
Telemetry events (v1, legacy) | No (can be stubbed) |
| POST | /api/claude_code/metrics |
Usage metrics | No (can be stubbed) |
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/mcp/{server_id} |
MCP JSON-RPC calls to remote servers |
| Method | Path | Purpose |
|---|---|---|
| GET | /claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest |
Check latest CLI version |
| Method | Path | Purpose |
|---|---|---|
| GET | /anthropics/claude-plugins-official/refs/heads/security/security.json |
Plugin security blocklist |
| GET | /anthropics/claude-code/refs/heads/main/CHANGELOG.md |
Changelog for update notifications |
No authentication required. Can be stubbed or blocked. The security.json request includes a ?t={timestamp} cache-buster.
Note: github.com is also contacted but uses certificate pinning — it will reject MITM proxy certificates with TLS errors. This is expected and non-fatal.
Claude Code opens a browser to this URL:
https://claude.ai/oauth/authorize?
code=true&
client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e&
response_type=code&
redirect_uri=https%3A%2F%2Fplatform.claude.com%2Foauth%2Fcode%2Fcallback&
scope=org%3Acreate_api_key+user%3Aprofile+user%3Ainference+user%3Asessions%3Aclaude_code+user%3Amcp_servers+user%3Afile_upload&
code_challenge={PKCE_CHALLENGE}&
code_challenge_method=S256&
state={STATE}
Parameters:
| Parameter | Value |
|---|---|
client_id |
9d1c250a-e61b-44d9-88ed-5944d1962f5e (Claude Code app) |
response_type |
code |
redirect_uri |
https://platform.claude.com/oauth/code/callback |
scope |
org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload |
code_challenge_method |
S256 (PKCE) |
code_challenge |
Base64url-encoded SHA-256 of code_verifier |
state |
Random base64url string for CSRF protection |
The user logs in and authorizes the app. The browser redirects to:
https://platform.claude.com/oauth/code/callback?code={AUTH_CODE}#{STATE}
The page displays a code in the format:
{AUTH_CODE}#{STATE}
The user pastes this code back into the CLI. The # separates the authorization code from the state parameter.
POST https://platform.claude.com/v1/oauth/token
Content-Type: application/json
User-Agent: axios/1.13.4
Request Body:
{
"grant_type": "authorization_code",
"code": "{AUTH_CODE}",
"redirect_uri": "https://platform.claude.com/oauth/code/callback",
"client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
"code_verifier": "{PKCE_CODE_VERIFIER}",
"state": "{STATE}"
}Response (200 OK):
{
"token_type": "Bearer",
"access_token": "sk-ant-oat01-...",
"expires_in": 28800,
"refresh_token": "sk-ant-ort01-...",
"scope": "user:file_upload user:inference user:mcp_servers user:profile user:sessions:claude_code",
"organization": {
"uuid": "aaaaaaaa-...",
"name": "Organization Name"
},
"account": {
"uuid": "aaaaaaaa-...",
"email_address": "user@example.com"
}
}Key details:
- Access tokens have prefix
sk-ant-oat01- - Refresh tokens have prefix
sk-ant-ort01- expires_inis 28800 seconds (8 hours)- No
Authorizationheader is sent on this request - The token exchange request uses
User-Agent: axios/1.13.4(not the claude-cli user agent)
When the access token expires, Claude Code sends:
POST https://platform.claude.com/v1/oauth/token
Content-Type: application/json
{
"grant_type": "refresh_token",
"refresh_token": "sk-ant-ort01-...",
"client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
}Response format is the same as the initial token exchange.
After obtaining tokens, Claude Code makes these requests before accepting user input. All use Authorization: Bearer {access_token}. Many are fired concurrently.
POST https://api.anthropic.com/api/eval/sdk-{hash}
Authorization: Bearer {access_token}
Content-Type: application/json
{
"attributes": {
"id": "{device_hash}",
"sessionId": "{session_uuid}",
"deviceID": "{device_hash}",
"platform": "darwin",
"userType": "external",
"appVersion": "2.1.77"
},
"forcedVariations": {},
"forcedFeatures": [],
"url": ""
}Response: Contains 145+ feature flags. Key ones:
{
"features": {
"tengu_streaming_tool_execution2": { "value": true, "on": true, "off": false, "source": "force" },
"tengu_hawthorn_window": { "value": 200000, "on": true, "off": false, "source": "defaultValue" },
"tengu_workout2": { "value": true, "on": true, "off": false, "source": "defaultValue" },
"tengu_accept_with_feedback": { "value": true, "on": true, "off": false, "source": "force" },
"tengu_quartz_lantern": { "value": true, "on": true, "off": false, "source": "defaultValue" },
"tengu_sm_compact": { "value": false, "on": false, "off": true, "source": "defaultValue" },
"tengu_system_prompt_global_cache": { "value": false, "on": false, "off": true, "source": "defaultValue" }
}
}Each feature flag has the structure:
{
"value": true,
"on": true,
"off": false,
"source": "defaultValue",
"experiment": null,
"experimentResult": null,
"ruleId": null
}GET https://api.anthropic.com/api/oauth/account/settings
Authorization: Bearer {access_token}
Response: Large JSON with account preferences, feature toggles, onboarding state.
{
"enabled_web_search": true,
"paprika_mode": "off",
"tool_search_mode": "auto",
"grove_enabled": false,
"grove_updated_at": "2026-01-01T01:01:01.012345Z",
"onboarding_use_case": "personal",
"has_started_claudeai_onboarding": true,
"has_finished_claudeai_onboarding": true,
"enabled_artifacts_attachments": false,
"dismissed_claudeai_banners": [
{
"banner_id": "mcp_directory_chin_14-0-2026",
"dismissed_at": "2026-01-01T01:01:01.012345Z"
}
],
"dismissed_saffron_themes": true,
"internal_tier_org_type": null,
"internal_tier_rate_limit_tier": null,
"ccr_sharing_enforce_repo_check": null,
"ccr_sharing_show_display_name": null,
"ccr_sharing_auto_share_on_pr": null,
"ccr_auto_archive_on_pr_close": null,
"ccr_persistent_memory": null,
"ccr_plugins_mount": null,
"voice_preference": null,
"voice_speed": null
}GET https://api.anthropic.com/api/claude_code_grove
Authorization: Bearer {access_token}
{
"grove_enabled": true,
"domain_excluded": false,
"notice_is_grace_period": false,
"notice_reminder_frequency": 0
}GET https://api.anthropic.com/api/claude_code_penguin_mode
Authorization: Bearer {access_token}
{
"enabled": true,
"disabled_reason": null
}GET https://api.anthropic.com/api/oauth/claude_cli/client_data
Authorization: Bearer {access_token}
{
"client_data": {}
}GET https://api.anthropic.com/v1/mcp_servers?limit=1000
Authorization: Bearer {access_token}
{
"data": [
{
"type": "mcp_server",
"id": "mcpsrv_01234567890ABCDEFGHIJKLM",
"display_name": "Gmail",
"url": "https://gmail.mcp.claude.com/mcp",
"created_at": "2026-01-01T01:01:01.012345Z"
},
{
"type": "mcp_server",
"id": "mcpsrv_1234567890ABCDEFGHIJKLMN",
"display_name": "Google Calendar",
"url": "https://gcal.mcp.claude.com/mcp",
"created_at": "2026-01-01T01:01:01.012345Z"
}
],
"next_page": null
}Claude Code sends a small "preflight" messages request on startup (before user input):
POST https://api.anthropic.com/v1/messages?beta=true
Authorization: Bearer {access_token}
anthropic-beta: oauth-2025-04-20,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05
anthropic-dangerous-direct-browser-access: true
anthropic-version: 2023-06-01
Content-Type: application/json
{
"model": "claude-haiku-4-5-20251001",
"max_tokens": 1,
"messages": [{"role": "user", "content": "#"}],
"system": "#"
}This is a connectivity/auth check using the cheapest model. The proxy must handle this and return a valid response for Claude Code to proceed.
Expected response: Standard Messages API response (non-streaming).
GET https://api.anthropic.com/api/oauth/profile
Authorization: Bearer {access_token}
Response:
{
"account": {
"uuid": "00000000-...",
"full_name": "Jane",
"display_name": "Jane",
"email": "user@example.invalid",
"has_claude_max": true,
"has_claude_pro": false,
"created_at": "2026-01-01T01:01:01.012345Z"
},
"organization": {
"uuid": "00000000-...",
"name": "Organization Name",
"organization_type": "claude_max",
"billing_type": "stripe_subscription",
"rate_limit_tier": "default_claude_max_20x",
"has_extra_usage_enabled": true,
"subscription_status": "active",
"subscription_created_at": "2026-01-01T01:01:01.012345Z"
},
"application": {
"uuid": "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
"name": "Claude Code",
"slug": "claude-code"
}
}GET https://api.anthropic.com/api/oauth/claude_cli/roles
Authorization: Bearer {access_token}
{
"organization_uuid": "000000-...",
"organization_name": "Organization Name",
"organization_role": "admin",
"workspace_uuid": null,
"workspace_name": null,
"workspace_role": null
}GET https://api.anthropic.com/api/oauth/usage
Authorization: Bearer {access_token}
{
"five_hour": {
"utilization": 5.0,
"resets_at": "2026-01-01T01:01:01.012345+00:00"
},
"seven_day": {
"utilization": 4.0,
"resets_at": "2026-01-01T01:01:01.012345+00:00"
},
"seven_day_oauth_apps": null,
"seven_day_opus": null,
"seven_day_sonnet": {
"utilization": 1.0,
"resets_at": "2026-01-01T01:01:01.012345+00:00"
},
"seven_day_cowork": null,
"iguana_necktie": null,
"extra_usage": {
"is_enabled": true,
"monthly_limit": 100,
"used_credits": 0.0,
"utilization": null
}
}POST https://api.anthropic.com/v1/messages?beta=true
Required Headers:
accept: application/json
authorization: Bearer {access_token}
content-type: application/json
user-agent: claude-cli/2.1.77 (external, cli)
anthropic-beta: claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24
anthropic-dangerous-direct-browser-access: true
anthropic-version: 2023-06-01
x-app: cli
Note: The anthropic-beta flags vary per request. The CLI uses different combinations depending on context (preflight, inference, thinking mode). The proxy should forward whatever the CLI sends, not hardcode a specific set. The only critical flag is oauth-2025-04-20 — without it, OAuth tokens are rejected entirely.
Observed anthropic-beta variants (v2.1.77):
| Context | Flags |
|---|---|
| Inference (Opus, adaptive thinking) | claude-code-20250219,oauth-2025-04-20,adaptive-thinking-2026-01-28,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24 |
| Inference (1M context) | claude-code-20250219,oauth-2025-04-20,context-1m-2025-08-07,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24 |
| Preflight (Haiku check) | oauth-2025-04-20,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05 |
| MCP server listing | mcp-servers-2025-12-04 |
| Init endpoints (profile, settings, etc.) | oauth-2025-04-20 |
Stainless SDK Headers (informational, sent by the Anthropic JS SDK):
x-stainless-arch: arm64
x-stainless-lang: js
x-stainless-os: MacOS
x-stainless-package-version: 0.74.0
x-stainless-retry-count: 0
x-stainless-runtime: node
x-stainless-runtime-version: v24.3.0
x-stainless-timeout: 600
Request Body Structure:
{
"model": "claude-opus-4-6",
"max_tokens": 32000,
"stream": true,
"thinking": {
"type": "adaptive"
},
"system": [
{
"type": "text",
"text": "x-anthropic-billing-header: cc_version=2.1.77.<hash>; cc_entrypoint=cli; cch=00000;"
},
{
"type": "text",
"text": "You are Claude Code, Anthropic's official CLI for Claude.",
"cache_control": {
"type": "ephemeral",
"ttl": "1h"
}
},
{
"type": "text",
"text": "...(~23,000 chars of system instructions)...",
"cache_control": {
"type": "ephemeral",
"ttl": "1h"
}
}
],
"messages": [
{
"role": "user",
"content": "..."
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "..."
},
{
"type": "text",
"text": "...(user's actual message)...",
"cache_control": {
"type": "ephemeral",
"ttl": "1h"
}
}
]
}
],
"tools": [
{
"name": "Agent",
"description": "...",
"input_schema": { ... }
},
{
"name": "Bash",
"description": "...",
"input_schema": { ... }
},
{
"name": "Glob",
"description": "...",
"input_schema": { ... }
},
{
"name": "Grep",
"description": "...",
"input_schema": { ... }
},
{
"name": "Read",
"description": "...",
"input_schema": { ... }
},
{
"name": "Edit",
"description": "...",
"input_schema": { ... }
},
{
"name": "Write",
"description": "...",
"input_schema": { ... }
},
{
"name": "Skill",
"description": "...",
"input_schema": { ... }
},
{
"name": "ToolSearch",
"description": "...",
"input_schema": { ... }
}
],
"metadata": {
"user_id": "user_{device_hash}_account_{account_uuid}_session_{session_uuid}"
},
"context_management": {
"edits": [
{
"type": "clear_thinking_20251015",
"keep": "all"
}
]
},
"output_config": {
"effort": "medium"
}
}Key observations:
stream: true— responses are always SSE streamedthinking.type: "adaptive"— extended thinking is enabled- System prompt block 1 is a billing header (
x-anthropic-billing-header) — no cache_control - System prompt blocks 2 and 3 use
cache_controlwithephemeraltype and1hTTL - The last content block in messages also uses
cache_controlfor prompt caching toolsarray contains 9 tool definitions (Agent, Bash, Glob, Grep, Read, Edit, Write, Skill, ToolSearch) — these are sent on every requestmetadata.user_idfollows the format:user_{device_hash}_account_{account_uuid}_session_{session_uuid}context_management.editscontrols thinking context behaviouroutput_config.effortcan be"low","medium", or"high"- Total request body can be 70-120KB+ depending on conversation length
Content-Type: text/event-stream; charset=utf-8
See SSE Streaming Response Format below.
Responses use Server-Sent Events format:
event: message_start
data: {"type":"message_start","message":{"model":"claude-sonnet-4-6","id":"msg_1234567890ABCDEFGHIJKLMN","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":100,"cache_read_input_tokens":100,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":100},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: ping
data: {"type": "ping"}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"OK"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":3,"cache_creation_input_tokens":100,"cache_read_input_tokens":100,"output_tokens":100},"context_management":{"applied_edits":[]}}
event: message_stop
data: {"type":"message_stop"}
Event sequence:
message_start— contains model, message ID, initial usage with cache statscontent_block_start— starts a content block (text, thinking, tool_use)ping— keepalive (sent periodically during long responses)content_block_delta— incremental content (text_delta,thinking_delta,input_json_delta)content_block_stop— ends a content blockmessage_delta— finalstop_reason, cumulative usage, andcontext_managementmessage_stop— end of response
Usage fields in message_start:
{
"input_tokens": 3,
"cache_creation_input_tokens": 100,
"cache_read_input_tokens": 100,
"cache_creation": {
"ephemeral_5m_input_tokens": 0,
"ephemeral_1h_input_tokens": 100
},
"output_tokens": 1,
"service_tier": "standard",
"inference_geo": "not_available"
}Important:
- Each
data:line may have trailing whitespace/padding — the proxy must preserve this - The
context_managementfield appears inmessage_deltawithapplied_editsarray - The proxy must stream SSE events in real-time — do not buffer the entire response
The Messages API response includes these rate limit headers:
anthropic-organization-id: {org_uuid}
anthropic-ratelimit-unified-status: allowed
anthropic-ratelimit-unified-reset: 1773723880
anthropic-ratelimit-unified-5h-status: allowed
anthropic-ratelimit-unified-5h-reset: 1773723880
anthropic-ratelimit-unified-5h-utilization: 0.01
anthropic-ratelimit-unified-7d-status: allowed
anthropic-ratelimit-unified-7d-reset: 1773723880
anthropic-ratelimit-unified-7d-utilization: 0.01
anthropic-ratelimit-unified-7d_sonnet-status: allowed
anthropic-ratelimit-unified-7d_sonnet-reset: 1773723880
anthropic-ratelimit-unified-7d_sonnet-utilization: 0.01
anthropic-ratelimit-unified-representative-claim: five_hour
anthropic-ratelimit-unified-fallback-percentage: 0.5
anthropic-ratelimit-unified-overage-disabled-reason: out_of_credits
anthropic-ratelimit-unified-overage-status: rejected
Notes:
- The
7d_sonnetvariant appears when using Sonnet models representative-claimindicates which rate limit window is most relevantoverage-status: rejectedwithoverage-disabled-reason: out_of_creditsmeans extra usage credits are exhausted- The proxy must forward all
anthropic-ratelimit-*headers — Claude Code uses them for the/usagedisplay and rate limit decisions
Additional response headers to forward:
request-id: req_1234567890ABCDEFGHIJKLMN
anthropic-organization-id: 00000000-...
server-timing: proxy;dur=100
x-envoy-upstream-service-time: 100
content-security-policy: default-src 'none'; frame-ancestors 'none'
x-robots-tag: none
Used for all API calls after authentication:
Authorization: Bearer sk-ant-oat01-...
Endpoints using Bearer auth:
- All
/api/oauth/*endpoints /api/eval/sdk-*/api/claude_code_grove/api/claude_code_penguin_mode/v1/messages/v1/mcp_servers/api/event_logging/v2/batch/api/event_logging/batch/api/claude_code/metrics- All
mcp-proxy.anthropic.comrequests
POST /v1/oauth/tokenonplatform.claude.com(uses code + PKCE instead)GET /claude-code-releases/latestonstorage.googleapis.com(public version check)
Claude Code connects to remote MCP servers through Anthropic's proxy:
POST https://mcp-proxy.anthropic.com/v1/mcp/{server_id}
Authorization: Bearer {access_token}
Content-Type: application/json
Accept: application/json, text/event-stream
User-Agent: claude-code/2.1.77 (cli)
X-MCP-Client-Session-Id: {uuid}
Initialize request:
{
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {
"roots": {},
"elicitation": {
"form": {},
"url": {}
}
},
"clientInfo": {
"name": "claude-code",
"version": "2.1.77"
}
},
"jsonrpc": "2.0",
"id": 0
}Note: MCP servers may require separate OAuth tokens (not the Claude Code OAuth token). If the server requires auth, the response will be:
{
"type": "error",
"error": {
"type": "authentication_error",
"message": "MCP server requires authentication but no OAuth token is configured."
},
"request_id": "req_..."
}Status code: 401
These endpoints can be stubbed with success responses if not needed:
POST https://api.anthropic.com/api/event_logging/v2/batch
Authorization: Bearer {access_token}
Content-Type: application/json
Stub response:
{"accepted_count": 0, "rejected_count": 0}POST https://api.anthropic.com/api/event_logging/batch
Authorization: Bearer {access_token}
Content-Type: application/json
Stub response:
{"accepted_count": 0, "rejected_count": 0}POST https://api.anthropic.com/api/claude_code/metrics
Authorization: Bearer {access_token}
Can return: 200 OK with empty body.
POST https://http-intake.logs.us5.datadoghq.com/api/v2/logs
Not proxied through Anthropic — goes direct to Datadog. Can be blocked entirely.
POST https://api.segment.io/v1/batch
Not proxied through Anthropic — goes direct to Segment. Can be blocked entirely.
GET https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases/latest
Response: Plain text version number (e.g., 2.1.71)
No authentication required. Can be stubbed or proxied. Called periodically (multiple times per session).
The system prompt is sent as an array of 3 text blocks:
| Block | Content | Cache Control | Typical Size |
|---|---|---|---|
| 0 | Billing header (REQUIRED for Sonnet/Opus — see Model Access) | None | ~80 chars |
| 1 | Identity: You are Claude Code, Anthropic's official CLI for Claude. |
ephemeral, 1h |
~57 chars |
| 2 | Full system instructions (tools usage, safety rules, tone, etc.) | ephemeral, 1h |
~23,000 chars |
CRITICAL: Block 0 — Billing Header
The first system block must be the billing header. Without it, Sonnet and Opus models return 400 Error (Haiku works without it):
x-anthropic-billing-header: cc_version=2.1.77.b88; cc_entrypoint=cli; cch=00000;
| Field | Value | Purpose |
|---|---|---|
cc_version |
2.1.77.<hash> |
CLI version + 3-char content hash (see algorithm below) |
cc_entrypoint |
cli |
Entry point (cli, vscode, sdk, etc.) or CLAUDE_CODE_ENTRYPOINT env var |
cch |
00000 |
Static placeholder (was a computed checksum in earlier versions) |
cc_workload |
(optional) | Workload identifier if configured |
Billing header hash algorithm (v2.1.77):
The 3-char hash appended to cc_version is derived from the first user message in the conversation:
const salt = "59cf53e54c78";
const VERSION = "2.1.77";
// 1. Extract text of the first "user" type message from the normalized messages array
const text = messages.find(m => m.type === "user")?.message.content; // get text
// 2. Take characters at 0-indexed positions 4, 7, 20 (default "0" if missing)
const chars = [4, 7, 20].map(i => text[i] || "0").join("");
// 3. SHA-256 hash the concatenation, take first 3 hex chars
const hash = crypto.createHash("sha256")
.update(salt + chars + VERSION)
.digest("hex")
.slice(0, 3);
// 4. cc_version becomes "2.1.77.<hash>" e.g. "2.1.77.b88"
// 5. cch is always "00000" (static)Example: For first user message "Hello, how are you?":
- Chars at [4,7,20] =
"o","h","0"(pos 20 doesn't exist) →"oh0" - Input:
"59cf53e54c78oh02.1.77" - SHA-256 → first 3 hex chars:
"b88" - Header:
cc_version=2.1.77.b88; cc_entrypoint=cli; cch=00000;
Feature flag: The billing header is controlled by the tengu_attribution_header feature flag and can be disabled via CLAUDE_CODE_ATTRIBUTION_HEADER=false env var.
This block must have no cache_control — it is not cached. The proxy must preserve this block exactly as sent by the CLI.
9 tools are defined in every messages request:
| Tool | Purpose |
|---|---|
Agent |
Launch sub-agents for complex tasks |
Bash |
Execute shell commands |
Glob |
File pattern matching |
Grep |
Content search (ripgrep-based) |
Read |
Read file contents |
Edit |
Edit files with string replacement |
Write |
Write/create files |
Skill |
Invoke slash command skills |
ToolSearch |
Discover deferred tools |
{
"user_id": "user_{device_hash}_account_{account_uuid}_session_{session_uuid}"
}{
"edits": [
{
"type": "clear_thinking_20251015",
"keep": "all"
}
]
}{
"effort": "medium"
}Valid effort levels: "low", "medium", "high"
A complete proxy must:
- Handle OAuth authorization redirect (
claude.ai/oauth/authorize) - Handle OAuth token exchange (
platform.claude.com/v1/oauth/token) - Handle token refresh (same endpoint,
grant_type=refresh_token) - Forward
Authorization: Bearertokens on all API requests - Support SSE streaming for
/v1/messagesresponses (do NOT buffer) - Forward all
anthropic-ratelimit-*headers from Messages API responses - Forward the
anthropic-betaheader (required for beta features) - Forward
anthropic-dangerous-direct-browser-access: true - Forward
anthropic-version: 2023-06-01 - Forward
request-idandanthropic-organization-idresponse headers - Handle the preflight Haiku messages request (connectivity check)
- Handle
/api/oauth/profile— returns account/org/subscription info - Handle
/api/oauth/claude_cli/roles— returns org role - Handle
/api/oauth/account/settings— returns feature flags - Handle
/api/oauth/usage— returns rate limit utilization - Handle
/api/oauth/claude_cli/client_data— returns client config - Handle
/api/claude_code_grove— returns grove status - Handle
/api/claude_code_penguin_mode— returns penguin mode status - Handle
/api/eval/sdk-{hash}— returns GrowthBook feature flags - Handle
/v1/mcp_serverslisting - Optionally proxy MCP server requests (
mcp-proxy.anthropic.com) - Handle or stub telemetry endpoints (
event_logging/v2/batch,event_logging/batch,metrics) - Preserve
cache_controlin request bodies (affects billing/performance) - Support
?beta=truequery parameter on messages endpoint - Handle large request bodies (70-120KB+ for messages with tools + system prompt)
To point Claude Code at your proxy:
# Route API calls through your proxy (only /v1/messages and /v1/mcp_servers)
ANTHROPIC_BASE_URL=https://your-proxy.example.com claude
# Route ALL traffic (including OAuth, profile, feature flags) through a forward proxy
HTTPS_PROXY=https://your-proxy.example.com claude
# If your proxy uses a custom CA certificate
NODE_EXTRA_CA_CERTS=/path/to/ca-cert.pem claude
# Combine for full proxy with custom CA
HTTPS_PROXY=https://your-proxy.example.com \
NODE_EXTRA_CA_CERTS=/path/to/ca-cert.pem \
claudeNote: ANTHROPIC_BASE_URL only affects the Messages API endpoint. OAuth, profile, feature flags, and telemetry still go directly to api.anthropic.com and platform.claude.com. Use HTTPS_PROXY for full traffic interception.
Both of these are required for Sonnet and Opus models to work with OAuth tokens:
anthropic-betaheader must includeoauth-2025-04-20— without it, ALL OAuth requests fail with401 "OAuth authentication is currently not supported"- Billing header must be present in system prompt block 0 — without it, Sonnet/Opus return
400 "Error"(Haiku works without it)
Required anthropic-beta flags (v2.1.77):
anthropic-beta: claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,advanced-tool-use-2025-11-20,effort-2025-11-24
The billing header in system prompt block 0 gates access to Sonnet and Opus models. The proxy must either preserve the billing header from the CLI or generate it.
Billing header format:
x-anthropic-billing-header: cc_version=2.1.77.<hash>; cc_entrypoint=cli; cch=00000;
See Billing Header Hash Algorithm for the hash computation.
OAuth access tokens can be invalidated by certain API errors. Specifically:
401auth errors (wrong/missingoauth-2025-04-20beta flag) burn the token — all subsequent requests return401 "Invalid authentication credentials"400errors (e.g., missing billing header for Opus) do NOT invalidate the token — the same token works on the next correct request
Implications for proxy implementations:
- The proxy must not strip or modify the
anthropic-betaheader — a401will burn the client's token - The proxy must preserve the billing header system block — without it, Sonnet/Opus return
400 - A
400from a missing billing header is recoverable (same token works on retry), but a401from wrong beta flags is not - If the proxy needs to generate the billing header, it must compute the
cc_versionhash correctly (see algorithm above)
| Scenario | Haiku | Sonnet | Opus |
|---|---|---|---|
| OAuth + beta flag + billing header | 200 | 200 | 200 |
| OAuth + beta flag + NO billing header | 200 | 400 | 400 |
OAuth without oauth-2025-04-20 flag |
401 (burns token) | 401 (burns token) | 401 (burns token) |
- The proxy must forward the
anthropic-betaheader exactly as sent by the CLI - The proxy must preserve the billing header in system prompt block 0 (or generate it)
- The proxy should forward all request headers without modification
- If the proxy needs to inject its own system prompt content, add it as additional blocks after block 0
- Token exchange and refresh can be proxied transparently
400errors are safe (token survives),401errors are destructive (token is burned)
Verified working endpoints with OAuth tokens:
GET /api/oauth/profile— worksGET /api/oauth/claude_cli/roles— worksGET /api/oauth/account/settings— worksGET /api/oauth/usage— works (requiresoauth-2025-04-20beta context)POST /v1/messageswith Haiku — works without billing headerPOST /v1/messageswith Sonnet/Opus — works only with billing header