# claude-tokenization-proxy A local Data Loss Prevention (DLP) proxy that sits between `claude-code` and the Anthropic API. It scans outgoing requests for secrets with [GitGuardian's `ggshield`](https://github.com/GitGuardian/ggshield), swaps each secret for an opaque placeholder token before it leaves your machine, then restores the real secret in the streamed response on the fly — so Claude never sees your secrets but tooling that echoes a placeholder still gets the real value back. ``` claude-code ──POST /v1/messages──► proxy ──redacted──► api.anthropic.com (ggshield scan, secret → <<>>) │ claude-code ◄──SSE (detokenized)─── proxy ◄──SSE stream──┘ (real secret ← <<>>, streaming) ``` ## How it works 1. **Request interception** — the JSON body of `POST /v1/messages` is scanned by `ggshield`. Every detected secret is encrypted with **AES-256-GCM** under a persistent master key and replaced with a self-contained `<<>>` placeholder, where the hex payload is `IV ++ authTag ++ ciphertext`. No server-side mapping is kept — the token carries everything needed to restore it (`src/tokenize.ts`, `src/store.ts`). The redacted body is forwarded upstream. 2. **Streaming detokenization** — the response is piped through a stateful `Transform` (`src/detokenize.ts`) that finds placeholders, decrypts the hex payload, and substitutes the original secret back. For SSE streams it parses `content_block_delta` events and reassembles the reconstructed text, because the model emits a long placeholder fragmented across many deltas (`…"text":"<<>>"…`) with event framing wedged between the fragments — a raw byte scan can't bridge that. Plain JSON responses, where the token is contiguous, take a simpler direct byte scan. The proxy is **stateless**: tokens are cryptographically self-contained, so they resolve correctly even after a restart or when replaying older chat histories. The only persistent state is the 256-bit master key, generated on first run and stored `0600` at `~/.config/claude-tokenization-proxy/master.key` (override with `GG_MASTER_KEY_FILE`). Anyone with that key file can decrypt captured tokens — treat it like any other secret, and deleting it makes existing tokens unrecoverable. ## Requirements * **Node.js ≥ 23.6** (the proxy runs TypeScript directly via Node's built-in type stripping — no build step, no `ts-node`/`tsx`). On Node 22.6–23.5 run with `node --experimental-strip-types src/index.ts`. * **`ggshield`** on your `PATH` and authenticated (`ggshield auth login`). If `ggshield` is missing or errors, the proxy logs a warning and forwards traffic **unredacted** (fail-open) — it never blocks your requests. ## Setup ```bash npm install # dev-only deps (TypeScript types); not needed to run npm start # starts the proxy on http://127.0.0.1:8080 ``` Point `claude-code` at the proxy: ```bash export ANTHROPIC_BASE_URL=http://127.0.0.1:8080 claude ``` For the [fish shell](https://fishshell.com/): ```fish set -gx ANTHROPIC_BASE_URL http://localhost:8080 claude ``` That's the only change `claude-code` needs — it keeps using your normal `ANTHROPIC_API_KEY` / `x-api-key`, which the proxy forwards untouched. ## Configuration | Env var | Default | Purpose | | ------------------------- | ------------------- | ------------------------------- | | `PORT` | `8080` | Port the proxy listens on | | `ANTHROPIC_UPSTREAM_HOST` | `api.anthropic.com` | Upstream API host to forward to | ## Tell Claude to leave the tokens alone So the model preserves placeholders verbatim (e.g. when writing them to files), copy [`templates/CLAUDE.md`](templates/CLAUDE.md) into the `CLAUDE.md` of any project you run `claude-code` in: ```markdown # Security Constraints Do not modify, remove, or evaluate strings formatted as `<<>>`. Treat them as opaque identifiers and preserve them exactly as they appear when writing or reading files. ``` ## Run at boot/login (systemd user service) [`templates/claude-tokenization-proxy.service`](templates/claude-tokenization-proxy.service) is a ready-to-use systemd **user** unit that runs the proxy in the background and restarts it on failure. ```bash # 1. Install the unit mkdir -p ~/.config/systemd/user cp templates/claude-tokenization-proxy.service ~/.config/systemd/user/ # 2. Edit WorkingDirectory= to point at your clone (and the node path if you # use a version manager like nvm/fnm/asdf — see the comments in the unit) $EDITOR ~/.config/systemd/user/claude-tokenization-proxy.service # 3. Enable + start it systemctl --user daemon-reload systemctl --user enable --now claude-tokenization-proxy.service # Check status / logs systemctl --user status claude-tokenization-proxy.service journalctl --user -u claude-tokenization-proxy.service -f ``` A user service starts when **you** log in. To keep it running at boot before any login (and after you log out), enable lingering for your account: ```bash sudo loginctl enable-linger "$USER" ``` ## Permanently point claude-code at the proxy Setting `ANTHROPIC_BASE_URL` per shell (as in [Setup](%23setup)) only lasts for that session. To make it stick: * **fish** — set a universal variable once; it persists across sessions: ```fish set -Ux ANTHROPIC_BASE_URL http://localhost:8080 ``` * **bash/zsh** — add the export to your shell rc (or `~/.profile` for all shells): ```bash echo 'export ANTHROPIC_BASE_URL=http://localhost:8080' >> ~/.bashrc ``` * **All processes in a graphical session** (works regardless of shell) — drop it in systemd's user environment: ```bash mkdir -p ~/.config/environment.d echo 'ANTHROPIC_BASE_URL=http://localhost:8080' >> ~/.config/environment.d/claude.conf ``` Takes effect on next login. ## Scripts | Command | What it does | | ------------------- | --------------------------------------------- | | `npm start` | Run the proxy | | `npm run dev` | Run with `--watch` (restarts on file changes) | | `npm test` | Run the detokenizer state-machine self-check | | `npm run typecheck` | Type-check the source with `tsc --noEmit` | ## Limitations * Detection is only as good as `ggshield`. It catches known secret patterns and generic high-entropy strings, not arbitrary sensitive prose. * Tokens are decryptable by anyone holding the master key file. Multiple proxy instances must share the same `master.key` to resolve each other's tokens. ## License Copyright (C) 2026 Rootiest. Licensed under the [GNU Affero General Public License v3.0 or later](LICENSE).