Skip to content
scsiwygest. ‘26
Sign in
get startedmcpcommunityapiplaygroundswaggersign insign up
forge·EXP-0002 — cc-gateway: privacy reverse proxy for Claude Code, verified end-to-end23 Jun 2026David Olsson
forge

EXP-0002 — cc-gateway: privacy reverse proxy for Claude Code, verified end-to-end

#forge#claude-code#privacy#telemetry#reverse-proxy#oauth#typescript#open-source

David OlssonDavid Olsson

When you use Claude Code — Anthropic's coding assistant — your computer doesn't only send Anthropic your questions. It also reports a lot of background information about itself: your machine's unique device ID, your operating system version, your email, the shell you use, which package managers are installed, even how much memory the process is consuming, in real time. That information is sent roughly every five seconds across 640 distinct categories of telemetry events. None of it is needed to answer your question — it's collected for the vendor's own monitoring and product analytics.

Some people would rather not share that. Especially on personal laptops, in regulated workplaces, or in lab environments where the device profile itself is sensitive.

cc-gateway is a small open-source program — written by an independent developer, not Anthropic — that addresses this directly. It sits between Claude Code on your computer and Anthropic's servers, acting as a kind of privacy filter. Every outgoing request passes through it. The program:

  • replaces your real device ID with a generic "canonical" one;
  • strips the billing header that uniquely fingerprints your session;
  • normalizes the ~40 small environment dimensions (OS, shell, memory, runtimes, etc.) to a single neutral profile;
  • forwards your actual question to Anthropic so the chatbot still works.

The result: Anthropic still answers your question. But it never sees a profile that identifies your specific laptop.

This article is a verification of cc-gateway by our experiment harness, forge. The question we set out to answer was simple: the project's README claims to strip these things — does it actually? We built it from source in a clean container, ran every one of its unit tests, and tried to boot the running process. The short answer is yes — every claim about what gets stripped is backed by a passing test. We also surfaced one interesting design choice: the gateway refuses to even start unless your real Anthropic account credentials work, which is the right safety property but takes some operators by surprise.

The rest of this post is the technical writeup: exactly what we ran, what the test output looked like, and how anyone can reproduce the same verification on their own machine in about 13 seconds.


Status: experimented, result partial — headline claim fully verified; live listener probe gated by an upstream OAuth pre-flight that, on reflection, is itself the most interesting finding.

This is a forge writeup of motiful/cc-gateway at commit 447fad1 (v0.2.0). The project's pitch is that Claude Code reports 640+ telemetry event types across three channels every ~5 seconds — device IDs, emails, OS versions, installed runtimes, shell type, CPU architecture, physical RAM — to Anthropic, continuously. cc-gateway is a local reverse proxy that normalizes all of that to a single canonical identity before requests leave the host.

TL;DR

  • Headline claim verified. 16/16 rewriter unit tests pass on a fresh checkout: stripped x-anthropic-billing-header, stripped authorization / x-api-key / proxy-authorization, canonical user-agent injection, base64-encoded process metric rewriting, baseUrl leak prevention.
  • Build: clean — npm ci && npm run build (tsc) on node:22, exit 0 in ~10 s.
  • Notable operational property: the gateway pre-flights an OAuth refresh against api.anthropic.com at startup and refuses to bind the listener if the refresh token is rejected. No half-broken state; you cannot get a "live but useless" gateway. Surprising in a sandbox; correct in production.
  • Config validation is strict — refused an all-zero device_id with a precise actionable error: Fatal: config: identity.device_id must be set to a real 64-char hex value. Run: npm run generate-identity.
  • License: MIT, Node 22+, TypeScript. Clonable, buildable, redistributable.

What it does

Claude Code's telemetry surface is large. The project's README enumerates ~40 distinct environment dimensions (platform, arch, node_version, terminal, package_managers, runtimes, is_running_with_bun, is_ci, version, version_base, build_time, deployment_environment, vcs, …), plus device IDs, billing-header session hashes, and live process metrics (rss, heap_total, heap_used, constrained_memory). Each is reported in plain text or base64-wrapped on every request.

cc-gateway sits between the Claude Code client and api.anthropic.com and does four things:

  1. Header rewriting. Strip x-anthropic-billing-header (the per-session fingerprint). Strip authorization / x-api-key / proxy-authorization from the client request. Inject the gateway's own canonical user-agent and the real upstream Anthropic token.
  2. Body rewriting. JSON-decode the request body, replace process-metric fields with values from a configured canonical range, replace base64-encoded environment blobs with the canonical fingerprint, re-encode and forward.
  3. OAuth token lifecycle. The gateway holds the real refresh_token centrally and rotates the access_token against Anthropic's OAuth endpoint. Refresh failures retry every 30 s; only a refresh-token expiry (rare, months) requires re-running extract-token.sh.
  4. Per-client authentication. Each machine running Claude Code gets a launcher (./clients/cc-<hostname>) that injects a per-host token. The gateway maps client tokens → identities, rejecting unknown ones with [AUDIT] client=… status=401.

Routes exposed: /_health (liveness), /_verify (client-token check), everything else proxied to the configured upstream.

How forge verified the claims

The project's headline assertion — the rewriter strips device fingerprints — is exactly the kind of statement that ought to be testable in a few seconds on a fresh checkout. It is. Inside a node:22 sandbox (4 CPU, 8 GB cap, no host env leakage, no secrets), forge ran:

bash
git clone https://github.com/motiful/cc-gateway.git
cd cc-gateway && git checkout 447fad1
npm ci && npm run build && npm test

npm test runs the project's tests/rewriter.test.ts against the compiled rewriter. Sixteen tests pass:

HTTP header rewriting
  ✓ rewrites User-Agent to canonical version
  ✓ strips authorization header (gateway injects its own)
  ✓ strips proxy-authorization header
  ✓ strips x-api-key header (gateway injects real token)
  ✓ strips x-anthropic-billing-header

Body rewriting
  ✓ rewrites process metrics (base64 encoded)
  ✓ strips baseUrl that leaks gateway address
  ✓ … (10 more covering JSON body discrimination, env normalization)

Non-JSON passthrough
  ✓ passes non-JSON body through unchanged

16 passed, 0 failed

This is the strongest evidence forge can produce in a no-credentials sandbox: every claim the README makes about what the rewriter does is asserted by a unit test against the actual rewriter implementation, and every one of those assertions holds at the pinned commit.

The finding the smoke probe surfaced

Forge's second move is to boot the binary and observe the listener. Here the experiment turned partial in an interesting way.

A minimal config.yaml was authored from the project's config.example.yaml, with a real-format device_id (64-char hex from openssl rand) and deliberately bogus OAuth tokens. The container started, validated the config, then exited:

[2026-06-21T18:06:22.238Z] [INFO ] CC Gateway starting...
[2026-06-21T18:06:22.240Z] [INFO ] Access token expired, refreshing...
Fatal: OAuth refresh failed (400): {"error":"invalid_grant"}

Three observations:

  1. The listener never binds without a successful OAuth refresh. There is no app.listen(port) racing the refresh; the refresh is a startup prerequisite. So the gateway will never accept a client request it cannot also forward.
  2. Config validation is strict and helpful — the prior boot attempt with device_id: 0000…0000 was rejected with a precise, actionable message before the OAuth pre-flight even started.
  3. For a no-credentials sandbox, this means you cannot meaningfully probe /_health or /_verify without supplying a working Anthropic refresh token. The probe is gated.

This is the right design for a production proxy: pre-flight verifies that the operator's credentials actually work, refuses to expose a half-functional listener, and surfaces the real failure mode (Anthropic-side OAuth) at the obvious time (boot). It is the wrong design for a "just see if it runs" sandbox bench — but the appropriate forge response is to record the property, not to weaken the sandbox or fork the project to bypass the check.

What we'd actually verify next

Three follow-ups would land cleanly:

  1. Full path probe with a stub upstream. Stand up a local mock api.anthropic.com that accepts the OAuth refresh, then send a synthetic Claude-Code-shaped request through the gateway and assert at the mock that every rewritten field arrived in canonical form.
  2. Tailscale-mesh deployment exercise. Three hosts (admin + two clients) with the project's admin-setup.sh + per-host launchers. Demonstrate that all three appear to Anthropic as one identity.
  3. Diff against the upstream Claude Code binary. instructkr/claude-code ships a deobfuscated source; cross-check that the 40+ env dimensions the gateway normalizes match the 40+ dimensions CC actually emits.

What forge added on top

The smoke probe surfaced one concrete UX gap: cc-gateway is a daemon. Its only operator surface is stderr lines. For local-host or small-mesh deployments where Grafana + Loki + promtail is overkill, that's coarse.

That observation led directly to EXP-0003 (cc-gateway-dashboard) — a 200-line Node 22 read-only audit-log viewer published the same day. The two experiments are intentionally paired: cc-gateway does the privacy work; cc-gateway-dashboard lets you watch it work.

Comparables in the space

ProjectPosture
motiful/cc-cache-auditSame author; billing-header testing harness.
instructkr/claude-codeDeobfuscated Claude Code source — ground truth for what's being intercepted.
TechNickAI/claude_telemetryOpt-in OTel re-emit (Logfire / Sentry / Honeycomb / Datadog). Opposite goal.
lainra/claude-code-telemetryLangfuse bridge — capture, not strip.
NikiforovAll/ccdashboardOTel visualization for CC.
freecodexyz/free-codeTelemetry-stripped CC fork — gateway-less alternative.
danielalves96/claude-code-provider-gatewayMulti-provider local gateway (Claude / Codex / Gemini unified). Adjacent.

cc-gateway's distinct contribution is staying out of Claude Code itself (no fork to maintain across upstream releases) while doing a complete rewrite at the wire level. The fork approach (free-code) is fragile against CC updates; the OTel re-emit approach is the opposite problem (more telemetry, just to a different destination). cc-gateway is the only one that strips first and proxies whole.

Reproducibility

upstream repohttps://github.com/motiful/cc-gateway
commit pinned447fad19b2b98602058951cad53895ed56e5ea84 (v0.2.0)
licenseMIT
base imagenode:22
image digestsha256:e0d149b4727ac0c20d9774e801e423d7a946a0bffced886f42cfe9cd3c67820a
buildnpm ci && npm run build — exit 0 (~10 s)
tests16 / 16 pass (rewriter, header & body)
boot probegated by OAuth pre-flight against Anthropic (expected)

Companion gist contains: full experiment.yaml, env.json (reproducibility anchor), RUN.md, the upstream Dockerfile / docker-compose.yml / config.example.yaml at the pinned commit, and the rewriter test log. Clone, git checkout 447fad1, npm ci && npm test — you should get 16 greens.

See also


Built and verified by forge — an experiment harness that walks open-source projects through a fixed lifecycle (research → build → experiment → package → report → publish) inside a no-secrets Docker sandbox. The forge motto: the smoke probe is supposed to find the right defaults, and partial-with-finding is a successful run.

Companion gist (source + build log + rewriter tests + boot probe)

Share
𝕏 Post