From ccproxy
Guides users through ccproxy as an OpenAI-compatible and Anthropic-compatible LLM API server with SDK integration, OAuth authentication, sentinel key substitution, model routing, and troubleshooting.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ccproxy:using-ccproxy-apiThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
ccproxy exposes an OpenAI-compatible and Anthropic-compatible API via a mitmproxy-based interceptor. Any SDK or HTTP client that supports custom `base_url` can use it.
ccproxy exposes an OpenAI-compatible and Anthropic-compatible API via a mitmproxy-based interceptor. Any SDK or HTTP client that supports custom base_url can use it.
Add ccproxy as a flake input and enable the Home Manager module:
# flake.nix
inputs.ccproxy.url = "github:starbaser/ccproxy";
# home configuration
programs.ccproxy = {
enable = true;
settings = {
# Override defaults here (port, providers, transforms, etc.)
};
};
This installs the ccproxy binary, generates ~/.config/ccproxy/ccproxy.yaml from Nix, and creates a systemd --user service that auto-restarts on config changes.
# Clone and enter devShell
git clone https://github.com/starbaser/ccproxy
cd ccproxy
nix develop # or: direnv allow
# Initialize config
ccproxy init # copies template to ~/.config/ccproxy/ccproxy.yaml
ccproxy init --force # overwrites existing config
# Edit config
$EDITOR ~/.config/ccproxy/ccproxy.yaml
# Start
ccproxy start
Each project can run its own ccproxy with isolated config, port, and transforms via the flake's mkConfig. Use ccproxy.defaultSettings.settings (top-level, no ${system} selector needed) as the base to inherit all defaults (hooks, shaping, providers, otel).
# project flake.nix
{
inputs.ccproxy.url = "github:starbaser/ccproxy";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils, ccproxy }:
let
defaults = ccproxy.defaultSettings.settings;
in
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
proxyConfig = ccproxy.lib.${system}.mkConfig {
settings = {
port = 4010; # per-project: use 4010+ to avoid collisions
inspector = {
port = 8090;
cert_dir = "./.ccproxy";
};
lightllm = {
transforms = [
{ match_path = "/v1/messages"; action = "redirect";
dest_provider = "anthropic"; dest_host = "api.anthropic.com";
dest_path = "/v1/messages"; }
] ++ defaults.lightllm.transforms;
};
};
};
in {
devShells.default = pkgs.mkShell {
packages = with pkgs; [
ccproxy.packages.${system}.default
just process-compose
];
shellHook = proxyConfig.shellHook;
};
});
}
mkConfig generates a Nix store ccproxy.yaml, and its shellHook symlinks it into .ccproxy/ and exports CCPROXY_CONFIG_DIR. The .envrc just needs use flake.
Add .ccproxy/ to .gitignore — the directory contains a Nix-generated symlink that is machine-specific and regenerated on nix develop:
# .gitignore
.ccproxy/
| Port | Use |
|---|---|
| 4000 | System-wide ccproxy (Home Manager, default) |
| 4001 | ccproxy project's own devShell |
| 4010+ | Per-project instances |
| 8083 | System inspector UI (default) |
| 8084 | ccproxy dev inspector |
| 8090+ | Per-project inspector UI |
# Foreground
ccproxy start
# Via process-compose (recommended for dev)
just up # process-compose up --detached
just down # process-compose down
# Check health
ccproxy status # Rich panel
ccproxy status --json # Machine-readable
ccproxy status --proxy # Exit 0 if proxy up, 1 if down
ccproxy status --inspect # Exit 0 if inspector up, 2 if down
Use ccproxy status --proxy as the readiness probe so dependent processes wait for the proxy to be healthy:
# process-compose.yml
version: "0.5"
processes:
ccproxy:
command: "ccproxy start"
readiness_probe:
exec:
command: "ccproxy status --proxy"
initial_delay_seconds: 5
period_seconds: 30
timeout_seconds: 10
failure_threshold: 6
availability:
restart: on_failure
backoff_seconds: 2
max_restarts: 5
myapp:
command: "python -m myapp"
depends_on:
ccproxy:
condition: process_healthy
Point any SDK at the per-project port with a sentinel key:
import anthropic
client = anthropic.Anthropic(
api_key="sk-ant-oat-ccproxy-anthropic",
base_url="http://localhost:4010", # per-project port
)
Or via environment variables in shellHook / .envrc:
export ANTHROPIC_BASE_URL="http://localhost:4010"
export ANTHROPIC_API_KEY="sk-ant-oat-ccproxy-anthropic"
All config lives in $CCPROXY_CONFIG_DIR/ccproxy.yaml (default ~/.config/ccproxy/ccproxy.yaml).
ccproxy:
host: 127.0.0.1
port: 4000
providers:
anthropic:
auth:
type: command
command: "jq -r '.claudeAiOauth.accessToken' ~/.claude/.credentials.json"
host: api.anthropic.com
path: /v1/messages
type: anthropic
gemini:
auth:
type: command
command: "jq -r '.access_token' ~/.gemini/oauth_creds.json"
host: cloudcode-pa.googleapis.com
path: "/v1internal:{action}"
type: gemini
hooks:
inbound:
- ccproxy.hooks.inject_auth
- ccproxy.hooks.extract_session_id
outbound:
- ccproxy.hooks.inject_mcp_notifications
- ccproxy.hooks.verbose_mode
- ccproxy.hooks.shape
shaping:
enabled: true
shapes_dir: ~/.config/ccproxy/shapes
inspector:
port: 8083
cert_dir: ~/.config/ccproxy
lightllm:
transforms:
- match_path: /v1/messages
action: redirect
dest_provider: anthropic
dest_host: api.anthropic.com
dest_path: /v1/messages
See reference/routing-and-config.md for transform rules, providers patterns, and hook parameters.
OAuth mode (subscription accounts -- Claude Max, Team, Enterprise):
sk-ant-oat-ccproxy-{provider} as API keyinject_auth hook detects sentinel prefix, looks up real token from providers[name].authshape hook replays a captured {provider}.mflow shape: strips configured headers, injects content_fields from the incoming request, runs shape inner-DAG hooks (UUID regeneration, Anthropic billing-header re-signing, cache breakpoint normalization), stamps the result onto the outbound flowAPI key mode (direct API keys):
x-api-key or Authorization headersk-ant-oat-ccproxy-{provider}
Where {provider} matches a key in providers config. Common values:
sk-ant-oat-ccproxy-anthropic -- uses providers.anthropic.auth tokensk-ant-oat-ccproxy-gemini -- uses providers.gemini.auth tokenhooks:
inbound:
- ccproxy.hooks.inject_auth
- ccproxy.hooks.extract_session_id
outbound:
- ccproxy.hooks.gemini_cli
- ccproxy.hooks.inject_mcp_notifications
- ccproxy.hooks.verbose_mode
- ccproxy.hooks.shape
- ccproxy.hooks.commitbee_compat
inject_auth -- substitutes sentinel key with real token, sets Authorization: Bearer {token} (or the custom auth.header), clears other auth headers, and stamps ccproxy auth metadata for routing/retryextract_session_id -- parses metadata.user_id for MCP notification routinggemini_cli -- wraps Gemini sentinel-key bodies in the v1internal envelope, conditionally masquerades google-genai-sdk/* UAs, rewrites paths to cloudcode-pa.googleapis.cominject_mcp_notifications -- injects buffered MCP terminal events as tool_use/tool_result pairsverbose_mode -- strips redact-thinking-* from anthropic-beta to enable full thinking outputshape -- replays a captured shape ({provider}.mflow) onto the outbound flow, stamping identity headers, billing header, and system prompt prefixcommitbee_compat -- last-mile compatibility shim for the commitbee toolAuthAddon and GeminiAddon are full mitmproxy addons (not pipeline hooks) registered after the outbound stage: AuthAddon handles 401 detection / refresh / replay; GeminiAddon handles capacity fallback + cloudcode-pa envelope unwrap.
ccproxy does not synthesize Claude Code identity headers in code. Anthropic-bound traffic depends on a shape: a real mitmproxy.http.HTTPFlow from the Claude CLI persisted as a .mflow file. ccproxy ships a packaged default shape for Anthropic; a user-captured shape at ~/.config/ccproxy/shapes/anthropic.mflow overrides it. The shape hook replays the shape on every outbound flow, providing user-agent, anthropic-beta, x-stainless-*, the signed x-anthropic-billing-header, and the system prompt prefix.
If the shape in effect is from an outdated Claude CLI release, Anthropic will reject the request with 401/400. Capture (or refresh) a local override with:
ccproxy run --inspect -- claude -p "shape capture"
ccproxy shapes save anthropic
See docs/shaping.md for the canonical reference (capture workflow, shape inner-DAG hooks, billing salt configuration, custom hooks).
# Anthropic SDK (OAuth via sentinel key)
import anthropic
client = anthropic.Anthropic(
api_key="sk-ant-oat-ccproxy-anthropic",
base_url="http://localhost:4000",
)
# OpenAI SDK
from openai import OpenAI
client = OpenAI(
api_key="sk-ant-oat-ccproxy-anthropic",
base_url="http://localhost:4000",
)
import anthropic
client = anthropic.Anthropic(
api_key="sk-ant-oat-ccproxy-anthropic",
base_url="http://localhost:4000",
)
response = client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
)
No extra headers needed -- the shape hook replays the captured Anthropic shape, supplying anthropic-beta, anthropic-version, the signed billing header, and the system prompt prefix automatically.
Streaming:
with client.messages.stream(
model="claude-sonnet-4-5-20250929",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello"}],
) as stream:
for text in stream.text_stream:
print(text, end="")
from openai import OpenAI
client = OpenAI(
api_key="sk-ant-oat-ccproxy-anthropic",
base_url="http://localhost:4000",
)
response = client.chat.completions.create(
model="claude-sonnet-4-5-20250929",
messages=[{"role": "user", "content": "Hello"}],
)
Requires a transform rule to rewrite from OpenAI format to the destination provider format via lightllm.
import asyncio, litellm
async def main():
response = await litellm.acompletion(
model="claude-sonnet-4-5-20250929",
messages=[{"role": "user", "content": "Hello"}],
api_base="http://127.0.0.1:4000",
api_key="sk-ant-oat-ccproxy-anthropic",
)
print(response.choices[0].message.content)
asyncio.run(main())
Note: litellm.anthropic.messages bypasses proxies. Always use litellm.acompletion().
import os
os.environ["ANTHROPIC_BASE_URL"] = "http://localhost:4000"
os.environ["ANTHROPIC_API_KEY"] = "sk-ant-oat-ccproxy-anthropic"
from claude_agent_sdk import query, ClaudeAgentOptions
async for message in query(
prompt="Your prompt here",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob"],
permission_mode="default",
cwd=os.getcwd(),
),
):
# Handle AssistantMessage, ResultMessage, etc.
pass
export ANTHROPIC_BASE_URL="http://localhost:4000"
export ANTHROPIC_API_KEY="sk-ant-oat-ccproxy-anthropic"
# OpenAI compat
export OPENAI_BASE_URL="http://localhost:4000"
export OPENAI_API_BASE="http://localhost:4000"
curl http://localhost:4000/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: sk-ant-oat-ccproxy-anthropic" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-sonnet-4-5-20250929",
"max_tokens": 100,
"messages": [{"role": "user", "content": "Hello"}]
}'
Model routing is configured via lightllm.transforms in ccproxy.yaml when an explicit override is needed. Each transform rule matches by match_host, match_path, and/or match_model, then rewrites to dest_provider/dest_model via the lightllm dispatch. First match wins. Requests without an override normally route through sentinel-key Provider resolution. Unmatched reverse proxy flows get a 501 error; unmatched WireGuard flows pass through unchanged.
See reference/routing-and-config.md for transform configuration patterns.
Authentication failures are the most common issue. Follow this decision tree:
Error message?
│
├─ "This credential is only authorized for use with Claude Code"
│ ▶ See: Missing or stale captured shape (system prompt prefix not stamped)
│
├─ "OAuth is not supported" / "invalid x-api-key"
│ ▶ See: Missing or stale captured shape (anthropic-beta not stamped)
│
├─ 401 Unauthorized / token errors
│ ▶ See: Token issues
│
├─ Connection refused / timeout
│ ▶ See: Connectivity
│
└─ Other / unclear
▶ See: General diagnostics
See reference/troubleshooting.md for the full diagnostic guide with resolution steps for each branch.
ccproxy status # Verify proxy is running
ccproxy status --json # Machine-readable status with URL
ccproxy logs -f # Stream logs in real-time
ccproxy logs -n 50 # Last 50 lines
~/.config/ccproxy/shapes/anthropic.mflow) is stale for the current Claude CLI release, requests fail with 401/400. Refresh via ccproxy shapes save anthropic.devConfig overwrites inspector atomically — top-level // merge on inspector drops sub-keys not re-specified. Deep merge each nested attrset explicitly: defaults.inspector // { ... }.supportedSystems limited — only x86_64-linux and aarch64-linux; aarch64-darwin not supported.npx claudepluginhub starbaser/ccproxyOperates the ccproxy inspector MITM system for intercepting, inspecting, and transforming LLM API traffic. Covers running CLI tools through the reverse proxy or WireGuard namespace capture, inspecting flows with client-vs-forwarded request comparison, and debugging the hook pipeline.
Unifies Python LLM API calls to 100+ providers (OpenAI, Anthropic, Ollama, llamafile) in OpenAI format with retries, fallbacks, exceptions, cost tracking. Triggers on litellm imports/completion().
Configures Claude Code CLI to use MiniMax API endpoint. Sets environment variables and provides a claudem function for running commands via MiniMax.