Skip to content

Latest commit

 

History

History
876 lines (684 loc) · 32.5 KB

File metadata and controls

876 lines (684 loc) · 32.5 KB

ccproxy Usage Guide

ccproxy is a transparent LLM API interceptor built on mitmproxy. It embeds mitmweb in-process, intercepts HTTP traffic from any LLM client, and feeds it through a configurable pipeline that can observe, rewrite, and re-route requests between providers. Two entry points serve different use cases: a reverse proxy for SDK clients and a WireGuard tunnel for full transparent capture of arbitrary processes.


1. Getting Started

Install configuration

ccproxy init              # writes ~/.config/ccproxy/ccproxy.yaml
ccproxy init --force      # overwrite existing config

Edit ~/.config/ccproxy/ccproxy.yaml to configure providers, transform overrides, and hooks. The config directory can be overridden with --config PATH or the CCPROXY_CONFIG_DIR environment variable.

Start the server

ccproxy start

Runs in the foreground. The server binds two listeners:

  • Reverse proxy on the configured port (default 4000) for SDK clients.
  • WireGuard UDP tunnel on an auto-assigned port for namespace-jailed processes.

The mitmweb UI URL (with auth token) is printed at startup. Use process-compose or systemd for background supervision.

Check status

ccproxy status            # rich table: proxy, inspector, config, logs
ccproxy status --json     # machine-readable JSON
ccproxy status --proxy    # health check: exit 0 if proxy is up, 1 if down
ccproxy status --inspect  # health check: exit 0 if inspector is up, 2 if down

Health check flags use a bitmask: --proxy --inspect exits 0 only if both are healthy, 3 if both are down.

View logs

ccproxy logs              # tail the daemon log file ($CCPROXY_CONFIG_DIR/ccproxy.log)
ccproxy logs -f           # follow
ccproxy logs -n 50        # last 50 lines

For journal-routed logging (use_journal: true) read journalctl --user -t <identifier> directly; for a process-compose-supervised dev instance use process-compose process logs ccproxy.


2. Two Entry Points

Every flow enters ccproxy through one of two listeners. The entry point determines how the flow is treated by the pipeline.

Reverse proxy

SDK clients point their base URL at ccproxy:

ccproxy run -- my-tool          # sets ANTHROPIC_BASE_URL, OPENAI_BASE_URL, OPENAI_API_BASE

Or set the environment manually:

export ANTHROPIC_BASE_URL=http://127.0.0.1:4000
export OPENAI_BASE_URL=http://127.0.0.1:4000

The client sends requests to ccproxy as if it were the provider. Transform rules determine where the request actually goes. Unmatched reverse proxy flows receive a 501 error — there is no default upstream since the placeholder backend (localhost:1) is intentionally invalid.

WireGuard namespace jail

For full transparent capture of all outbound traffic from a process:

ccproxy run --inspect -- claude --model haiku -p "hello"
ccproxy run -i -- aider --model claude-3-haiku

This creates a rootless Linux network namespace (no root required on Linux 5.6+ with unprivileged user namespaces enabled), routes all TCP/UDP traffic through a WireGuard tunnel into mitmproxy, and injects a combined CA bundle so TLS interception works transparently. The confined process has no direct internet access — everything exits through the WireGuard tunnel and passes through the full addon pipeline.

Unmatched WireGuard flows pass through to their original destination unchanged, so the subprocess works normally even for traffic that ccproxy has no transform rules for.

Requirements: ccproxy start must be running. The following tools must be in PATH: slirp4netns, unshare, nsenter, ip, wg, iptables, and sysctl. NixOS with kernel 6.18+ satisfies these by default. On Windows, this path is supported only inside WSL2; use the ccproxy.wsl artifact for the supported out-of-box environment.

Key differences

Reverse Proxy WireGuard Namespace
How traffic arrives Client sets base_url to ccproxy All traffic captured transparently
Client modification Requires base_url env var None — process is unaware of ccproxy
Unmatched flows 501 error Pass through unchanged
Shaping observation Not observed (consumer of profiles) Always observed (reference traffic)
Shaping application Applied (when transform matched) Not applied
TLS Client connects via plain HTTP mitmproxy intercepts and re-signs with its CA

3. The Pipeline

Every request passes through a fixed addon chain:

┌─────────────────────────┐
│       ReadySignal       │  Startup synchronization
└────────────┬────────────┘
┌────────────▼────────────┐
│     InspectorAddon      │  Flow capture, OTel spans, client request snapshot, SSE streaming
└────────────┬────────────┘
┌────────────▼────────────┐
│ FingerprintCaptureAddon │  TLS ClientHello capture (JA3/JA4 material for shapes)
└────────────┬────────────┘
┌────────────▼────────────┐
│      MultiHARSaver      │  ccproxy.dump command (multi-page HAR export)
└────────────┬────────────┘
┌────────────▼────────────┐
│      ShapeCapturer      │  ccproxy.shape command (validate + persist .mflow)
└────────────┬────────────┘
┌────────────▼────────────┐
│      Inbound Hooks      │  Auth token injection, session ID extraction, Perplexity ingest
└────────────┬────────────┘
┌────────────▼────────────┐
│        Transform        │  Route matching, provider dispatch (passthrough / redirect / transform)
└────────────┬────────────┘
┌────────────▼────────────┐
│     Outbound Hooks      │  Gemini envelope wrap, Perplexity headers, MCP notification injection, verbose mode, shape replay, commitbee compat
└────────────┬────────────┘
┌────────────▼────────────┐
│ TransportOverrideAddon  │  Reroute fingerprint-profile providers through the curl-cffi sidecar
└────────────┬────────────┘
┌────────────▼────────────┐
│        AuthAddon        │  401-detect → refresh → replay (for auth-injected flows)
└────────────┬────────────┘
┌────────────▼────────────┐
│       GeminiAddon       │  Gemini capacity fallback + cloudcode-pa envelope unwrap
└────────────┬────────────┘
┌────────────▼────────────┐
│     PerplexityAddon     │  Perplexity SSE patching + thread bookkeeping
└────────────┬────────────┘
┌────────────▼────────────┐
│  EgressSanitizerAddon   │  Strip ccproxy-internal headers before egress
└────────────┬────────────┘
             ▼
        Provider API

InspectorAddon

The first real addon in the chain. Before any hook touches the request, it captures a complete snapshot of the original client request (method, URL, headers, body). This snapshot is the ground truth of what the client sent and is used for:

  • Shaping observation — learning what a reference client sends.
  • Client Request content view — visible in the mitmweb UI under the "Client-Request" tab.
  • ccproxy flows compare — diffing what the client sent vs what the pipeline forwarded.
  • HAR export — each flow's HAR page includes both the forwarded and client request.

InspectorAddon also manages OTel span lifecycle and enables SSE streaming on responses with content-type: text/event-stream.

Inbound hooks

Run before the transform stage. Default hooks:

  • inject_auth — Detects sentinel API keys (see OAuth) and substitutes real tokens from configured credential sources.
  • extract_session_id — Parses metadata.user_id from the request body and stores the session ID for downstream hooks (MCP notification injection).
  • extract_pplx_files — For Perplexity Pro traffic, uploads image_url attachments through Perplexity's S3 batch chain and rewrites the body to reference the uploaded files.
  • pplx_thread_inject — Resolves Perplexity Pro thread continuation (explicit body session_id, organic L1 cache hit, or pass-through).

Transform

Matches the request against inspector.transforms rules (first match wins) and dispatches in one of three modes. See Transform Rules.

Outbound hooks

Run after the transform stage. Default hooks:

  • gemini_cli — For Gemini sentinel-key traffic, wraps the body in the v1internal envelope, conditionally masquerades google-genai-sdk/* UAs as the Gemini CLI, and rewrites the path to cloudcode-pa.googleapis.com.
  • pplx_stamp_headers — For Perplexity Pro traffic, swaps Bearer auth for the browser-shape Cookie + UA + Origin + sec-fetch-* header bundle.
  • pplx_preflight — Best-effort GET /search/new?q=... warm-up before a Perplexity Pro ask.
  • inject_mcp_notifications — Drains buffered MCP terminal events for the current session and injects them as synthetic tool_use/tool_result message pairs before the final user message.
  • verbose_mode — Strips redact-thinking-* from the anthropic-beta header to enable full thinking block output from Anthropic models.
  • shape — Replays a captured shape ({provider}.mflow) onto reverse proxy and OAuth-injected flows: strips configured headers, injects content_fields from the incoming request, runs shape inner-DAG hooks (UUID regeneration, billing-header re-signing, cache breakpoint normalization), stamps the result onto the outbound flow. Only fires on flows that matched a transform/redirect rule.
  • commitbee_compat — Last-mile compatibility shim for the commitbee tool — appends a raw-JSON instruction to its system prompt.

AuthAddon and GeminiAddon run after this stage as full mitmproxy addons (not pipeline hooks): AuthAddon handles 401 detection / refresh / replay, and GeminiAddon handles Gemini capacity fallback (sticky retry on 429/503 plus walking gemini_capacity.fallback_models) and cloudcode-pa envelope unwrapping for streaming and buffered responses.

Hook execution

Hooks declare data dependencies (reads and writes) and are sorted into a DAG via topological sort. Hooks that don't depend on each other can run in parallel. Errors in one hook don't block others — the sole exception is AuthConfigError, which is fatal and propagates through the pipeline.

Hooks can be configured per-request via the x-ccproxy-hooks header:

x-ccproxy-hooks: +extra_hook,-verbose_mode

+ force-runs a hook, - force-skips it.


4. Transform Rules

Transform rules — TransformOverride entries under inspector.transforms — are an optional override layer on top of sentinel-driven Provider routing. The default list is empty; most routing comes from providers via inject_auth's sentinel detection. Override rules cover edge cases: forcing a specific destination for a path/model/host combination, bypassing auth for a specific host, etc. Rules are evaluated in order; first match wins.

Matching

All match fields are optional regexes and combined with AND logic:

  • match_host — regex matched against the request's host, Host header, and X-Forwarded-Host.
  • match_path — regex matched against the URL path (default .* matches everything).
  • match_model — regex matched against glom(body, "model") from the JSON request body.

Three actions

passthrough — Forward to the original destination unchanged. The request is observed (logged, traced) but not modified. Useful for WireGuard reference traffic that should flow through transparently.

inspector:
  transforms:
    - action: passthrough
      match_host: cloudcode-pa\.googleapis\.com$

redirect — Rewrite the destination host/port/scheme/path and inject auth credentials, but preserve the request body format. For same-format routing where the body is already correct. Auth resolves via dest_providerproviders[name].

inspector:
  transforms:
    - action: redirect
      match_path: ^/v1internal
      dest_provider: gemini

transform — Full cross-provider rewrite via lightllm. Changes the destination URL and rewrites the entire request body from one API format to another (e.g. OpenAI format to Anthropic format). The response is also transformed back to the client's expected format.

inspector:
  transforms:
    - action: transform
      match_path: ^/v1/chat/completions
      match_model: ^gpt-4o
      dest_provider: anthropic
      dest_model: claude-haiku-4-5-20251001

Transform rule fields

Field Actions Purpose
action all passthrough, redirect, or transform (default: redirect)
match_host all Hostname regex (optional)
match_path all Path regex (default: .*)
match_model all Model regex (optional)
dest_provider redirect, transform Provider name in providers — resolves host/path/auth/format
dest_model transform Destination model name
dest_host redirect Raw host override (bypasses Provider lookup)
dest_path redirect Raw path override
dest_vertex_project transform GCP project ID (Vertex AI)
dest_vertex_location transform GCP region (Vertex AI)

Response handling

  • Non-streaming responses with a matched transform rule are converted back to OpenAI format before being sent to the client.
  • SSE streaming responses use an SSETransformer that parses SSE events from the upstream provider and re-serializes them as OpenAI-format SSE chunks in real time.
  • Passthrough and redirect responses are forwarded unchanged.

5. OAuth and Sentinel Keys

ccproxy uses sentinel API keys to trigger automatic token substitution. A sentinel key is a special value that signals ccproxy to look up the real credential from a configured source.

Sentinel format

sk-ant-oat-ccproxy-{provider}

For example, sk-ant-oat-ccproxy-anthropic tells the inject_auth hook to resolve the real token from providers.anthropic.auth.

Configuring providers

providers:
  anthropic:
    auth:
      type: command
      command: "cat ~/.anthropic/oauth_token"
    host: api.anthropic.com
    path: /v1/messages
    type: anthropic

  gemini:
    auth:
      type: file
      file: "~/.config/gemini/oauth_token"
    host: cloudcode-pa.googleapis.com
    path: "/v1internal:{action}"
    type: gemini

  openai:
    auth:
      type: command
      command: "op read 'op://vault/openai/api_key'"
      header: "authorization"
    host: api.openai.com
    path: /v1/chat/completions
    type: openai

Each auth block is a discriminated AnyAuthSourcecommand, file, anthropic_oauth, or google_oauth. A bare YAML string under auth: auto-coerces to a command source. Optional auth.header overrides the target header name (default: authorization with Bearer prefix; set to x-api-key for raw injection).

401 retry

When a response returns 401 and the request used an injected token (metadata_from_flow(flow).auth_injected), AuthAddon.response() calls config.resolve_auth_token(provider) to re-resolve the credential source. For OAuth-source providers (anthropic_oauth, google_oauth) this triggers another in-process refresh attempt; for static command / file loaders it just re-reads the source. The request is then replayed with whatever token the resolver returns; if the resolver yields nothing (empty token, refresh failed), the 401 propagates to the client.


6. Shape Replay

Some providers (Anthropic in particular) enforce client identity via headers, beta flags, system prompt prefixes, and signed billing headers. When ccproxy receives an SDK call lacking those markers, the request is structurally valid but will be rejected with 401/400.

A shape is a captured mitmproxy.http.HTTPFlow (a real, known-good request from the target SDK) persisted as a {provider}.mflow file. At runtime, the shape outbound hook replays the shape: configured headers are stripped, content_fields from the incoming request are injected per the provider's merge_strategies, shape inner-DAG hooks run (regenerating UUIDs, signing the Anthropic billing header, normalizing cache_control breakpoints), and the final shape is stamped onto the outbound flow.

Capturing a shape

Capture or refresh a shape any time the target CLI version changes:

ccproxy run --inspect -- claude -p "shape capture"
ccproxy shapes save anthropic

Where to learn more

docs/shaping.md is the full reference: capture workflow, storage layout, the inject/strip/shape-hooks pipeline, the cache breakpoint hooks, the Anthropic billing salt configuration, custom shape hooks.


7. Inspecting Flows

mitmweb UI

The inspector UI is available at the URL printed at startup (includes an auth token). It provides the standard mitmproxy flow list with two additions:

  • Client-Request content view — a tab showing the pre-pipeline request snapshot (what the client originally sent, before any hooks or transforms modified it).
  • ccproxy.clientrequest command — returns the client request snapshot as structured JSON.

ccproxy flows CLI

All subcommands accept repeatable --jq FILTER flags. Each filter is a jq expression that consumes and produces a JSON array. Filters chain with |. Default filters from flows.default_jq_filters config are applied first.

# List recent flows
ccproxy flows list
ccproxy flows list --json

# Filter to Anthropic traffic
ccproxy flows list --jq 'map(select(.request.host | endswith("anthropic.com")))'

# Export HAR (opens in Chrome DevTools, Charles, Fiddler)
ccproxy flows dump > all.har

# Diff consecutive request bodies (sliding window)
ccproxy flows diff

# Compare client request vs forwarded request per flow
ccproxy flows compare

# Clear flows
ccproxy flows clear          # clear filtered set
ccproxy flows clear --all    # clear everything

HAR export

ccproxy flows dump produces a multi-page HAR 1.2 file. Each flow becomes one page with two entries:

  • Entry 0 (even index): the forwarded request and response — what was actually sent to the provider.
  • Entry 1 (odd index): the client request (reconstructed from the pre-pipeline snapshot) paired with the same response.

This lets you compare what the client sent vs what the pipeline forwarded in any HAR viewer.

Default flow filters

Configure persistent filters in ccproxy.yaml:

flows:
  default_jq_filters:
    - 'map(select(.request.host | endswith("anthropic.com")))'

8. MCP Server & Notification Buffer

The daemon hosts a FastMCP streamable-HTTP server (flow inspection, shape capture, conversation grouping, model catalog, Perplexity tools). MCP clients connect to http://127.0.0.1:4030/mcp (the mcp.http bind) or to /mcp on the proxy port itself — the proxy listener forwards it to the same in-process server. Bearer auth is configured at mcp.http.auth.

The proxy listener also exposes POST /mcp/notify, which accepts MCP terminal events fire-and-forget (no auth, always answers 200):

curl -X POST http://127.0.0.1:4000/mcp/notify \
  -H 'Content-Type: application/json' \
  -d '{"task_id": "...", "session_id": "...", "event": {...}}'

Events are buffered per task (default max 65536 events, FIFO drop on overflow, 600s TTL — see mcp.buffer in docs/configuration.md). The inject_mcp_notifications outbound hook drains the buffer for the current session and injects events as synthetic tool_use/tool_result pairs before the final user message in the conversation. This allows external MCP servers to surface information into the LLM's context window.


9. Wireshark Decryption

ccproxy exports keylogs for full packet capture decryption.

Keylog files

At startup, ccproxy writes:

  • {config_dir}/tls.keylog — TLS master secrets for all intercepted connections (inner TLS to provider APIs).
  • {config_dir}/wg.keylog — WireGuard static private keys for the outer UDP tunnel.

Capture and decrypt

# Capture traffic
sudo tcpdump -i any -w capture.pcap

# Open in Wireshark, then:
# 1. Decrypt WireGuard: Edit -> Preferences -> Protocols -> WireGuard -> Key log file -> wg.keylog
# 2. Decrypt TLS: Edit -> Preferences -> Protocols -> TLS -> (Pre)-Master-Secret log -> tls.keylog

With both keylogs loaded, the entire traffic path is visible: outer WireGuard UDP packets, inner TLS handshakes, and plaintext HTTP request/response bodies.


10. OpenTelemetry

ccproxy emits OTel spans for every intercepted flow. Three modes with graceful degradation:

Mode Condition Behavior
Real OTLP export otel.enabled: true + packages installed Spans exported via gRPC
No-op tracer enabled: false + API packages present Zero overhead
Stub OTel packages absent No imports, zero overhead

Configuration

otel:
  enabled: true
  endpoint: "http://localhost:4317"
  service_name: "ccproxy"

Span attributes

Each span includes HTTP semantics (http.request.method, url.full, server.address), ccproxy-specific attributes (ccproxy.proxy_direction, ctx.metadata.session_id), and GenAI semantic conventions (gen_ai.system, gen_ai.operation.name) for flows to known provider hosts.

The Jaeger container in compose.yaml accepts OTLP gRPC on port 4317 and serves the trace UI on port 16686.


11. WireGuard Namespace Internals

The namespace jail creates a fully isolated network environment routed through mitmproxy. No root privileges are required.

Network topology

  ┌─ Confined process ─────────────────────────────────┐
  │                                                     │
  │  wg0: 10.0.0.1/32          default route → wg0     │
  │  tap0: 10.0.2.100/24       gateway → 10.0.2.2      │
  │                             DNS → 10.0.2.3          │
  │                                                     │
  └──────────────────┬──────────────────────────────────┘
                     │ WireGuard UDP
                     │ Endpoint: 10.0.2.2:{wg_port}
                     ▼
  ┌─ slirp4netns NAT ──────────────────────────────────┐
  │  10.0.2.2 (gateway) ──────▶ host network           │
  └──────────────────┬──────────────────────────────────┘
                     │
                     ▼
  ┌─ mitmweb WireGuard listener ───────────────────────┐
  │  Decrypts tunnel → feeds into addon chain          │
  └────────────────────────────────────────────────────┘
Address Role
10.0.0.1/32 WireGuard client interface (wg0)
10.0.2.100/24 Namespace TAP interface (tap0)
10.0.2.2 Host gateway (slirp4netns NAT) — WireGuard endpoint
10.0.2.3 DNS forwarder (libslirp built-in)

Port forwarding

A background thread polls the namespace's /proc/{pid}/net/tcp every 0.5 seconds and dynamically forwards new listening ports via the slirp4netns API. This allows tools that start local servers (e.g. OAuth callback listeners) to receive connections from the host.

Localhost routing

Inside the namespace, 127.0.0.1 is isolated loopback — host services are not reachable there. iptables DNAT rules transparently redirect namespace localhost traffic to the slirp4netns gateway (10.0.2.2), so tools with hardcoded 127.0.0.1 base URLs work without modification. When the running ccproxy port differs from the default (4000), a port remap rule handles the translation.

TLS trust

ccproxy run --inspect builds a combined CA bundle (mitmproxy's CA + system CAs) and injects it into the subprocess environment via:

SSL_CERT_FILE          = <combined bundle>
REQUESTS_CA_BUNDLE     = <combined bundle>
CURL_CA_BUNDLE         = <combined bundle>
NODE_EXTRA_CA_CERTS    = <combined bundle>

This covers Python (ssl, urllib3, httpx, requests), curl, and Node.js clients.

Prerequisites

Requirement Check
Unprivileged user namespaces /proc/sys/kernel/unprivileged_userns_clone == 1
slirp4netns In PATH
unshare In PATH
nsenter In PATH
ip In PATH
wg In PATH
iptables In PATH
sysctl In PATH

12. Configuration Reference

Config file: $CCPROXY_CONFIG_DIR/ccproxy.yaml (default: ~/.config/ccproxy/ccproxy.yaml). Individual fields can be overridden via CCPROXY_ prefixed environment variables.

Top-level

Field Default Description
host 127.0.0.1 Bind address
port 4000 Reverse proxy listener port
log_level INFO Root logger level (CCPROXY_LOG_LEVEL env var overrides)
log_file ccproxy.log Daemon log file (relative to config dir; null disables)
provider_timeout null Timeout (seconds) for ccproxy's own outbound httpx calls (OAuth refresh, 401 retry). null = no enforced timeout.
use_journal false Route daemon logs to systemd journal
journal_identifier derived from config-dir basename SYSLOG_IDENTIFIER for the journal handler

The startup readiness probe is configured at inspector.readiness.url (default https://1.1.1.1/) and inspector.readiness.timeout_seconds (default 5.0). Set inspector.readiness.url to null to skip the probe.

inspector

Field Default Description
port 8083 mitmweb UI port
cert_dir null mitmproxy CA certificate store (default: ~/.mitmproxy)
provider_map (see below) Hostname to OTel gen_ai.system mapping
transforms [] Transform rules (see Transform Rules)
mitmproxy (object) mitmproxy option overrides

Default provider_map:

provider_map:
  api.anthropic.com: anthropic
  api.openai.com: openai
  generativelanguage.googleapis.com: google
  openrouter.ai: openrouter

inspector.mitmproxy

Field Default Description
ssl_insecure true Skip upstream TLS verification
stream_large_bodies null Stream threshold (null disables; otherwise 512k, 1m, 10m)
body_size_limit null Hard body size limit (null = unlimited)
web_host 127.0.0.1 mitmweb UI bind address
web_password null UI password (string, or {command:} / {file:} source). null generates a random token on each startup.
web_open_browser false Auto-open browser on start
ignore_hosts [] Regex patterns for hosts to bypass
allow_hosts [] Regex patterns for hosts to intercept (exclusive)
termlog_verbosity warn mitmproxy terminal log level
flow_detail 0 Flow output verbosity (0-4)

The CA certificate store directory is set at inspector.cert_dir (a sibling of inspector.mitmproxy), not inside this block.

providers

providers:
  anthropic:
    auth:
      type: command
      command: "cat ~/.anthropic/oauth_token"
    host: api.anthropic.com
    path: /v1/messages
    type: anthropic

  deepseek:
    auth:
      type: command
      command: "printenv DEEPSEEK_API_KEY"
      header: x-api-key
    host: api.deepseek.com
    path: /anthropic/v1/messages
    type: anthropic

Per-entry fields:

  • auth — discriminated AnyAuthSource (command / file / anthropic_oauth / google_oauth). A bare string auto-coerces to a command source. Optional auth.header overrides the target auth header name.
  • host — single destination hostname.
  • path — destination path. Supports {model} and {action} templating.
  • type — adapter-family name (anthropic, openai, google, gemini, vertex_ai, vertex_ai_beta, perplexity_pro) driving wire-format dispatch. Anthropic-compatible forks like DeepSeek and Z.AI use type: anthropic.
  • fingerprint_profile — optional curl-cffi browser impersonation profile (e.g. chrome131); routes the provider through the TLS fingerprint sidecar.

hooks

hooks:
  inbound:
    - ccproxy.hooks.inject_auth
    - ccproxy.hooks.extract_session_id
    - ccproxy.hooks.extract_pplx_files
    - ccproxy.hooks.pplx_thread_inject
  outbound:
    - ccproxy.hooks.gemini_cli
    - ccproxy.hooks.pplx_stamp_headers
    - ccproxy.hooks.pplx_preflight
    - ccproxy.hooks.inject_mcp_notifications
    - ccproxy.hooks.verbose_mode
    - ccproxy.hooks.commitbee_compat
    - ccproxy.hooks.shape

Entries can also be {hook, params} dicts. The same form works for the shape inner-DAG hooks under shaping.providers.{name}.shape_hooks:

shaping:
  providers:
    anthropic:
      shape_hooks:
        - ccproxy.shaping.regenerate
        - hook: ccproxy.shaping.caching.strip
          params:
            paths: ["system.*.cache_control"]

otel

Field Default Description
enabled false Enable OTLP span export
endpoint http://localhost:4317 OTLP gRPC endpoint
service_name ccproxy OTel resource service name

shaping

Field Default Description
enabled true Master switch for shape storage and application
shapes_dir ~/.config/ccproxy/shapes Directory holding per-provider {provider}.mflow shape files and patch series
providers {} Per-provider shaping profiles (content_fields, merge_strategies, shape_hooks, preserve_headers, strip_headers, capture.path_pattern, optional billing for Anthropic) — see docs/shaping.md

flows

Field Default Description
default_jq_filters [] jq filters pre-applied to all ccproxy flows commands

13. CLI Reference

ccproxy start                                  Start inspector server (foreground)
ccproxy init [--force]                         Initialize config files
ccproxy run [--inspect] -- <command> [args...]  Run command with proxy environment
ccproxy status [--json] [--proxy] [--inspect]  Show status / health check
ccproxy namespace status [--json]              Show namespace runtime inputs
ccproxy namespace doctor [--json]              Probe namespace DNS/egress/localhost
ccproxy logs [-f] [-n N]                       View logs
ccproxy flows list [--json] [--jq FILTER]...   List flows
ccproxy flows dump [--jq FILTER]...            Export multi-page HAR
ccproxy flows diff [--jq FILTER]...            Sliding-window diff across flows
ccproxy flows compare [--jq FILTER]...         Per-flow client-vs-forwarded diff
ccproxy flows clear [--all] [--jq FILTER]...   Clear flows

Global options (before any subcommand):

  • --config PATH — override config directory
  • -v / --verbose — show INFO/DEBUG output on CLI commands

14. Smoke Test

The quickest end-to-end verification:

ccproxy start &                    # or via process-compose / systemd
ccproxy run --inspect -- claude --model haiku -p "what's 2+2"

This exercises: namespace creation, WireGuard tunnel, TLS interception, the full hook pipeline, transform dispatch, upstream provider call, and SSE streaming back to the client.