Skip to content

Local proxy (client-side PII)

The local proxy (cloak-proxy) is a localhost, OpenAI-compatible endpoint that tokenises PII on your own machine before anything leaves it, then detokenises the reply locally. You change exactly one line of your existing OpenAI code — the base_url — and every request now leaves your machine carrying only opaque tokens (<<CLOAK:CONTACT_EM:22fff1>>), never the raw PII.

The proxy is gateway-locked by design: the already-tokenised request always flows through the CloakAPI gateway to your provider. The gateway is a blind relay — it sees only tokens, never your raw data — and that is exactly what lets it meter the call, sign a receipt, and bill the markup on every request. Your provider, model and BYOK provider key choice are configured in your CloakAPI account; the proxy itself accepts no provider URL, so there is no unmetered, receipt-less path.

your app → cloak-proxy (tokenise on YOUR machine)
→ CloakAPI gateway (blind relay: tokens only; metering + signed receipt)
→ your AI provider (your BYOK key or CloakAPI's pooled key)

You need two secrets from your CloakAPI account: your tenant API key and your pre-tokenised trust key (the per-tenant HMAC key that proves to the gateway the request was already tokenised client-side, so it must relay blind).

Install & run

Terminal window
mkdir -p ~/.cloak
printf '%s' 'cloak_your-tenant-api-key' > ~/.cloak/cloakapi.key
printf '%s' 'your-pretokenised-trust-key' > ~/.cloak/trust.key
chmod 600 ~/.cloak/cloakapi.key ~/.cloak/trust.key
docker run --rm -p 127.0.0.1:8799:8799 \
-e CLOAK_API_KEY_FILE=/run/secrets/cloakapi.key \
-e CLOAK_TENANT_HMAC_KEY_FILE=/run/secrets/trust.key \
-e CLOAK_PROXY_LISTEN=0.0.0.0:8799 \
-e CLOAK_NER=off \
-v ~/.cloak/cloakapi.key:/run/secrets/cloakapi.key:ro \
-v ~/.cloak/trust.key:/run/secrets/trust.key:ro \
cloakapi/proxy:latest

Load the published image tarball first:

Terminal window
curl -fsSLO https://app.cloakapi.io/proxy/cloakapi-proxy-0.2.0-docker.tar.gz
curl -fsSLO https://app.cloakapi.io/proxy/cloakapi-proxy-0.2.0-docker.tar.gz.sha256
sha256sum -c cloakapi-proxy-0.2.0-docker.tar.gz.sha256 # must print OK
docker load -i cloakapi-proxy-0.2.0-docker.tar.gz

Linux x86_64 binary (no Docker)

Terminal window
curl -fsSLO https://app.cloakapi.io/proxy/cloak-proxy-0.2.0-linux-x86_64
curl -fsSLO https://app.cloakapi.io/proxy/cloak-proxy-0.2.0-linux-x86_64.sha256
sha256sum -c cloak-proxy-0.2.0-linux-x86_64.sha256 # must print OK
chmod +x cloak-proxy-0.2.0-linux-x86_64 && mv cloak-proxy-0.2.0-linux-x86_64 /usr/local/bin/cloak-proxy
CLOAK_API_KEY_FILE=$HOME/.cloak/cloakapi.key \
CLOAK_TENANT_HMAC_KEY_FILE=$HOME/.cloak/trust.key \
cloak-proxy

macOS and Windows binaries are coming soon on the downloads page.

Confirm it is up:

Terminal window
curl -s http://localhost:8799/healthz
# {"service":"cloak-proxy","status":"ok"}

Point your OpenAI code at it

from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8799/v1", # the ONLY change
api_key="unused", # the proxy holds your CloakAPI tenant key
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Email jane.roe@example.com, call +44 20 7946 0958."}],
)

Configuration

VarDefaultMeaning
CLOAK_API_KEY_FILEPath to a file holding your CloakAPI tenant API key (never logged)
CLOAK_TENANT_HMAC_KEY_FILEPath to a file holding your pre-tokenised trust key (never logged)
CLOAK_GATEWAY_BASE_URLhttps://api.cloakapi.io/api/v1The CloakAPI gateway. Gateway-locked: only *.cloakapi.io (or loopback, for dev) is accepted — a direct provider URL is refused at startup
CLOAK_PROXY_LISTEN127.0.0.1:8799Bind address (localhost-only by default). Port is configurable — see below.
CLOAK_NERautoFree-form NAME protection: auto / on / off
CLOAK_NER_FALLBACKfail-closedWhen auto + model unreachable: fail-closed or l1-only
CLOAK_NER_BASE_URLhttp://127.0.0.1:11434/v1Loopback Ollama endpoint (must be loopback)
CLOAK_NER_MODELphi3.5Local NAME-detection model tag

The default port (8799) is a default, not a requirement. We picked 8799 specifically to avoid clashing with common data-science tools (RStudio Server / Dask on 8787, Jupyter on 8888). If 8799 is already taken on your machine — or you just prefer another port — set CLOAK_PROXY_LISTEN=127.0.0.1:<free-port> and point your SDK’s base_url at the same port. If the port is busy the proxy exits with a clear message telling you exactly that (it never silently rebinds, so your base_url always stays predictable). Example:

Terminal window
CLOAK_PROXY_LISTEN=127.0.0.1:8801 cloak-proxy
# then: base_url="http://localhost:8801/v1"

NAME coverage needs a local model (honest note)

Structured PII works standalone — email, phone, card PAN, SSN, IBAN, IP, DOB, passport, UK NINO, medical-record IDs, API keys — all tokenised with no extra dependencies. Run with CLOAK_NER=off to get exactly this.

Free-form PERSON names carry no structural signal, so they need a small local NER model. The proxy talks to a loopback-only Ollama endpoint running Phi-3.5-Mini by default:

Terminal window
ollama pull phi3.5 && ollama serve
# then run the proxy with the default CLOAK_NER=auto

The local model only detects name spans; the deterministic engine still mints the <<CLOAK:PERSON:…>> tokens, so the same name always maps to the same token and is restored locally. The NER endpoint is enforced loopback — no fragment of your text is ever sent to a non-local host.

With the default CLOAK_NER=auto, if the local model is unreachable the proxy fails closed (HTTP 503) rather than let a name egress — unless you opt into CLOAK_NER_FALLBACK=l1-only (structured PII still tokenised, names not, loudly labelled). Every response carries an x-cloak-name-protection: active|disabled|fail-closed header.

Status (honest)

Implemented + live-proven: non-streaming /v1/chat/completions, /v1/models passthrough, /healthz; structured-PII round-trip; the client-side NAME layer; and the gateway pre-tokenised passthrough (the only forwarding mode — every call is metered and carries a signed receipt, while the gateway sees only tokens). Phase 2 (not yet built): SSE streaming ("stream": true returns 501) and Anthropic-native /v1/messages.