Skip to content
scsiwygest. ‘26
Sign in
get startedmcpcommunityapiplaygroundswaggersign insign up
forge·EXP-0003 — cc-gateway-dashboard: a 200-line read-only viewer for Claude Code's privacy gateway23 Jun 2026David Olsson
forge

EXP-0003 — cc-gateway-dashboard: a 200-line read-only viewer for Claude Code's privacy gateway

#forge#claude-code#privacy#telemetry#sse#node#typescript#docker#open-source

David OlssonDavid Olsson

Update (2026-06-24): the dashboard now has its own canonical home at github.com/worksona/cc-gateway-dashboard — clone, fork, file issues there. The companion gist remains the frozen reproducibility anchor for this writeup. Forge's forge-packager skill now codifies the promote-to-repo rule for forge-original installable artifacts; see the follow-up post on repos vs gists.


For the layman

When you use Claude — Anthropic's chatbot — through the Claude Code coding assistant, your computer doesn't just send your prompts. It also reports a lot of background information about itself: your device's unique ID, your operating system, what shell you use, how much memory the process is consuming, what package managers you have installed, and several hundred other small dimensions. All of that gets sent to Anthropic, continuously, every few seconds.

Some people find that uncomfortable, especially on personal machines or in regulated workplaces. cc-gateway is a small open-source program written by an independent developer that sits in between Claude Code on your laptop and Anthropic's servers. It catches every outgoing message and rewrites the identity-revealing bits — replacing your real device ID with a generic "canonical" one, stripping headers that fingerprint your session, and forwarding the rest of the request normally. The end effect: Anthropic still gets your question, but it doesn't get a profile of your laptop.

cc-gateway works well — but it has no screen. The only way to see what it's doing is to read text logs scrolling by in a terminal. That's fine for a server-room operator; it's clunky for everyday use.

This article is about a tiny web page we built — about 200 lines of code total — that watches those logs and shows them in your browser as a live, color-coded feed. Like a quiet little dashboard for the gateway. We built it inside our experiment harness (called forge), tested it, ran it in a Docker container against fake log data, and confirmed every piece works. The technical writeup below covers exactly what we built, how we verified it, and how anyone can rebuild the same thing in about 13 seconds.


Status: experimented, result success. Built, tested, smoke-probed end-to-end in a sandbox. Closes a UI gap surfaced by EXP-0002 (cc-gateway) — that experiment verified the rewriter's headline claim (16/16 unit tests) but is fundamentally headless, expecting operators to tail logs or ship them to Loki. For a local-host or Tailscale-mesh deployment, that's overkill. This experiment is the smallest thing that closes the gap.

Canonical home: github.com/worksona/cc-gateway-dashboard (the dashboard's living source). Frozen anchor: gist.github.com/worksona/620453dd… (this writeup's reproducibility snapshot).

TL;DR

  • Stack: Node 22 + tsc + plain http.createServer + Server-Sent Events. Zero runtime dependencies.
  • Build: 13s on node:22, exit 0; parser tests: 5/5 pass on a fresh checkout.
  • Endpoints: / (HTML view), /_health (JSON), /api/snapshot (last N parsed lines), /api/stream (SSE).
  • Behavior verified: backfill on startup, live tail via fs.watch, SSE fan-out to multiple clients, color-coded status rendering, INFO/WARN/ERROR inline display.
  • License: MIT. Container: ~120 MB on node:22-slim.

Why it exists

cc-gateway is a reverse proxy that strips Claude Code's device fingerprint, billing header, and ~40 environment dimensions before requests leave for api.anthropic.com. It's a strong piece of work — but it's a daemon, and the only operator surface is stderr lines like:

[2026-06-21T18:00:01.120Z] [AUDIT] client=machine-a POST /v1/messages → 200

That's fine for journalctl. Less fine when you want a one-glance view of who's hitting the gateway right now — especially on a small Tailscale mesh where you don't want to stand up Grafana + Loki + promtail just to read a log file.

The dashboard is the minimum viable answer: tail the audit log, parse the two line formats the gateway emits, stream the result to a browser. No DB, no auth (it's expected to live on the same private network as the gateway), no framework, no build step beyond tsc.

What it does

The HTML page is a single inlined string — no bundler, no client-side framework. The server is ~120 lines of TypeScript:

src/
├── parse.ts        # two regexes: one for AUDIT, one for [INFO|WARN|ERROR|DEBUG]
├── server.ts       # http.createServer + ring buffer + SSE
└── index-html.ts   # the one and only page

Five behaviours that the smoke probe verified:

  1. Backfill. On startup the server reads the existing audit log, parses every line through parse.ts, keeps the last RING_SIZE (default 500) in memory, and exposes them via GET /api/snapshot. Without this, the dashboard would show "waiting for events…" on every container restart — the bad default. This is the kind of design choice a smoke probe is supposed to surface, and it did.

  2. Live tail. A fs.watch callback fires a position-tracked createReadStream(path, { start: lastPos, end: size - 1 }) and a readline interface parses only the new bytes. Append a line to the audit log; the dashboard sees it within ~1s.

  3. SSE fan-out. Connected EventSource clients are kept in a Set<ServerResponse>. Every parsed line is JSON.stringify'd and written to all of them. Disconnected clients are reaped via req.on('close').

  4. Robust parsing. The two regexes match the gateway's exact format including trailing (yes, a Unicode arrow). Garbage lines (any line that matches neither) return null and are silently dropped — no crashes on malformed input.

  5. Color-coded display. Dark monospaced table, status colored by class: 2xx green, 4xx amber, 5xx red. INFO/WARN/ERROR lines render as a single dim-italic row spanning the AUDIT columns.

How it was built

This was a forge-internal experiment — not harvested from Slack. The orchestrator generated it as a direct response to "is there no UI?" while walking EXP-0002 (cc-gateway). All work happened inside the forge sandbox: a node:22 Docker image, 4 CPU / 8 GB memory cap, no host environment leakage, no secrets in the data plane.

The whole loop — scaffolding, two iterations of server.ts, the inline HTML view, parser tests, Docker image build, live smoke probe with a synthetic fixture log — took 10 minutes of wall clock and one mid-experiment correction. The initial tail() positioned at EOF. The probe showed /api/snapshot returning [] on a non-empty audit log. Adding a backfill() step bounded by RING_SIZE fixed it. That correction is the actual artifact — the smoke probe found the right default.

Run it yourself

The canonical source is github.com/worksona/cc-gateway-dashboard; clone there if you want to fork, file issues, or pin a commit. The full source tree at the time of the original forge bench (with build log, env manifest, and audit fixture) is preserved in the companion gist. Short version:

bash
git clone https://github.com/worksona/cc-gateway-dashboard.git
cd cc-gateway-dashboard
docker build -t cc-gateway-dashboard:0.1.0 .
docker run --rm -p 8444:8444 \
  -e AUDIT_LOG=/data/audit.log \
  -v /path/to/cc-gateway-audit:/data:ro \
  cc-gateway-dashboard:0.1.0
open http://localhost:8444

Reproducibility

canonical repohttps://github.com/worksona/cc-gateway-dashboard
frozen gisthttps://gist.github.com/worksona/620453dd7aa1f454a98db4f6fc57d9cf
base imagenode:22
image digestsha256:e0d149b4727ac0c20d9774e801e423d7a946a0bffced886f42cfe9cd3c67820a
build commandsnpm install && npm run build && npm test
build exit0 (13 s)
parser tests5 / 5 pass
smoke probes/_health ✓, /api/snapshot 7 lines ✓, SSE 1 s latency ✓

What it's not

  • It's not a metrics dashboard. No request counts, no p99, no histograms. If you want those, point cc-gateway's stderr at NikiforovAll/ccdashboard or your existing OTel pipeline.
  • It's not authenticated. Don't expose it on the public internet. The threat model assumes it sits on the same Tailscale mesh as the gateway.
  • It doesn't tail multiple log files or merge sources. One audit log in, one stream out.

What's next

If this proves useful, two natural follow-ups:

  1. Filters. Click a client column to filter to that client. Trivial to add — the ring buffer is already in memory.
  2. Persist the ring across restarts. Spill to a SQLite WAL on a 30 s timer. Costs about 30 lines.

Both are forge-friendly experiments — they're scoped tight enough to fit the same 200-line ceiling.


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. EXP-0003 was generated as a direct response to the UI gap surfaced by EXP-0002 (cc-gateway), and is published with the reproducibility anchors that let any reader rebuild it in 13 seconds.

Canonical repo (clone + fork) · Companion gist (frozen anchor)

Share
𝕏 Post