cloakport
v0.1.0
Published
Reversible, deterministic, local-first PII redaction for LLM traffic: cloak sensitive values before they reach a model, restore them in the response.
Maintainers
Readme
cloakport
Reversible, deterministic, local-first PII redaction for LLM traffic.
cloakport replaces sensitive values with placeholder tokens before text
reaches a model, and restores the originals in the response — so the model works
with [EMAIL_1] while you keep working with real data.
Status: early. Detection is a strong first pass, not a guarantee. Review the output before trusting it with regulated or high-risk data.
Why
Sending prompts to a hosted LLM often means shipping emails, phone numbers, keys, and account numbers to a third party. Most existing tools redact destructively (you lose the original) or depend on a cloud NER service (more data egress). cloakport is different:
- Reversible — a vault maps each token back to its original value, so you can restore a model's response to real, usable text.
- Deterministic & local — detection is plain regex plus checksum validators (Luhn for cards, ISO mod-97 for IBANs). No network, no model download, fully auditable.
- Composable — a small engine with a CLI and a drop-in OpenAI-compatible proxy built on top of it. Zero runtime dependencies.
Install
git clone <this-repo> && cd cloakport
npm install && npm run build
npm link # exposes `cloakport` on your PATHRequires Node.js >= 20.
Usage
Redact / restore (CLI)
# Cloak PII; the vault records how to undo it (keep it private!)
cloakport redact notes.txt --map vault.json --out notes.redacted.txt
# ... send notes.redacted.txt to a model, get a reply that mentions tokens ...
# Restore the originals in the reply
cloakport restore reply.txt --map vault.json
# See what would be redacted, without changing anything
cloakport scan notes.txt --counts
cat notes.txt | cloakport scan --jsonDrop-in proxy
Point any OpenAI-compatible client's base URL at cloakport; it redacts request bodies, forwards upstream, and restores the response:
cloakport proxy --upstream https://api.openai.com --port 8787
# then set your client's baseURL to http://127.0.0.1:8787The per-request vault lives only in memory and is never logged or written to
disk. The proxy fails closed: JSON bodies are redacted field-by-field, any
other text body is redacted as plaintext, and a binary body is rejected rather
than forwarded raw. Streaming (text/event-stream) responses are restored on
line boundaries. Request bodies above a configurable cap (default 25 MB) are
rejected with 413.
See examples/proxy-demo.mjs for a self-contained,
runnable demo (npm run build && node examples/proxy-demo.mjs) that shows the
model receiving only tokens while the client gets real values back.
What it detects
| Type | Method |
|---|---|
| EMAIL, IPV4, IPV6, URL | pattern |
| CREDIT_CARD | pattern + Luhn checksum |
| IBAN | pattern + ISO 7064 mod-97 checksum |
| SSN (US), PHONE | pattern (+ digit-count check for phone) |
| JWT, AWS_ACCESS_KEY, API_KEY | pattern (common prefixes) |
Detection favors precision where a checksum exists and recall elsewhere. Limit
or widen it with --include / --exclude (e.g. --include EMAIL,PHONE).
How reversibility works
redact emits tokens like [EMAIL_1] and a vault:
{
"version": 1,
"createdAt": "2026-01-01T00:00:00.000Z",
"nonce": "9f3a2b7c",
"entries": [
{ "token": "[EMAIL_1_9f3a2b7c]", "type": "EMAIL", "value": "[email protected]" }
]
}The same original value always maps to the same token (pass a vault back into
redact to keep tokens stable across calls). Each vault carries a random
nonce that is embedded in every token (e.g. [EMAIL_1_9f3a2b7c]) so a
placeholder can never collide with token-shaped text that already exists in your
input. restore replaces tokens with their values, longest-token-first so names
never collide as substrings.
The vault contains your original secrets. Treat it as sensitive: store it separately from the redacted text and never send it to a model.
Limitations
- Heuristic detection: unusual PII formats, names, and addresses without a structured pattern are not caught. This is a filter, not a guarantee.
- The proxy inspects request/response bodies; it does not redact values placed in the URL/query string (the request path is forwarded upstream as-is).
- Body redaction walks string values; PII sent as a bare JSON number is not redacted.
- Detection runs on text; binary request bodies (e.g. audio/multipart uploads)
are rejected with
415rather than forwarded unredacted. - Tokens are stable within a shared vault (reuse a vault for cross-call stability); independent runs use a fresh random nonce, so tokens differ between runs by design.
Development
npm run build # tsc -> dist/
npm test # build + vitest
npm run test:watchLicense
MIT
