44f06a33e1
docs: fix remaining formatting bugs docs: tweak a couple lingering nitpicks
175 lines
6.8 KiB
Markdown
175 lines
6.8 KiB
Markdown
# 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 → <<<gg_token_…>>>) │
|
||
claude-code ◄──SSE (detokenized)─── proxy ◄──SSE stream──┘
|
||
(real secret ← <<<gg_token_…>>>, 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
|
||
`<<<gg_token_[hex]>>>` 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":"<<<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
|
||
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 `<<<gg_token_[a-f0-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`](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).
|