Help us shape the future of Go! Take the 2026 Go Developer Survey

redactproxy

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: GPL-3.0

README

RedactProxy

A local, two-way redaction proxy for Claude Code, or anything else that speaks the Anthropic Messages API. It sits between the client and the real API: on the way out it replaces real client data (domains, IPs, emails, credentials, and more) with stable fake values, and on the way back it puts the real ones in again. Claude only ever sees the fakes. Your tool calls still run against real infrastructure.

Built for pentest and consulting teams who want to use Claude Code on a live engagement without that client's data reaching the model provider.

Why

Point an AI coding agent at a live engagement and everything ends up in the model: client domains, internal hostnames, credentials out of a config dump, employee emails, the client's own name. Almost none of it has any reason to leave your machine. The model doesn't need the real hostname to reason about a finding. It needs one that stays the same every time it sees it.

That is all redactproxy does. It rewrites the API traffic and nothing else: no added prompts, no tool restrictions, no change to how Claude Code behaves. It listens on loopback and refuses to start on any other address.

How it works

%%{init: {'theme':'base','themeVariables':{
  'actorBkg':'#2f6fb2','actorBorder':'#2f6fb2','actorTextColor':'#ffffff',
  'actorLineColor':'#6e7681','signalColor':'#6e7681','signalTextColor':'#6e7681',
  'noteBkgColor':'#48525c','noteTextColor':'#ffffff','noteBorderColor':'#6e7681',
  'lineColor':'#6e7681'}}}%%
sequenceDiagram
    participant CC as Claude Code
    participant RP as redactproxy
    participant API as api.anthropic.com

    CC->>RP: request (real PII)
    Note over RP: find text fields, tokenize:<br/>real value -> stable fake
    RP->>API: request (tokens only)
    API-->>RP: response (tokens)
    Note over RP: detokenize:<br/>fake -> real
    RP-->>CC: response (real PII)

A given real value always maps to the same fake one for the life of an engagement (one client project), so Claude can still work out that two hosts belong to the same organization without ever seeing which organization. Each engagement keeps its own storage and shares nothing with the others.

"Token" here means a redaction placeholder, not the unit an LLM's context window is measured in.

Before you point this at real client data: --debug-level anything above off writes real client values to debug.log in plaintext. Leave it off unless you are actively debugging, and never share that file. See Debug logging.

Documentation

The full manual is in docs/, and is published at https://cspf-founder.github.io/redactproxy/: install and first engagement, the rules and tokens commands, Claude Code configuration, the threat model and its known gaps, and reference pages for the CLI, every detector category, the placeholder shapes and rules.json.

Start with Your first engagement. Read Known gaps before you point this at real client data.

Quick start

go install github.com/CSPF-Founder/redactproxy/cmd/redactproxy@latest

Or from a clone, if you plan to change anything:

git clone https://github.com/CSPF-Founder/redactproxy.git
cd redactproxy
make install        # builds into (go env GOPATH)/bin

Check your shell can find it:

redactproxy -h

If that comes back "command not found", $GOPATH/bin isn't on your PATH. Run make build instead and use ./bin/redactproxy everywhere redactproxy appears below.

Now set up an engagement:

cd /path/to/your/engagement-folder
redactproxy wizard --engagement eng-2026-014

The wizard asks which API provider to use, either real Claude or another provider speaking the same Messages API (z.ai, for example). Then it collects any client domains and names you always want redacted, and hardens this folder's .claude/settings.local.json against a few client-side channels that bypass the proxy entirely (see What this doesn't protect).

Then, in the same folder:

redactproxy      # terminal 1
claude           # terminal 2; the wizard already pointed it at the proxy

Or without the wizard:

redactproxy --engagement eng-2026-014 &
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787
claude

ANTHROPIC_BASE_URL is what Claude Code reads to decide where to send its traffic. It only applies to the shell you set it in, so close that terminal when you're done with the engagement (or unset it) to avoid an unrelated session later going through the same proxy by accident.

redactproxy memory prints a short note worth pasting into the engagement's CLAUDE.md. It explains what the placeholder shapes mean, so Claude treats them as values to reuse verbatim instead of typos to correct.

Skipping the wizard is fine. You just lose those two conveniences, the CLAUDE.md note and the settings hardening, and have to do them yourself.

Storage locations

Each engagement is a self-contained directory under a base data directory, $HOME/.redactproxy by default or wherever --data-dir points:

$HOME/.redactproxy/
└── engagements/
    └── eng-2026-014/
        ├── tokens.db      # real <-> fake mapping (bbolt), persists across runs
        ├── rules.json     # enabled categories, custom block/allow entries
        ├── upstream.txt   # this engagement's API provider, if not Anthropic
        └── debug.log      # opt-in only, contains REAL values when enabled

Passing --engagement also drops a .redactproxy-engagement marker in the working directory, and later commands run from that folder pick the name up from there, so you only have to type it once. Passing a different name overwrites the marker.

None of this is sent anywhere. There is no telemetry, no sync, no backup.

tokens.db is crash-consistent. A value committed to disk survives an abrupt kill, not just a clean shutdown. A request in flight when the process dies fails or times out like any other network interruption; nothing ends up corrupted or silently mismapped.

Command reference

redactproxy [flags]             start the proxy
redactproxy wizard [flags]      interactive engagement setup, start here
redactproxy rules <subcommand>  show | validate | enable | disable | block | allow | remove
redactproxy tokens <subcommand> show | remove, against already-minted tokens
redactproxy memory [--write P]  print (or append) the CLAUDE.md placeholder note
redactproxy version             print the build this binary was made from

Every subcommand takes -h. A few things worth knowing up front.

rules block "XYZCorp" redacts that value wherever it appears, matching case-insensitively as a substring, so it catches "XYZCorporation" too. If the value is a domain, add --domain; see Blocking a domain.

rules allow "mylab.internal" is the opposite, and matches the exact value only. That's deliberately narrower than block: an allow entry takes protection away, so it should be as specific as possible.

rules disable <category> turns off a detector category (cloud.aws, network.mac, and so on) for one engagement. rules show lists all of them with descriptions. It also flags the allowlist.* categories, where disabling one means more gets redacted rather than less.

You can type the same things into a running proxy's terminal (show, remove <value>, rules ..., help) if you'd rather not interrupt a session to fix a mapping.

Rule changes only affect text tokenized from that point on. Adding a block rule won't scrub a value that already reached the model earlier in the same conversation, so start a fresh one if that matters. Disabling a category likewise only gates future detection; anything already tokenized keeps detokenizing correctly.

Blocking a domain

By default rules block treats its value as opaque text: matched and redacted, nothing about it preserved. Add --domain when the value really is a domain and it gets the same structure-preserving treatment as any domain the tool finds on its own, everywhere that domain turns up, whether as a subdomain, inside an email address, or in a URL:

redactproxy rules block --domain "xyzcorp-fixture.internal"

The wizard's "Domains" prompt does this for you, so anything entered there already counts as a domain entry.

Always give the base domain, no www. and no subdomain. Subdomains, emails and URLs are resolved from the base automatically; it doesn't work in the other direction. Entering a subdomain by mistake still works, since it resolves back to the base, but the CLI will point it out.

Internal-only names work too: an Active Directory forest, a private naming scheme, anything that will never appear on a public TLD list. --domain takes your word for it and only checks that the value is syntactically a hostname. A URL, an email address or an arbitrary string still gets added, but as an ordinary literal value, with a warning explaining why.

The same thing by hand in rules.json is an is_domain field:

{"value": "xyzcorp-fixture.internal", "regex": false, "is_domain": true}

A hand-edited file gets no interactive warning, so a nonsensical is_domain (set on a URL by accident, say) is handled quietly instead of breaking anything: it falls back to an ordinary literal block value, and redactproxy logs a one-time warning at startup or reload.

Debug logging

--debug-level controls diagnostic logging to debug.log, off by default. Every level above off writes real client values to disk in plaintext. Treat the file like the engagement data itself and delete it when you're done.

Level Logs
off (default) nothing
new the real value the first time each one is seen
replacements every substitution, every time
full entire request/response bodies, real and tokenized

Levels are cumulative. full in particular grows fast, because every logged request body is the whole conversation history so far, so the log is rotated and gzipped once it passes --debug-log-max-mb (10 MiB by default).

Multiple engagements, multiple providers

--upstream accepts any base URL that speaks the Anthropic Messages API. It defaults to real Claude, and something like z.ai works with no changes on this side. The wizard sets this up interactively, including writing the provider's API key into the folder's .claude/settings.local.json, and each engagement remembers its own choice, so different client projects can run against different providers side by side.

A provider with a genuinely different wire format, such as the OpenAI-shaped tools Codex CLI uses, won't work. The detection and streaming code is built around the Messages API shape specifically; z.ai works because it deliberately exposes an Anthropic-compatible endpoint, not because this is provider-agnostic.

To run several engagements at once, start one redactproxy per engagement on its own --listen address and point each Claude Code session at the matching port. One process serves exactly one engagement.

What this doesn't protect

Detection is regex-based and only ever sees request and response bodies. Known gaps:

  • Encoded data. A .env piped through base64, an xxd dump, Terraform state: all of it goes straight through. Decode it locally and look first.
  • Names and prose. Only recognizable shapes get detected, so client names, codenames and similar need rules block.
  • Bare apex domains on file-extension TLDs (.do, .ai, .rs, .sh, .py and friends), since main.rs and logo.ai are far more often filenames. Use rules block --domain for those.
  • Your folder name. Claude Code puts the working directory in the system field, which is never scanned, so don't name an engagement folder after the client. redactproxy warns about it but can't fix it.
  • thinking blocks, which are signed by the API and so can't be rewritten. If a real value leaks some other way, the model can restate it there permanently. Start a new conversation.
  • HTTP headers, forwarded verbatim and never inspected.
  • Claude Code's own transcripts, which keep the real values you saw on screen, same as terminal scrollback.
  • Claude Code's other traffic. Feature flags and token refresh don't go through ANTHROPIC_BASE_URL at all. The wizard's settings hardening closes what config can close.

Found one that isn't on this list? Please report it: see SECURITY.md.

Performance

Scan results are cached by content hash, so you pay per distinct string, once. Conversation history gets resent every turn, so most of a typical request is cache hits.

The exception is a body that mints hundreds of new values at once, a full-subnet nmap the first time you run it being the obvious one. Each value is its own committed write, which is what makes the store crash-safe and what turns milliseconds into seconds. Sending the same output again costs nothing.

Building and testing

make build   # bin/redactproxy for the current platform
make test    # go test ./...
make race    # go test -race -count=1 ./...
make vet     # go vet ./...
make lint    # golangci-lint (installed separately)
make check   # vet + race, run this before calling anything done
make fuzz    # exploratory fuzzing, 60s per target
make dist    # cross-compiled binaries (linux/darwin/windows, amd64/arm64)

Requires Go 1.26.6 or newer. See CONTRIBUTING.md to work on the code and SECURITY.md to report a leak.

License

Copyright (C) 2026 Cyber Security & Privacy Foundation

Released under the GNU General Public License v3.0 or later (GPL-3.0-or-later). This program comes with ABSOLUTELY NO WARRANTY; see the license for details.

Directories

Path Synopsis
cmd
redactproxy command
Command redactproxy runs the local HTTP proxy that sits between Claude Code and the real Anthropic API, tokenizing real PII out of every outbound request and detokenizing it back into every inbound response.
Command redactproxy runs the local HTTP proxy that sits between Claude Code and the real Anthropic API, tokenizing real PII out of every outbound request and detokenizing it back into every inbound response.
internal
debuglog
Package debuglog provides opt-in, leveled diagnostic logging for redactproxy, entirely separate from the normal operational log (startup, rules reload, errors) that always goes to stderr.
Package debuglog provides opt-in, leveled diagnostic logging for redactproxy, entirely separate from the normal operational log (startup, rules reload, errors) that always goes to stderr.
jsonwalk
Package jsonwalk locates and surgically rewrites the content-carrying string fields inside an Anthropic Messages API request or response body (system prompt, message text, tool_result content, tool_use input), leaving every other byte (key order, protocol fields, unrelated values) untouched.
Package jsonwalk locates and surgically rewrites the content-carrying string fields inside an Anthropic Messages API request or response body (system prompt, message text, tool_result content, tool_use input), leaving every other byte (key order, protocol fields, unrelated values) untouched.
proxyserver
Package proxyserver implements the HTTP proxy that sits between Claude Code (pointed here via ANTHROPIC_BASE_URL) and the real Anthropic API.
Package proxyserver implements the HTTP proxy that sits between Claude Code (pointed here via ANTHROPIC_BASE_URL) and the real Anthropic API.
redact
Package redact implements PII detection and bidirectional substitution (real value <-> token) over arbitrary text, backed by a tokenstore.Store.
Package redact implements PII detection and bidirectional substitution (real value <-> token) over arbitrary text, backed by a tokenstore.Store.
redact/redacttest command
Command redacttest is a read-only manual-testing harness: it does not modify any redaction logic.
Command redacttest is a read-only manual-testing harness: it does not modify any redaction logic.
rules
Package rules holds the persisted, per-engagement customization on top of the built-in redact detector set: which detector categories/ subcategories are disabled, and custom block/allow entries.
Package rules holds the persisted, per-engagement customization on top of the built-in redact detector set: which detector categories/ subcategories are disabled, and custom block/allow entries.
tokenstore
Package tokenstore provides a persistent, process-local, bidirectional mapping between real sensitive values (domains, IPs, emails, phone numbers) and fake token values, backed by bbolt with a full in-memory mirror for zero-disk-I/O lookups on the hot path.
Package tokenstore provides a persistent, process-local, bidirectional mapping between real sensitive values (domains, IPs, emails, phone numbers) and fake token values, backed by bbolt with a full in-memory mirror for zero-disk-I/O lookups on the hot path.
tokenstore/crashtest command
crashtest is a test helper, not a shipped binary: it continuously mints tokens for a known sequence of real values, printing each one to stdout immediately after it commits, so a driver (see internal/tokenstore/crash_test.go) can SIGKILL it at an arbitrary point and verify every value printed before the kill survives with the same token after a fresh Open, proving the token store is genuinely crash- consistent, not just safe across a clean Close.
crashtest is a test helper, not a shipped binary: it continuously mints tokens for a known sequence of real values, printing each one to stdout immediately after it commits, so a driver (see internal/tokenstore/crash_test.go) can SIGKILL it at an arbitrary point and verify every value printed before the kill survives with the same token after a fresh Open, proving the token store is genuinely crash- consistent, not just safe across a clean Close.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL