
Claude Code Proxy Patterns
- 96 installs
- 62 repo stars
- Updated August 3, 2026
- terrylica/cc-skills
Helps with ai & agent building tasks.
About
claude-code-proxy-patterns is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- claude-code-proxy-patterns
- AI & Agent Building
- AI-coding skill
Claude Code Proxy Patterns by the numbers
- 96 all-time installs (skills.sh)
- +1 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #4,561 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/terrylica/cc-skills --skill claude-code-proxy-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 96 |
|---|---|
| repo stars | ★ 62 |
| Last updated | August 3, 2026 |
| Repository | terrylica/cc-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
<!-- # SSoT-OK: version references are documentation of binary analysis findings, not package versions -->
Claude Code Proxy Patterns
Multi-provider proxy that routes Claude Code model tiers to different backends. Haiku to MiniMax (cost/speed), Sonnet/Opus to Anthropic (native OAuth passthrough). Includes Go binary proxy with launchd auto-restart and failover wrapper for resilience.
Scope: Local reverse proxy for Claude Code with OAuth subscription (Max plan). Routes based on model name in request body.
Reference implementations:
- Go proxy binary:
/usr/local/bin/claude-proxy(port 8082)
---
Self-Evolving Skill: This skill improves through use. If instructions are wrong, parameters drifted, or a workaround was needed — fix this file immediately, don't defer. Only update for real, reproducible issues.
When to Use This Skill
- Building or debugging a Claude Code multi-provider proxy
- Setting up
ANTHROPIC_BASE_URLwith OAuth subscription mode - Integrating Anthropic-compatible providers (MiniMax, etc.)
- Diagnosing "OAuth not supported" or auth failures through a proxy
- Understanding how Claude Code stores and transmits OAuth tokens
Do NOT use for: Claude API key-only setups (no proxy needed), MCP server development, Claude Code hooks (operate at tool level, not API level), or corporate HTTPS proxy traversal.
---
Architecture
Claude Code (OAuth/Max subscription)
|
| ANTHROPIC_BASE_URL=http://127.0.0.1:8082 (Go proxy)
| (unset ANTHROPIC_API_KEY to avoid auth conflict)
v
+----------------------------------+
| Go proxy (:8082) |
| launchd managed, auto-restart |
+----------------------------------+
|
| model =
| claude-haiku-
| 4-5-20251001
v
+-----------+
| MiniMax |
| highspeed |
+-----------+Port Configuration:
:8082- Go proxy (entry point, launchd-managed, auto-restart)
The Go proxy uses cenkalti/backoff/v4 for built-in retry logic.
The proxy reads the model field from each /v1/messages request body. If it matches the configured Haiku model ID, the request goes to MiniMax. Everything else falls through to real Anthropic with OAuth passthrough.
---
Working Patterns
WP-01: Keychain OAuth Token Reading
Read OAuth tokens from macOS Keychain where Claude Code stores them.
Service: "Claude Code-credentials" (note the space before the hyphen) Account: Current username via getpass.getuser()
import subprocess, json, getpass
result = subprocess.run(
["security", "find-generic-password",
"-s", "Claude Code-credentials",
"-a", getpass.getuser(), "-w"],
capture_output=True, text=True, timeout=5, check=False,
)
if result.returncode == 0:
data = json.loads(result.stdout.strip())
oauth = data.get("claudeAiOauth")See references/oauth-internals.md for the full deep dive.
WP-02: Token JSON Structure
The Keychain stores a JSON envelope with the claudeAiOauth key.
{
"claudeAiOauth": {
"accessToken": "eyJhbG...",
"refreshToken": "rt_...",
"expiresAt": 1740268800000,
"subscriptionType": "claude_pro_2025"
}
}Note: expiresAt is in milliseconds (Unix epoch _ 1000). Compare with time.time() _ 1000 or divide by 1000 for seconds.
WP-03: OAuth Beta Header
The anthropic-beta: oauth-2025-04-20 header is required for OAuth token authentication. Without it, Anthropic rejects the Bearer token.
Critical: APPEND to existing beta headers, do not replace them.
# proxy.py:304-308
existing_beta = original_headers.get("anthropic-beta", "")
beta_parts = [b.strip() for b in existing_beta.split(",") if b.strip()] if existing_beta else []
if "oauth-2025-04-20" not in beta_parts:
beta_parts.append("oauth-2025-04-20")
target_headers["anthropic-beta"] = ",".join(beta_parts)WP-04: ANTHROPIC_API_KEY=proxy-managed
Setting ANTHROPIC_BASE_URL alone is insufficient in OAuth mode. Claude Code must also see ANTHROPIC_API_KEY set to switch from OAuth-only mode to API-key mode, which then honors ANTHROPIC_BASE_URL.
# In .zshenv (managed by proxy-toggle)
export ANTHROPIC_BASE_URL="http://127.0.0.1:8082"
export ANTHROPIC_API_KEY="proxy-managed"The value "proxy-managed" is a dummy sentinel. The proxy intercepts it (line 324) and never forwards it to providers.
WP-05: OAuth Token Cache with TTL
Avoid repeated Keychain subprocess calls by caching the token for 5 minutes.
# proxy.py:117-118
_oauth_cache: dict = {"token": None, "expires_at": 0.0, "fetched_at": 0.0}
_OAUTH_CACHE_TTL = 300 # Re-read from Keychain every 5 minutesCache invalidation triggers:
- TTL expired (5 minutes since last fetch)
- Token's
expiresAthas passed - Proxy restart
WP-06: Auth Priority Chain
The proxy tries multiple auth sources in order for Anthropic-bound requests.
1. REAL_ANTHROPIC_API_KEY env var -> x-api-key header (explicit config)
2. Keychain OAuth token -> Authorization: Bearer + anthropic-beta
3. ~/.claude/.credentials.json -> Authorization: Bearer (plaintext fallback)
4. Forward client Authorization -> Pass through whatever Claude Code sent
5. No auth -> Will 401 (expected)See proxy.py:293-314 for the implementation.
WP-07: count_tokens Endpoint Auth
The /v1/messages/count_tokens endpoint needs the same auth as /v1/messages. Claude Code calls this for preflight token counting. Missing auth here causes silent failures. Returns 501 for non-Anthropic providers (MiniMax doesn't support it).
WP-08: Anthropic-Compatible Provider URLs
Third-party providers that support the Anthropic /v1/messages API format.
| Provider | Base URL | Notes |
|---|---|---|
| MiniMax highspeed | https://api.minimax.io/anthropic | Returns base_resp field, extra thinking block |
See references/provider-compatibility.md for the full matrix.
WP-09: Concurrency Semaphore
Per-provider rate limiting prevents overwhelming third-party APIs. No semaphore for Anthropic (they handle their own rate limiting).
# proxy.py:207-209
MAX_CONCURRENT_REQUESTS = int(os.getenv("MAX_CONCURRENT_REQUESTS", "5"))
haiku_semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)
opus_semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)
sonnet_semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS)WP-10: proxy-toggle Enable/Disable
The proxy-toggle script manages .zshenv entries and a flag file atomically.
~/.claude/bin/proxy-toggle enable # Adds env vars, creates flag file, checks health
~/.claude/bin/proxy-toggle disable # Removes env vars, removes flag file
~/.claude/bin/proxy-toggle status # Shows routing flag, proxy process, .zshenv stateImportant: Claude Code must be restarted after toggling because ANTHROPIC_BASE_URL is read at startup.
WP-11: Health Endpoint
The /health endpoint returns provider configuration state for monitoring.
curl -s http://127.0.0.1:8082/health | jq .WP-12: Go Proxy with Retry
Go proxy with built-in retry using cenkalti/backoff/v4 (exponential backoff: 500ms -> 1s -> 2s, max 5s elapsed).
import "github.com/cenkalti/backoff/v4"
backoffConfig := backoff.NewExponentialBackOff(
backoff.WithInitialInterval(500 * time.Millisecond),
backoff.WithMultiplier(2),
backoff.WithMaxInterval(2 * time.Second),
backoff.WithMaxElapsedTime(5 * time.Second),
)
err := backoff.Retry(operation, backoffConfig)Location: /usr/local/bin/claude-proxy | Environment: ANTHROPIC_BASE_URL=http://127.0.0.1:8082 in .zshenv
WP-13: Launchd Service Configuration
The Go proxy runs as a macOS launchd daemon for auto-restart on crash and boot persistence.
Plist: /Library/LaunchDaemons/com.terryli.claude-proxy.plist
Full plist configuration, commands, verification checklist, and debugging: references/launchd-configuration.md
WP-14: OAuth Token Auto-Refresh
Background goroutine refreshes OAuth tokens every 30 minutes, 5 minutes before expiry. Falls back to Keychain if API refresh fails.
Full implementation and refresh logic: references/oauth-auto-refresh.md
---
Anti-Patterns Summary
Full details with code examples: references/anti-patterns.md
| ID | Severity | Gotcha | Fix |
|---|---|---|---|
| CCP-01 | HIGH | ANTHROPIC_BASE_URL alone without ANTHROPIC_API_KEY | Set ANTHROPIC_API_KEY=proxy-managed |
| CCP-02 | HIGH | Missing anthropic-beta: oauth-2025-04-20 header | Append to existing beta headers |
| CCP-03 | MEDIUM | Using /api/oauth/claude_cli/create_api_key endpoint | Requires org:create_api_key scope (users only have user:inference) |
| CCP-04 | HIGH | Lowercase keychain service "claude-code-credentials" | Actual name has space: "Claude Code-credentials" |
| CCP-05 | MEDIUM | Reading ~/.claude/.credentials.json as primary | Keychain is SSoT; credential file is stale fallback |
| CCP-06 | HIGH | Hardcoding OAuth tokens | Tokens expire; read dynamically with cache |
| CCP-07 | HIGH | Using gh auth token in proxy/hooks | Causes process storms (recursive spawning) |
| CCP-08 | HIGH | ANTHROPIC_API_KEY set in env while having OAuth token | Auth conflict warning in Claude Code; unset it |
| CCP-09 | MEDIUM | cache_control param sent to MiniMax | MiniMax doesn't support it; remove from allowedParams |
| CCP-10 | MEDIUM | Setting ANTHROPIC_API_KEY to real key while proxy runs | Proxy forwards it to all providers, leaking key |
| CCP-11 | MEDIUM | Not handling /v1/messages/count_tokens | Causes auth failures on preflight requests |
| CCP-12 | LOW | Running proxy on 0.0.0.0 | Bind to 127.0.0.1 for security |
---
TodoWrite Task Templates
Setup, provider addition, diagnostics, and disable templates: references/task-templates.md
---
Reference Implementation
The working production deployment (Go proxy is primary):
| File | Purpose |
|---|---|
/usr/local/bin/claude-proxy | Go proxy binary (~960 lines) |
~/.claude/tools/claude-code-proxy-go/main.go | Go proxy source |
~/.claude/tools/claude-code-proxy-go/oauth_refresh.go | OAuth auto-refresh (80 lines) |
~/.claude/tools/claude-code-proxy-go/.env | Provider config (chmod 600) |
/Library/LaunchDaemons/com.terryli.claude-proxy.plist | launchd config |
~/.zshenv | Environment (ANTHROPIC_BASE_URL) |
---
Post-Change Checklist
After modifying this skill:
1. [ ] Anti-patterns table matches references/anti-patterns.md 2. [ ] Working patterns verified against proxy.py source 3. [ ] No hardcoded OAuth tokens in examples 4. [ ] Beta header version current (oauth-2025-04-20) 5. [ ] All internal links use relative paths (./references/...) 6. [ ] Link validator passes 7. [ ] Skill validator passes 8. [ ] Append changes to references/evolution-log.md
---
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Claude Code ignores ANTHROPIC_BASE_URL | Missing ANTHROPIC_API_KEY (CCP-01) | Set ANTHROPIC_API_KEY=proxy-managed in .zshenv |
| 401 Unauthorized from Anthropic | Missing anthropic-beta header (CCP-02) | Ensure proxy appends oauth-2025-04-20 |
| Keychain read returns empty | Wrong service name (CCP-04) | Use "Claude Code-credentials" (with space) |
| Proxy forwards real API key | ANTHROPIC_API_KEY set to real key (CCP-10) | Use proxy-managed sentinel value |
| count_tokens auth failure | Missing endpoint handler (CCP-11) | Proxy must handle /v1/messages/count_tokens |
| Proxy accessible from network | Bound to 0.0.0.0 (CCP-12) | Bind to 127.0.0.1 only |
| Process storms on enable | gh auth token in hooks (CCP-07) | Never call gh CLI from hooks/credential helpers |
| MiniMax returns wrong model name | MiniMax quirk | Cosmetic only; Claude Code handles it |
| Token expired after 5 min | Cache TTL (WP-05) | Normal behavior; proxy re-reads from Keychain |
| Auth conflict warning in Claude Code | ANTHROPIC_API_KEY set (CCP-08) | Unset ANTHROPIC_API_KEY in .zshenv |
| cache_control.ephemeral.scope error | MiniMax doesn't support cache_control (CCP-09) | Remove cache_control from allowedParams |
Post-Execution Reflection
After this skill completes, check before closing:
1. Did the command succeed? — If not, fix the instruction or error table that caused the failure. 2. Did parameters or output change? — If the underlying tool's interface drifted, update Usage examples and Parameters table to match. 3. Was a workaround needed? — If you had to improvise (different flags, extra steps), update this SKILL.md so the next invocation doesn't need the same workaround.
Only update if the issue is real and reproducible — not speculative.
Claude Code Proxy Anti-Patterns
<!-- PROCESS-STORM-OK: This file documents anti-patterns including gh CLI recursion as a WARNING, not as executable code --> <!-- SSoT-OK: version references below are documentation of binary analysis findings, not package versions -->
Gotchas discovered during multi-provider proxy implementation (2026-02-22 to 2026-02-23). Severity ratings indicate impact of hitting each issue without prior knowledge.
---
CCP-01: ANTHROPIC_BASE_URL Alone Without ANTHROPIC_API_KEY [HIGH]
Symptom: Claude Code shows "API Usage Billing" instead of subscription, or ignores ANTHROPIC_BASE_URL entirely. Proxy receives no requests.
Root cause: In OAuth mode, Claude Code only honors ANTHROPIC_BASE_URL when ANTHROPIC_API_KEY is also set. Without the API key, Claude Code stays in pure OAuth mode and talks directly to api.anthropic.com, bypassing the proxy.
Fix: Set ANTHROPIC_API_KEY to a dummy sentinel value.
# WRONG - Claude Code ignores ANTHROPIC_BASE_URL in OAuth mode
export ANTHROPIC_BASE_URL="http://127.0.0.1:8083"
# RIGHT - forces Claude Code to use ANTHROPIC_BASE_URL
export ANTHROPIC_BASE_URL="http://127.0.0.1:8083"
export ANTHROPIC_API_KEY="proxy-managed"The proxy detects "proxy-managed" and never forwards it to providers.
---
CCP-02: Missing anthropic-beta: oauth-2025-04-20 Header [HIGH]
Symptom: Anthropic returns 401 Unauthorized or "invalid token" when forwarding OAuth Bearer tokens through the proxy.
Root cause: Anthropic requires the anthropic-beta: oauth-2025-04-20 header alongside the Authorization: Bearer {token} header. Without this beta flag, the API endpoint does not recognize OAuth tokens and rejects them.
Fix: Append oauth-2025-04-20 to the existing beta headers. Do NOT replace them.
# WRONG - replaces all existing beta features
target_headers["anthropic-beta"] = "oauth-2025-04-20"
# RIGHT - appends to existing beta features
existing_beta = original_headers.get("anthropic-beta", "")
beta_parts = [b.strip() for b in existing_beta.split(",") if b.strip()] if existing_beta else []
if "oauth-2025-04-20" not in beta_parts:
beta_parts.append("oauth-2025-04-20")
target_headers["anthropic-beta"] = ",".join(beta_parts)Claude Code sends other beta features (like extended thinking) that must be preserved.
---
CCP-03: Using /api/oauth/claude_cli/create_api_key Endpoint [MEDIUM]
Symptom: Attempting to create an API key from an OAuth token fails with a permission error.
Root cause: The /api/oauth/claude_cli/create_api_key endpoint requires the org:create_api_key OAuth scope. Free/Pro/Max subscription users are granted user:inference scope only. This endpoint is reserved for organization administrators.
Fix: Do not attempt to convert OAuth tokens to API keys. Use the OAuth token directly with the Authorization: Bearer header plus the anthropic-beta: oauth-2025-04-20 header.
# WRONG - trying to create API key from OAuth token
curl https://api.anthropic.com/api/oauth/claude_cli/create_api_key \
-H "Authorization: Bearer $OAUTH_TOKEN"
# Returns: 403 Forbidden (insufficient scope)
# RIGHT - use OAuth token directly for inference
curl https://api.anthropic.com/v1/messages \
-H "Authorization: Bearer $OAUTH_TOKEN" \
-H "anthropic-beta: oauth-2025-04-20" \
-H "anthropic-version: 2023-06-01" \
-d '{"model": "claude-sonnet-4-6", "max_tokens": 100, "messages": [...]}'---
CCP-04: Lowercase Keychain Service Name [HIGH]
Symptom: security find-generic-password returns "The specified item could not be found in the keychain."
Root cause: The Keychain service name is "Claude Code-credentials" with a capital C, capital C, and a space before the hyphen. Using "claude-code-credentials" (all lowercase) or "claude code-credentials" (lowercase c) will not find the item.
Fix: Use the exact service name with correct casing.
# WRONG - lowercase
security find-generic-password -s "claude-code-credentials" -a "$USER" -w
# WRONG - wrong capitalization
security find-generic-password -s "Claude code-credentials" -a "$USER" -w
# RIGHT - exact casing from Claude Code binary
security find-generic-password -s "Claude Code-credentials" -a "$USER" -wThis was discovered by reverse-engineering the compiled Claude Code binary where _d() generates the service name string.
---
CCP-05: Reading ~/.claude/.credentials.json as Primary Auth Source [MEDIUM]
Symptom: Stale or expired tokens used for authentication. Intermittent auth failures that resolve after Claude Code restart.
Root cause: ~/.claude/.credentials.json is a plaintext fallback that may not be updated when Claude Code refreshes its OAuth token. The macOS Keychain is the primary (SSoT) token store. Claude Code writes to both, but the credential file may lag behind Keychain updates.
Fix: Always try Keychain first, fall back to credential file only if Keychain read fails.
# WRONG - credential file as primary
cred_path = Path.home() / ".claude" / ".credentials.json"
data = json.loads(cred_path.read_text())
token = data["claudeAiOauth"]["accessToken"]
# RIGHT - Keychain first, credential file as fallback
token_data = _read_keychain_oauth() # Keychain (SSoT)
if not token_data:
# Fallback: try credential file
for cred_path in _OAUTH_CREDENTIAL_PATHS:
...See oauth-internals.md for the full auth priority chain.
---
CCP-06: Hardcoding OAuth Tokens [HIGH]
Symptom: Proxy works for a while, then all Anthropic-bound requests return 401 Unauthorized.
Root cause: OAuth tokens have an expiresAt timestamp. Hardcoded tokens will eventually expire. Claude Code refreshes tokens automatically via the OAuth flow, but a hardcoded token in a config file or environment variable will go stale.
Fix: Read tokens dynamically from Keychain with a cache TTL.
# WRONG - hardcoded token
OAUTH_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
# WRONG - read once at startup
OAUTH_TOKEN = _read_keychain_oauth()["accessToken"]
# RIGHT - dynamic read with 5-minute cache
_OAUTH_CACHE_TTL = 300
def _get_oauth_token():
now = time.time()
if _oauth_cache["token"] and (now - _oauth_cache["fetched_at"]) < _OAUTH_CACHE_TTL:
token_expires = _oauth_cache["expires_at"]
if token_expires == 0 or (token_expires / 1000) > now:
return _oauth_cache["token"]
# Re-read from Keychain
token_data = _read_keychain_oauth()
...---
CCP-07: Using gh auth token in Proxy or Hooks [HIGH]
Symptom: System freeze. macOS becomes unresponsive. Hundreds of gh and git-credential-osxkeychain processes spawn.
Root cause: gh auth token triggers the Git credential helper, which may invoke Claude Code hooks, which may invoke gh again, creating a recursive process storm. This is especially dangerous in .zshenv or any code path that runs on every shell invocation.
Fix: Never call gh CLI in proxy code, hooks, or credential helpers. Use direct API calls or 1Password for credential resolution.
# WRONG - causes recursive process storm (DO NOT RUN)
# PROCESS-STORM-OK: documenting anti-pattern, not executable
# GH_TOKEN="$(gh auth token)"
# RIGHT - use 1Password or direct file read
GH_TOKEN=$(op read "op://vault/item/token")See the itp-hooks plugin CLAUDE.md (Process Storm Prevention section) for details.
---
CCP-08: Setting ANTHROPIC_API_KEY to Real Key While Proxy Runs [MEDIUM]
Symptom: Your real Anthropic API key appears in proxy logs for MiniMax-routed requests. Potential key leakage to third-party providers.
Root cause: Claude Code sends x-api-key header to ANTHROPIC_BASE_URL. If ANTHROPIC_API_KEY contains a real Anthropic key, the proxy receives it and may forward it to all providers, including third-party ones.
Fix: Always use the sentinel value "proxy-managed". The proxy checks for this value and strips it (proxy.py:324).
# WRONG - real key gets forwarded to all providers
export ANTHROPIC_API_KEY="sk-ant-api03-..."
# RIGHT - sentinel value, proxy knows to ignore it
export ANTHROPIC_API_KEY="proxy-managed"If you need to use a real API key for Anthropic-bound requests, set REAL_ANTHROPIC_API_KEY in the proxy's .env file instead.
---
CCP-08b: ANTHROPIC_API_KEY Set in Environment With OAuth Token [HIGH]
Symptom: Claude Code shows "Auth conflict: Both a token (claude.ai) and an API key (ANTHROPIC_API_KEY) are set" warning in the header.
Root cause: Even setting ANTHROPIC_API_KEY=proxy-managed causes a conflict warning because Claude Code detects any ANTHROPIC_API_KEY as both a token AND an API key being present.
Fix: Unset ANTHROPIC_API_KEY entirely. The Go proxy handles the sentinel internally via request header x-api-key: proxy-managed, not environment variable.
# WRONG - causes auth conflict warning
export ANTHROPIC_BASE_URL="http://127.0.0.1:8082"
export ANTHROPIC_API_KEY="proxy-managed"
# RIGHT - unset ANTHROPIC_API_KEY, proxy uses x-api-key header
export ANTHROPIC_BASE_URL="http://127.0.0.1:8082"
unset ANTHROPIC_API_KEYIn .zshenv:
# Claude Code proxy configuration
unset ANTHROPIC_API_KEY # CRITICAL: unset to avoid auth conflict
export ANTHROPIC_BASE_URL="http://127.0.0.1:8082"---
CCP-09: cache_control Parameter Sent to MiniMax [MEDIUM]
Symptom: API error: system.2.cache_control.ephemeral.scope: Extra inputs are not permitted or similar cache_control validation error.
Root cause: MiniMax (and some other Anthropic-compatible providers) does not support the cache_control parameter that Anthropic's API accepts. The Go proxy's processBody function was passing this through to MiniMax.
Fix: Remove cache_control from the allowed parameters map in the Go proxy.
// WRONG - allows cache_control through to MiniMax
var allowedParams = map[string]bool{
// ...
"cache_control": true, // Remove this
}
// RIGHT - filter out cache_control for all providers
var allowedParams = map[string]bool{
"model": true, "messages": true, "system": true, "max_tokens": true,
"metadata": true, "stream": true, "temperature": true, "top_p": true,
"top_k": true, "stop_sequences": true, "tools": true, "tool_choice": true,
"thinking": true, "prompt_truncation": true, "provider": true,
"extra": true, "force_conclusive": true, "include_usage_for": true,
"output": true, // Note: cache_control removed
}---
CCP-11: Not Handling /v1/messages/count_tokens [MEDIUM]
Symptom: Claude Code shows errors during preflight token counting, or silently falls back to estimation. Auth failures appear in proxy logs for count_tokens requests.
Root cause: Claude Code calls /v1/messages/count_tokens before sending messages to verify they fit within context limits. If the proxy only handles /v1/messages and returns 404 for count_tokens, Claude Code's preflight check fails.
Fix: Add a dedicated handler for the count_tokens endpoint with the same auth logic.
// Go proxy: handle count_tokens endpoint
case strings.HasPrefix(r.URL.Path, "/v1/messages/count_tokens"):
// Same auth logic as /v1/messages
// Return 501 for MiniMax (doesn't support it)---
CCP-12: Running Proxy on 0.0.0.0 [LOW]
Symptom: The proxy is accessible from other machines on the network. OAuth tokens in transit could be intercepted on the local network.
Root cause: Binding to 0.0.0.0 makes the proxy listen on all network interfaces, not just localhost.
Fix: Bind to 127.0.0.1 for local-only access.
# WRONG - accessible from network
uvicorn.run(app, host="0.0.0.0", port=PORT)
# RIGHT - localhost only
uvicorn.run(app, host="127.0.0.1", port=PORT)Note: The reference implementation (proxy.py:658) currently uses 0.0.0.0 for flexibility. Override with HOST=127.0.0.1 env var or update the code if security is a concern.
Evolution Log
2026-02-24: Auth Conflict & cache_control Fixes
Source: Debugging plan mode failure.
Key fixes:
- CCP-08b: Added ANTHROPIC_API_KEY unset in
.zshenv- auth conflict warning fixed - CCP-09: Removed
cache_controlfrom allowedParams - MiniMax compatibility fixed - Added OAuth auto-refresh (
oauth_refresh.go) - background token refresh every 30 minutes
Files changed:
~/.claude/tools/claude-code-proxy-go/main.go- removed cache_control, added getTokenFromKeychain~/.claude/tools/claude-code-proxy-go/oauth_refresh.go- new file for auto-refresh~/.zshenv- addedunset ANTHROPIC_API_KEY
New anti-patterns:
- CCP-08b: ANTHROPIC_API_KEY set in env with OAuth token → unset it
- CCP-09: cache_control param sent to MiniMax → remove from allowedParams
---
2026-02-23: Go-Only Implementation
Source: Migrated from Python to Go for launchd deployment.
Key changes:
- Go binary proxy deployed to
/usr/local/bin/claude-proxy(port 8082) - launchd plist for auto-restart:
/Library/LaunchDaemons/com.terryli.claude-proxy.plist - Uses
cenkalti/backoff/v4for retry logic (no Python fallback) - Python proxy deprecated
Reference implementation:
- Go proxy:
/usr/local/bin/claude-proxy - Source:
$HOME/eon/cc-skills/tools/claude-code-failover/main.go
Port configuration:
:8082- Go proxy (entry point, launchd-managed):8083- Optional failover wrapper (deprecated)
MiniMax credentials configured via launchd EnvironmentVariables.
2026-02-22: Initial skill creation
Source: Empirical discovery during proxy implementation. Key discoveries: OAuth Keychain storage ("Claude Code-credentials"), anthropic-beta: oauth-2025-04-20 header requirement, ANTHROPIC_API_KEY=proxy-managed forcing pattern. Reference implementation: $HOME/.claude/tools/claude-code-proxy/proxy.py 10 anti-patterns (CCP-01 through CCP-10) cataloged from real debugging sessions. Provider compatibility tested: MiniMax M2.5-highspeed, Real Anthropic. Binary reverse-engineering findings: _d() service name, hW() storage backend, WL="oauth-2025-04-20" constant.
Launchd Service Configuration
The Go proxy runs as a macOS launchd daemon for auto-restart on crash and boot persistence.
Why launchd?:
- Auto-restarts if proxy crashes
- Starts on system boot (RunAtLoad)
- Runs as root (needed for port 80/443 if ever needed)
- Resource limits can be enforced
Plist Location: /Library/LaunchDaemons/com.terryli.claude-proxy.plist
Full Configuration
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Unique identifier -->
<key>Label</key><string>com.terryli.claude-proxy</string>
<!-- Program to run -->
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/claude-proxy</string>
</array>
<!-- Start on boot -->
<key>RunAtLoad</key><true/>
<!-- Auto-restart on crash (any non-zero exit) -->
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key><false/>
</dict>
<!-- Environment variables passed to the proxy -->
<key>EnvironmentVariables</key>
<dict>
<key>PORT</key><string>8082</string>
<key>HAIKU_PROVIDER_API_KEY</key><string>sk-cp-49GSmHBfC0c65pvYrFoZZy8xEjOVxXrUiTIJn65ynTvgzoiGEvM7q9V5dYYe6PwjMfZaGelKoE2oTq1hKnttv8ODm36O8gklUIi1eaTVOKbPILlIPfNcM0E</string>
<key>HAIKU_PROVIDER_BASE_URL</key><string>https://api.minimax.io/anthropic</string>
<key>ANTHROPIC_DEFAULT_HAIKU_MODEL</key><string>claude-haiku-4-5-20251001</string>
</dict>
<!-- Resource limits -->
<key>SoftResourceLimits</key>
<dict>
<key>NumberOfFiles</key><integer>65536</integer>
</dict>
<!-- Log output -->
<key>StandardOutPath</key><string>/Users/terryli/.claude/logs/proxy-stdout.log</string>
<key>StandardErrorPath</key><string>/Users/terryli/.claude/logs/proxy-stderr.log</string>
</dict>
</plist>Key launchd Properties
| Key | Purpose | Value for Proxy |
|---|---|---|
Label | Unique identifier | com.terryli.claude-proxy |
ProgramArguments | Command + args | ["/usr/local/bin/claude-proxy"] |
RunAtLoad | Start at boot | true |
KeepAlive/SuccessfulExit | Restart on crash | false (always restart) |
EnvironmentVariables | Env vars for proxy | PORT, API keys, etc. |
SoftResourceLimits/NumberOfFiles | FD limit | 65536 |
StandardOutPath | stdout log | /Users/terryli/.claude/logs/proxy-stdout.log |
StandardErrorPath | stderr log | /Users/terryli/.claude/logs/proxy-stderr.log |
Commands
# Install plist (one-time)
sudo cp /path/to/com.terryli.claude-proxy.plist /Library/LaunchDaemons/
sudo chown root:wheel /Library/LaunchDaemons/com.terryli.claude-proxy.plist
sudo chmod 644 /Library/LaunchDaemons/com.terryli.claude-proxy.plist
# Start (load)
sudo launchctl load -w /Library/LaunchDaemons/com.terryli.claude-proxy.plist
# Stop (unload)
sudo launchctl unload -w /Library/LaunchDaemons/com.terryli.claude-proxy.plist
# Restart
sudo launchctl unload -w /Library/LaunchDaemons/com.terryli.claude-proxy.plist
sudo launchctl load -w /Library/LaunchDaemons/com.terryli.claude-proxy.plist
# Check status
sudo launchctl list | grep claude-proxy
# View running PID info
ps aux | grep claude-proxy
# View logs
tail -f /Users/terryli/.claude/logs/proxy-stdout.log
tail -f /Users/terryli/.claude/logs/proxy-stderr.log
# Test health
curl -s http://127.0.0.1:8082/health | jq .Verification Checklist
# 1. Plist exists
ls -la /Library/LaunchDaemons/com.terryli.claude-proxy.plist
# 2. Loaded in launchd
sudo launchctl list | grep claude-proxy
# 3. Process running
ps aux | grep claude-proxy | grep -v grep
# 4. Port listening
lsof -i :8082
# 5. Health endpoint responds
curl -s http://127.0.0.1:8082/health | jq .Debugging launchd Issues
# Check if plist is valid
plutil -lint /Library/LaunchDaemons/com.terryli.claude-proxy.plist
# View full launchd logs
log show --predicate 'process == "claude-proxy"' --last 5m
# Check stderr for errors
tail -50 /Users/terryli/.claude/logs/proxy-stderr.logOAuth Token Auto-Refresh
The Go proxy automatically refreshes OAuth tokens before they expire.
Use case: Prevent auth failures when tokens expire during long-running Claude Code sessions.
Implementation: Background goroutine runs every 30 minutes, checks if token expires within 5 minutes, and refreshes using the refresh token.
// oauth_refresh.go
func startTokenRefreshLoop() {
ticker := time.NewTicker(30 * time.Minute)
defer ticker.Stop()
refreshTokenIfNeeded() // Run immediately on startup
for {
select {
case <-ticker.C:
refreshTokenIfNeeded()
}
}
}
func refreshTokenIfNeeded() {
// Check if token expires within 5 minutes
needsRefresh := oauthCache.token == "" ||
(!oauthCache.expiresAt.IsZero() && time.Now().Add(5*time.Minute).After(oauthCache.expiresAt))
if !needsRefresh {
return // Token still valid
}
// Try API refresh first
newToken, newRefreshToken, newExpiresAt, err := refreshOAuthToken(refreshToken)
if err != nil {
// Fallback: get fresh token from Keychain
tryKeychainRefresh()
return
}
// Update cache and persist
oauthCache.token = newToken
oauthCache.refreshToken = newRefreshToken
oauthCache.expiresAt = newExpiresAt
saveOAuthToFile(newToken, newRefreshToken, newExpiresAt)
}Refresh Logic
1. Runs every 30 minutes in background goroutine 2. Checks if token expires within 5 minutes 3. If refresh token available -> calls Anthropic OAuth refresh endpoint 4. If API fails -> falls back to Keychain retrieval 5. Saves new tokens to .oauth.json for persistence
Key Files
oauth_refresh.go- Auto-refresh logic (~80 lines)main.go- Token cache + refreshOAuthToken function
<!-- # SSoT-OK: version references are documentation of binary analysis findings, not package versions -->
Claude Code OAuth Internals
Deep dive into how Claude Code stores, retrieves, and transmits OAuth tokens. Based on reverse-engineering of the Claude Code v2.1.50 compiled binary and empirical testing (2026-02-22).
---
macOS Keychain Storage
Claude Code stores OAuth credentials in the macOS Keychain using the security CLI.
| Field | Value |
|---|---|
| Service | "Claude Code-credentials" |
| Account | Current macOS username (getpass.getuser()) |
| Type | Generic password |
| Content | JSON string (see Token JSON Envelope below) |
Reading from Keychain
# CLI read (returns JSON string)
security find-generic-password \
-s "Claude Code-credentials" \
-a "$(whoami)" \
-w# Python read
import subprocess, json, getpass
result = subprocess.run(
["security", "find-generic-password",
"-s", "Claude Code-credentials",
"-a", getpass.getuser(), "-w"],
capture_output=True, text=True, timeout=5, check=False,
)
if result.returncode == 0:
data = json.loads(result.stdout.strip())
oauth = data.get("claudeAiOauth")
if oauth and oauth.get("accessToken"):
token = oauth["accessToken"]Keychain Item Metadata
The Keychain item also stores metadata accessible via security find-generic-password -s "Claude Code-credentials" -a "$(whoami)" (without -w):
svce(service):"Claude Code-credentials"acct(account): usernamecdat/mdat: creation/modification timestamps- Access control: application-specific (Claude Code binary)
Note: First-time Keychain access from a proxy may trigger a macOS authorization prompt. The user must click "Always Allow" or "Allow" to grant the proxy's Python process access.
---
Token JSON Envelope
The Keychain stores a JSON object with the following structure:
{
"claudeAiOauth": {
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "rt_abc123...",
"expiresAt": 1740268800000,
"subscriptionType": "claude_pro_2025",
"accountUuid": "uuid-...",
"organizationUuid": "uuid-..."
}
}| Field | Type | Notes |
|---|---|---|
accessToken | string (JWT) | Bearer token for API calls |
refreshToken | string | Used to obtain new access tokens |
expiresAt | number | Milliseconds since Unix epoch |
subscriptionType | string | Plan identifier (e.g., claude_pro_2025) |
Expiration Handling
# expiresAt is in MILLISECONDS
now = time.time() # seconds
token_expires_ms = oauth_data.get("expiresAt", 0)
if token_expires_ms == 0:
# No expiry set, assume valid
pass
elif (token_expires_ms / 1000) > now:
# Token is still valid
pass
else:
# Token expired, need to re-read from Keychain
# Claude Code may have refreshed it in the background
pass---
Binary Reverse-Engineering Findings
From decompiling the Claude Code v2.1.50 native binary:
| Symbol | Purpose |
|---|---|
_d() | Generates the Keychain service name string ("Claude Code-credentials") |
hW() | Storage backend selector (Keychain on macOS, different on Linux) |
WL | Constant: "oauth-2025-04-20" (the required beta header value) |
Key Observations
1. Service name construction: _d() concatenates the app name "Claude Code" with "-credentials". This is why the service name has a space (from the app name) before the hyphen.
2. Beta header: The WL="oauth-2025-04-20" constant confirms this header is hardcoded in the binary. It is not dynamically generated or versioned per-request.
3. Storage abstraction: hW() provides a platform-agnostic credential storage interface. On macOS it uses Keychain; on Linux it may use libsecret or a file-based fallback.
---
CLAUDE_CODE_OAUTH_TOKEN Environment Variable
Found in the decompiled binary: Claude Code checks for CLAUDE_CODE_OAUTH_TOKEN as an alternative OAuth token source.
# Override OAuth token via environment (bypasses Keychain)
export CLAUDE_CODE_OAUTH_TOKEN="eyJhbG..."Use cases:
- CI/CD environments without Keychain access
- Testing with specific tokens
- Headless Linux servers
Warning: This token is not auto-refreshed. It will expire based on its expiresAt value. Use for short-lived automation only.
---
Cache TTL Pattern
The proxy caches Keychain reads to avoid subprocess overhead on every request.
_oauth_cache: dict = {"token": None, "expires_at": 0.0, "fetched_at": 0.0}
_OAUTH_CACHE_TTL = 300 # 5 minutes
def _get_oauth_token() -> str | None:
now = time.time()
# Return cached if fresh and not expired
if _oauth_cache["token"] and (now - _oauth_cache["fetched_at"]) < _OAUTH_CACHE_TTL:
token_expires = _oauth_cache["expires_at"]
if token_expires == 0 or (token_expires / 1000) > now:
return _oauth_cache["token"]
# Re-read from Keychain
token_data = _read_keychain_oauth()
if token_data:
_oauth_cache["token"] = token_data["accessToken"]
_oauth_cache["expires_at"] = token_data.get("expiresAt", 0)
_oauth_cache["fetched_at"] = now
return token_data["accessToken"]
# Fallback to credential file...Why 5 minutes?: Balance between freshness and performance. Keychain reads spawn a subprocess (security CLI), which costs ~50ms. At proxy scale (dozens of requests/minute), this adds up. 5 minutes is short enough that expired tokens are caught quickly but long enough to avoid constant subprocess overhead.
---
Auth Header Format
For Anthropic API calls with OAuth tokens:
POST /v1/messages HTTP/1.1
Host: api.anthropic.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
anthropic-beta: oauth-2025-04-20
anthropic-version: 2023-06-01
Content-Type: application/jsonBoth the Authorization: Bearer header AND the anthropic-beta: oauth-2025-04-20 header are required. Missing either one results in 401 Unauthorized.
---
Credential File Fallback
Location: ~/.claude/.credentials.json (permissions: chmod 0600)
{
"claudeAiOauth": {
"accessToken": "eyJhbG...",
"refreshToken": "rt_...",
"expiresAt": 1740268800000
}
}This file mirrors the Keychain content but is a plaintext fallback. It exists for:
- Linux systems without Keychain
- Debugging and inspection
- Recovery if Keychain access is broken
Security note: This file contains plaintext tokens. Ensure chmod 0600 permissions. The Keychain is the preferred and more secure storage.
---
OAuth Token Lifecycle
1. User runs `claude` CLI for first time
2. Claude Code opens browser for OAuth consent
3. Anthropic returns access_token + refresh_token
4. Claude Code stores both in Keychain (and credential file)
5. On each API call, Claude Code reads from Keychain
6. When access_token expires, Claude Code uses refresh_token to get new one
7. New tokens stored back to Keychain
8. Proxy reads from Keychain with 5-min cache, tracks expiresAtImportant: The proxy does NOT handle token refresh. Claude Code handles refresh automatically. The proxy just reads whatever current token is in Keychain.
---
The OAuth Lockdown Context (January 2026)
Anthropic banned third-party tools from using OAuth tokens in January 2026. Key details:
- OAuth tokens from Free/Pro/Max plans are scoped to Claude Code and Claude.ai only
- Server-side validation checks client identity and request origin
- Third-party tools that spoofed Claude Code headers were blocked
Why the proxy still works: The proxy is a localhost passthrough, not a third-party tool. Claude Code itself makes the API calls, which route through the proxy to api.anthropic.com. Anthropic's servers see a legitimate Claude Code OAuth request.
Risk: If Anthropic adds certificate pinning or response signing, the proxy approach could break. As of 2026-02-22, no such validation exists.
Provider Compatibility Matrix
Tested Anthropic-compatible providers for use with claude-code-proxy (2026-02-22).
---
Tested Providers
MiniMax highspeed
| Field | Value |
|---|---|
| Endpoint | https://api.minimax.io/anthropic/v1/messages |
| Auth | API key via Authorization: Bearer |
| Base URL for proxy | https://api.minimax.io/anthropic |
| Streaming | Supported |
| Token counting | Not supported (proxy returns 501) |
Quirks:
1. Model name in response: Returns "model": "MiniMax" instead of the requested model name. Cosmetic only; Claude Code handles this gracefully.
2. Extra `thinking` block: MiniMax includes a thinking content block with a signature field in responses. Claude Code ignores unknown content block types.
3. Extra `base_resp` field: Responses include a base_resp metadata object not present in Anthropic responses. No functional impact.
4. API key source: 1Password at op://Claude Automation/MiniMax API - High-Speed Plan/password
Real Anthropic
| Field | Value |
|---|---|
| Endpoint | https://api.anthropic.com/v1/messages |
| Auth | OAuth Bearer token + anthropic-beta: oauth-2025-04-20 OR x-api-key |
| Base URL for proxy | https://api.anthropic.com |
| Streaming | Supported |
| Token counting | Supported (/v1/messages/count_tokens) |
Notes:
- OAuth requires the
anthropic-beta: oauth-2025-04-20header (see CCP-02) - API key auth uses
x-api-keyheader (notAuthorization: Bearer) - Both auth methods supported simultaneously (proxy tries OAuth first)
---
Generic Provider Requirements
To be compatible with claude-code-proxy, a provider must:
1. Support `/v1/messages` endpoint with Anthropic's request/response schema 2. Support streaming via text/event-stream SSE format 3. Accept `Authorization: Bearer {api_key}` for authentication 4. Return Anthropic-compatible response JSON with content, model, usage fields
Optional but Recommended
/v1/messages/count_tokenssupport (proxy returns 501 if missing)- Same error response format (
{"type": "error", "error": {"type": "...", "message": "..."}}) - Rate limit headers (
retry-after) for automatic backoff
---
Adding a New Provider
1. Verify the provider has an Anthropic-compatible endpoint 2. Add env vars to proxy .env (Python) or launchd plist (Go):
# Python proxy (.env)
HAIKU_PROVIDER_API_KEY=your_key_here
HAIKU_PROVIDER_BASE_URL=https://api.example.com/anthropic
# Go proxy (add to EnvironmentVariables in plist)
<key>HAIKU_PROVIDER_API_KEY</key><string>your_key_here</string>
<key>HAIKU_PROVIDER_BASE_URL</key><string>https://api.example.com/anthropic</string>1. Set the model tier mapping:
ANTHROPIC_DEFAULT_HAIKU_MODEL=example-model-name1. Restart the proxy:
# Python (port 3000)
cd $HOME/.claude/tools/claude-code-proxy
source .venv/bin/activate
pkill -f proxy.py
python proxy.py &
# Go (port 8082) - via launchd
sudo launchctl unload -w /Library/LaunchDaemons/com.terryli.claude-proxy.plist
sudo launchctl load -w /Library/LaunchDaemons/com.terryli.claude-proxy.plist
# Failover (port 8083) - via failover wrapper
cd $HOME/eon/cc-skills/tools/claude-code-failover
go build -o proxy-failover .
pkill -f proxy-failover
nohup ./proxy-failover > ~/.claude/logs/proxy-failover.log 2>&1 &1. Test:
# Python proxy (3000)
curl -s http://127.0.0.1:3000/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: any-value" \
-H "anthropic-version: 2023-06-01" \
-d '{"model": "example-model-name", "max_tokens": 100, "messages": [{"role": "user", "content": "Hello"}]}'
# Go proxy (8082)
curl -s http://127.0.0.1:8082/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: any-value" \
-H "anthropic-version: 2023-06-01" \
-d '{"model": "example-model-name", "max_tokens": 100, "messages": [{"role": "user", "content": "Hello"}]}'
# Failover wrapper (8083)
curl -s http://127.0.0.1:8083/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: any-value" \
-H "anthropic-version: 2023-06-01" \
-d '{"model": "example-model-name", "max_tokens": 100, "messages": [{"role": "user", "content": "Hello"}]}'1. Update this file with the provider's compatibility details.
---
Known Incompatible Approaches
| Approach | Why It Fails |
|---|---|
| LiteLLM standalone | No OAuth forwarding; forces API key billing |
| Cloudflare Worker proxy | Adds network hop; may strip OAuth headers |
| HTTPS_PROXY env var | Cannot inspect request bodies for model routing |
See the OAuth proxy research doc for a full evaluation of 11 approaches.
TodoWrite Task Templates
Template A - Set Up Go Proxy
1. [Preflight] Verify Go 1.21+ installed: go version
2. [Execute] Build Go proxy to /usr/local/bin/claude-proxy
3. [Execute] Create launchd plist at /Library/LaunchDaemons/com.terryli.claude-proxy.plist
4. [Execute] Load launchd: sudo launchctl load -w /Library/LaunchDaemons/com.terryli.claude-proxy.plist
5. [Execute] Add to ~/.zshenv: export ANTHROPIC_BASE_URL="http://127.0.0.1:8082"
6. [Execute] Add to ~/.zshenv: export ANTHROPIC_API_KEY="proxy-managed"
7. [Verify] Health: curl -s http://127.0.0.1:8082/health
8. [Verify] Restart Claude CodeTemplate B - Add New Provider
1. [Preflight] Verify provider supports /v1/messages
2. [Execute] Edit launchd plist, add to EnvironmentVariables:
- PROVIDER_API_KEY
- PROVIDER_BASE_URL
3. [Execute] Reload: sudo launchctl unload -w ... && sudo launchctl load -w ...
4. [Verify] Test: curl http://127.0.0.1:8082/healthTemplate C - Diagnose Proxy Auth Failure
1. Check running: sudo launchctl list | grep claude-proxy
2. Check port: lsof -i :8082
3. Check .zshenv: grep ANTHROPIC ~/.zshenv
4. Check logs: tail -50 /Users/terryli/.claude/logs/proxy-stdout.log
5. Health check: curl http://127.0.0.1:8082/healthTemplate D - Disable Proxy
1. Comment out ANTHROPIC_BASE_URL in ~/.zshenv
2. Unload: sudo launchctl unload -w /Library/LaunchDaemons/com.terryli.claude-proxy.plist
3. Restart Claude Code