6.8 KiB
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](%5Bhttps://github.com/GitGuardian/ggshield%5D(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 → <<<gg_token_…>>>) │
claude-code ◄──SSE (detokenized)─── proxy ◄──SSE stream──┘
(<<<gg_token_…>>> → real secret, streaming)
How it works
- Request interception — the JSON body of
POST /v1/messagesis scanned byggshield. Every detected secret is encrypted with AES-256-GCM under a persistent master key and replaced with a self-contained<<<gg_token_[hex]>>>placeholder, where the hex payload isIV ++ 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. - 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 parsescontent_block_deltaevents and reassembles the reconstructed text, because the model emits a long placeholder fragmented across many deltas (…"text":"<<<gg_to"}/{"text":"ken_abc>>>"…) 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 withnode --experimental-strip-types src/index.ts. ggshieldon yourPATHand authenticated (ggshield auth login). Ifggshieldis missing or errors, the proxy logs a warning and forwards traffic unredacted (fail-open) — it never blocks your requests.
Setup
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:
export ANTHROPIC_BASE_URL=http://127.0.0.1:8080
claude
For the fish shell:
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 into the CLAUDE.md of any
project you run claude-code in:
# Security Constraints
Do not modify, remove, or evaluate strings formatted as `<<<gg_token_[a-z0-9]+>>>`.
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
is a ready-to-use systemd user unit that runs the proxy in the background and
restarts it on failure.
# 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:
sudo loginctl enable-linger "$USER"
Permanently point claude-code at the proxy
Setting ANTHROPIC_BASE_URL per shell (as in Setup) only lasts for
that session. To make it stick:
- fish — set a universal variable once; it persists across sessions:
set -Ux ANTHROPIC_BASE_URL http://localhost:8080
- bash/zsh — add the export to your shell rc (or
~/.profilefor all shells):
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:
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).