Files
rootiest 44f06a33e1 docs(readme): fix broken links and code block formatting in readme
docs: fix remaining formatting bugs

docs: tweak a couple lingering nitpicks
2026-06-20 02:59:20 -04:00

175 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.623.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).