Caution
This documentation is for EDUCATIONAL PURPOSES ONLY. Using these methods may violate Anthropic's Terms of Service.
RISKS INCLUDE:
- π« Immediate account suspension or termination
- π« Loss of access to ALL Claude services
- π« Potential legal action from Anthropic
This is NOT intended for:
- Commercial SaaS redistribution
- Reselling access to Anthropic's services
- Production applications
By proceeding, you acknowledge these risks and accept full responsibility.
See LEGAL.md for detailed information.
β οΈ Legal Disclaimer- π§ Prerequisites
- π Overview
- π° Economic Context
- ποΈ Pattern 1: OAuth + Beta Headers
- ποΈ Pattern 2: CLI Proxy Server
- π Pattern Comparison
- π€ Which Pattern Should I Use?
- π Security Best Practices
- π Existing Implementations
- π€ AI Prompts for Custom Implementation
- π Failure Modes & Mitigations
- π Further Reading
- π€ Contributing
Before proceeding, ensure you have:
- β Active Claude Pro or Max subscription ($20-200/month)
- β Claude Code CLI installed and authenticated (Installation Guide)
- β Understanding of OAuth 2.0 flows
- β Basic HTTP/API knowledge
pip install httpx # or: uv pip install httpxcurl -fsSL https://bun.sh/install | bash
bun --version # Verify installation# Run this to confirm Claude CLI is working
claude --version
claude "Hello, world" --printThis repository documents two implementation patterns for accessing Anthropic Claude models using your Claude Max/Pro subscription programmatically, instead of the pay-per-token API.
flowchart LR
subgraph "Your Application"
A[Your Code]
end
subgraph "Pattern Choice"
B{Which Pattern?}
end
subgraph "Pattern 1: OAuth"
C[Extract OAuth Token]
D[Add Beta Headers]
E[Direct API Call]
end
subgraph "Pattern 2: CLI Proxy"
F[HTTP Server]
G[Spawn CLI Subprocess]
H[Parse JSON Output]
end
subgraph "Anthropic"
I[Claude API]
end
A --> B
B -->|Fast, High Risk| C
B -->|Stable, Medium Risk| F
C --> D --> E --> I
F --> G --> H --> I
π Expand: The Economic Context (The "Buffet Analogy")
Anthropic offers two pricing modelsβthink of it like ordering Γ la carte vs. an all-you-can-eat buffet:
| Model | API (Pay-Per-Token) | Claude Max Subscription |
|---|---|---|
| Claude Opus 4.5 | $15/M input, $75/M output | Unlimited* (flat monthly fee) |
| Claude Sonnet 4.5 | $3/M input, $15/M output | Unlimited* (flat monthly fee) |
*Within fair use policy
For a developer working extensively with Claude:
| Metric | Value |
|---|---|
| Monthly Usage | ~10M tokens (typical heavy usage) |
| API Cost | ~$150-450/month |
| Subscription Cost | $20-200/month |
| Potential Savings | 5-20x |
"In a month of Claude Code, it's easy to use so many LLM tokens that it would have cost you more than $1,000 if you'd paid via the API." β Hacker News Discussion, January 2026
Warning
ToS Risk: HIGH β This pattern spoofs Claude Code identity. Use at your own risk.
A technique that extracts OAuth credentials from the official Claude Code CLI and uses them to make direct API calls.
sequenceDiagram
participant App as Your App
participant Auth as Auth File<br/>(~/.local/share/opencode/auth.json)
participant Refresh as Token Refresh<br/>(console.anthropic.com)
participant API as Anthropic API
App->>Auth: 1. Extract OAuth credentials
Auth-->>App: access_token, refresh_token, expires
alt Token Expired
App->>Refresh: 2. POST /v1/oauth/token
Refresh-->>App: New tokens
App->>Auth: Save updated tokens
end
App->>API: 3. POST /v1/messages<br/>+ Bearer token<br/>+ Beta headers<br/>+ System prefix
API-->>App: Response
1. OAuth Token Extraction
from pathlib import Path
import json
from typing import Optional
# Claude Code stores auth in these locations
OPENCODE_AUTH_PATHS = [
Path.home() / ".local" / "share" / "opencode" / "auth.json",
Path.home() / ".opencode" / "data" / "auth.json",
Path.home() / ".config" / "opencode" / "auth.json",
]
def load_oauth_credentials() -> Optional[dict]:
"""Extract OAuth credentials from Claude Code auth file."""
for auth_path in OPENCODE_AUTH_PATHS:
if auth_path.exists():
data = json.loads(auth_path.read_text())
anthropic_auth = data.get("anthropic", {})
if anthropic_auth.get("type") == "oauth":
return {
"access": anthropic_auth.get("access"),
"refresh": anthropic_auth.get("refresh"),
"expires": anthropic_auth.get("expires", 0),
"auth_path": auth_path,
}
return None2. Token Refresh
import httpx
import time
async def refresh_oauth_token(refresh_token: str) -> Optional[dict]:
"""Refresh expired OAuth token."""
async with httpx.AsyncClient() as client:
response = await client.post(
"https://console.anthropic.com/v1/oauth/token",
json={
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": "<CLIENT_ID_FROM_OFFICIAL_APP>",
},
)
if response.is_success:
data = response.json()
return {
"access": data.get("access_token"),
"refresh": data.get("refresh_token"),
"expires": int(time.time() * 1000) + data.get("expires_in", 3600) * 1000,
}
return None3. Identity Spoofing Headers
# These headers tell Anthropic "I am Claude Code CLI"
CLAUDE_CODE_BETA_FLAGS = (
"oauth-2025-04-20,"
"claude-code-20250219,"
"interleaved-thinking-2025-05-14,"
"fine-grained-tool-streaming-2025-05-14"
)
# System prompt MUST start with this prefix
CLAUDE_CODE_SYSTEM_PREFIX = "You are Claude Code, Anthropic's official CLI for Claude."
def build_headers(access_token: str) -> dict:
return {
"authorization": f"Bearer {access_token}",
"anthropic-version": "2023-06-01",
"anthropic-beta": CLAUDE_CODE_BETA_FLAGS,
"content-type": "application/json",
}| β Pros | β Cons |
|---|---|
| No CLI dependency required | Needs OAuth extraction logic |
| Direct API calls (fastest) | Tokens expire, need refresh |
| Fallback to API key possible | HIGH ToS violation risk |
| Works with any HTTP client | OAuth endpoints may change |
Note
ToS Risk: MEDIUM β Uses official CLI, technically a wrapper.
A technique that spawns the official Claude Code CLI as a subprocess and wraps it in an OpenAI-compatible HTTP API.
sequenceDiagram
participant Client as Any OpenAI Client
participant Proxy as Proxy Server<br/>(localhost:8765)
participant CLI as Claude CLI<br/>(subprocess)
participant API as Anthropic API
Client->>Proxy: POST /v1/chat/completions<br/>(OpenAI format)
Proxy->>CLI: Spawn: claude --print --output-format json
CLI->>API: Official API call<br/>(auth handled by CLI)
API-->>CLI: Response
CLI-->>Proxy: JSON output
Proxy->>Proxy: Convert to OpenAI format
Proxy-->>Client: OpenAI-compatible response
1. Spawn Claude CLI Subprocess
async function spawnClaude(prompt: string, system?: string): Promise<ClaudeResponse> {
const args = ['--print', '--output-format', 'json'];
if (system) {
args.push('--system-prompt', system);
}
args.push(prompt);
const proc = Bun.spawn(['claude', ...args], {
stdout: 'pipe',
stderr: 'pipe',
});
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
const exitCode = await proc.exited;
if (exitCode !== 0) {
throw new Error(`Claude CLI failed: ${stderr}`);
}
return JSON.parse(stdout);
}
interface ClaudeResponse {
type: 'result' | 'error';
duration_ms: number;
result: string;
usage: {
input_tokens: number;
output_tokens: number;
};
}2. OpenAI-Compatible HTTP Server
function convertToOpenAIFormat(claude: ClaudeResponse, model: string) {
return {
id: `chatcmpl-${Date.now()}`,
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: model,
choices: [{
index: 0,
message: { role: 'assistant', content: claude.result },
finish_reason: claude.type === 'error' ? 'error' : 'stop',
}],
usage: {
prompt_tokens: claude.usage.input_tokens,
completion_tokens: claude.usage.output_tokens,
total_tokens: claude.usage.input_tokens + claude.usage.output_tokens,
},
};
}
const server = Bun.serve({
port: 8765,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === '/health') {
return Response.json({ status: 'ok' });
}
if (url.pathname === '/v1/chat/completions' && req.method === 'POST') {
const body = await req.json();
const prompt = body.messages.map((m: any) => `${m.role}: ${m.content}`).join('\n');
const claudeResponse = await spawnClaude(prompt, body.system);
return Response.json(convertToOpenAIFormat(claudeResponse, body.model));
}
return new Response('Not Found', { status: 404 });
},
});
console.log(`Proxy running at http://localhost:${server.port}`);3. Run as Background Service
# Using tmux
tmux new-session -d -s claude-proxy "bun run server.ts"
# Using systemd (Linux)
# Create /etc/systemd/system/claude-proxy.service
# Using Docker
docker run -d --name claude-proxy -p 8765:8765 your-image| β Pros | β Cons |
|---|---|
| Uses official CLI (no spoofing) | Requires Claude CLI installed |
| Automatic auth handling | Subprocess overhead (~100ms) |
| Works with ANY OpenAI client | Slower than direct API |
| Lower ToS risk | CLI updates may break it |
| Aspect | OAuth + Headers | CLI Proxy |
|---|---|---|
| Setup Complexity | Medium (OAuth flow) | Low (spawn CLI) |
| Performance | β‘ Fast (direct API) | π’ Medium (subprocess) |
| Stability | π΄ Low (beta flags change) | π‘ Medium (CLI updates) |
| Auth Handling | Manual (refresh logic) | Automatic (CLI handles) |
| ToS Risk | π΄ HIGH (spoofing) | π‘ MEDIUM (wrapper) |
| Best For | Maximum performance | Stability & safety |
flowchart TD
A[Start] --> B{Need maximum<br/>performance?}
B -->|Yes| C{Comfortable with<br/>HIGH ToS risk?}
B -->|No| D{Need OpenAI<br/>compatibility?}
C -->|Yes| E[Use Pattern 1:<br/>OAuth + Headers]
C -->|No| F[Use Pattern 2:<br/>CLI Proxy]
D -->|Yes| F
D -->|No| G{Have Claude CLI<br/>installed?}
G -->|Yes| F
G -->|No| H[Install Claude CLI first<br/>or use Pattern 1]
style E fill:#ffcccc
style F fill:#ffffcc
Quick Decision:
- ποΈ Need speed + accept high risk? β Pattern 1 (OAuth)
- π‘οΈ Prefer stability + lower risk? β Pattern 2 (CLI Proxy)
- π Need OpenAI API compatibility? β Pattern 2 (CLI Proxy)
Important
Never commit credentials to version control!
# β BAD: Hardcoded paths
AUTH_FILE = "~/.local/share/opencode/auth.json"
# β
GOOD: Environment variables
import os
AUTH_FILE = os.environ.get("CLAUDE_AUTH_FILE", "~/.local/share/opencode/auth.json")- π Use environment variables for sensitive paths
- π Never commit
auth.jsonto git (add to.gitignore) - π Implement rate limiting to avoid detection
- π Monitor for API changes that might break implementations
- π Use secure storage (keyring, vault) for production
# Auth files - NEVER commit these
auth.json
*.token
.env
.env.localThese patterns are already implemented by various projects. Review these for production-ready code:
| Project | Pattern | Language | Notes |
|---|---|---|---|
| horselock/claude-code-proxy | OAuth Proxy | Node.js | 67β, actively maintained |
| mergd/ccproxy | CLI Proxy | Go | Cursor-specific |
| phrazzld/switchboard | Service Proxy | Python | Multi-provider |
π‘ Tip: This documentation focuses on explaining how they work, not providing another implementation. Use the projects above for production code.
Copy-paste these prompts to ask an AI (Claude, GPT-4, etc.) to build a custom implementation.
π Prompt: OAuth + Headers Pattern (Python)
I need you to build a Python module that implements the Anthropic Claude OAuth access pattern.
## Requirements
1. Extract OAuth credentials from Claude Code CLI auth file:
- ~/.local/share/opencode/auth.json
- ~/.opencode/data/auth.json
- ~/.config/opencode/auth.json
2. Implement automatic token refresh when expired (60s buffer)
3. Make API calls to anthropic.com/v1/messages with these headers:
- authorization: Bearer {access_token}
- anthropic-version: 2023-06-01
- anthropic-beta: oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14
4. System prompt MUST start with: "You are Claude Code, Anthropic's official CLI for Claude."
5. Fallback to API key authentication if OAuth fails
## Technical Details
- Use httpx for async HTTP calls
- Token refresh: POST https://console.anthropic.com/v1/oauth/token
- Save updated tokens back to auth file
## Expected API
async def call_anthropic(model: str, prompt: str, system_prompt: Optional[str] = None) -> dict
Please provide complete, working code with proper error handling.
π Prompt: CLI Proxy Pattern (TypeScript/Bun)
I need you to build a Claude Code CLI proxy server that exposes an OpenAI-compatible API.
## Requirements
1. Create an HTTP server (port 8765) with endpoints:
- GET /health - Health check
- GET /v1/models - List available models
- POST /v1/chat/completions - OpenAI-compatible chat
2. For /v1/chat/completions:
- Accept OpenAI format: {model, messages, temperature, max_tokens, system}
- Spawn 'claude' CLI: --print --output-format json [--system-prompt SYSTEM] PROMPT
- Convert response to OpenAI format
3. Technologies:
- Bun.serve() for HTTP server
- Bun.spawn() for CLI subprocess
- TypeScript for type safety
## Error Handling
- CLI failure β 500 error with stderr
- Invalid JSON β 500 error with debug info
- Timeout β 504 error
Please provide complete, working TypeScript code.
π Prompt: Multi-Language Implementation
Help me create implementations in multiple languages for both Claude subscription patterns:
## Languages
- Python (httpx)
- TypeScript/Bun
- Go (net/http)
- Rust (reqwest, tokio)
## For Each Language
- Complete, runnable code
- Dependencies file (requirements.txt, package.json, go.mod, Cargo.toml)
- Run command
- Test curl command
- Error handling
## Folder Structure
/pattern1-oauth/{python,typescript,go,rust}
/pattern2-cli-proxy/{python,typescript,go,rust}
Please provide production-ready implementations with proper error handling.
| Failure | Detection | Mitigation |
|---|---|---|
| Token expired | 401 Unauthorized |
Auto-refresh with refresh token |
| Beta flags changed | 400/403 errors |
Fallback to API key; check GitHub issues |
| CLI not found | Spawn error | Prompt user to install Claude CLI |
| CLI update breaks | JSON parse error | Version pinning, graceful degradation |
| Rate limit | 429 Too Many Requests |
Exponential backoff, request queue |
| Session expired | 401 after refresh |
Re-authenticate via Claude CLI |
# Check if OAuth token is valid
import time
creds = load_oauth_credentials()
if creds:
expires_at = creds["expires"] / 1000 # Convert ms to seconds
if time.time() > expires_at:
print("Token expired, needs refresh")
else:
print(f"Token valid for {(expires_at - time.time()) / 60:.1f} more minutes")- Model Context Protocol (MCP)
- OpenRouter - API aggregation alternative
- OpenCode - AI-powered code editor
Contributions welcome for:
- π Additional language implementations
- π Bug fixes and error handling improvements
- π Documentation clarifications
- π Updates when Anthropic changes their APIs
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
If a pattern stops working:
- Open an issue with error logs
- Include: pattern used, error message, Claude CLI version
- Check if others have reported the same issue
MIT License - Educational use only.
This documentation is provided for learning purposes. Usage of these techniques may violate Anthropic's Terms of Service. Users are responsible for their own compliance.
- Community members who discovered and documented these patterns
- Maintainers of existing proxy implementations
- Anthropic for Claude and Claude Code
Built for the community to understand, learn, and make informed decisions.
β Star this repo if it helped you!