Skip to main content

Cloakwall

PII/PHI redaction and tamper-evident audit logging for LLM traffic. It strips emails, card numbers, SSNs, medical record numbers, API keys and more from requests before they leave your machine, and records activity in a hash-chained audit log that can reveal edits when verified against a trusted key or checkpoint.

No sidecar, no external service, no model download, no third-party dependencies. It runs unchanged in an air-gapped network.

user:  "Patient MRN: A1234567, email bob@clinic.org, card 4111111111111111"
model: "Patient <MRN>, email <EMAIL>, card <CARD>"

Start by checking what you would be leaking:

pip install cloakwall
cloakwall scan ./my-project ./chat-logs
Found 266 sensitive values in 5 files:
  EMAIL        214
  CARD          38
  API_KEY       12
  AWS_KEY        2

scan reads files on your machine, including .env files and JSON or JSONL chat logs, and reports what kind of sensitive value it found, how many and where (--details adds file and line). It never prints a value, never writes anything and never connects to the network. It exits with 1 when it finds something, so it can also guard a CI pipeline. (The numbers above are an illustration of the format.)

Then protect the traffic, one of two ways:

Standalone proxy (cloakwall serve) LiteLLM guardrail
Works with any OpenAI-compatible app, SDK or agent an existing LiteLLM proxy
Dependencies none (standard library only) LiteLLM
Restore real values in replies yes (--restore) no

Standalone proxy

pip install cloakwall
export OPENAI_API_KEY=sk-...
cloakwall serve --upstream https://api.openai.com/v1 --upstream-key-env OPENAI_API_KEY

Then point your app at http://127.0.0.1:8787/v1 instead of the provider. Any OpenAI-compatible upstream works: OpenAI, OpenRouter, Together, a company gateway, a local vLLM. The proxy listens on localhost only unless you pass --host.

What happens to replies:

  • default: passed through unchanged.
  • --redact-output: PII in replies is masked too, streamed or not.
  • --restore: the reverse. Requests are redacted with one token per value (<EMAIL:7f3a91c2>), and the original values are put back into the reply before your app sees it. The model never sees the data and your app gets a normal answer. For an agent this means a command can use your real API key without the key ever leaving your machine. The token-to-value map lives in memory for one request and is never written anywhere.

Both work on streamed replies: text is held back briefly so a value, or a token, split across chunks is still caught whole. Tool-call arguments are restored as valid JSON. Output redaction uses a 64-character holdback and a 4,096-character pending-buffer limit by default, supporting candidate spans up to 4,033 characters while streaming. If a potential sensitive candidate exceeds that limit, Cloakwall aborts the stream without flushing the pending text; ordinary long text is released at safe boundaries unchanged.

Other options: --block-on CARD SSN rejects requests containing those entities instead of redacting them. Card-shaped candidates are redacted regardless of their Luhn result; the deprecated --strict-cards flag remains accepted for compatibility. --entities limits what is detected. --audit-path sets the log file, and cloakwall verify <log> checks it.

If you omit --upstream-key-env, the caller's Authorization header is passed upstream instead. Keys are never logged, and never read from the command line, which ends up in shell history.

Agent harnesses

Coding and agent tools read whole projects, .env files included, and send what they read to the model. Run the proxy with --restore and point the tool at it.

pi (~/.pi/agent/models.json):

{
  "providers": {
    "cloakwall": {
      "baseUrl": "http://127.0.0.1:8787/v1",
      "api": "openai-completions",
      "apiKey": "$OPENAI_API_KEY",
      "models": [{ "id": "gpt-4o" }]
    }
  }
}

Then pi --model cloakwall/gpt-4o. oh-my-pi is built on pi: add the same provider through its custom-provider settings (see the oh-my-pi docs).

Hermes Agent: run hermes model, choose Custom endpoint, and enter http://127.0.0.1:8787/v1 as the base URL.

Streaming output processing (--redact-output, --restore) covers /chat/completions and /completions, which is what these tools use. A streamed request to another endpoint with output processing on is refused rather than passed through unprocessed.

Point every app at it at once

The official OpenAI SDKs, and many tools built on them, read OPENAI_BASE_URL. Set it once and every such app on the machine goes through Cloakwall, with no code change:

export OPENAI_BASE_URL=http://127.0.0.1:8787/v1      # macOS / Linux: add to ~/.zshrc or ~/.bashrc
setx OPENAI_BASE_URL http://127.0.0.1:8787/v1        # Windows: permanent, for new terminals

Run it permanently

The proxy is a background service: start it once and it keeps running. Each recipe below is a file in examples/run/, and each is installed for real on a fresh machine of that OS by CI on every change. None of them puts the provider key on a command line or in the service definition.

Linux (systemd user service), which starts at login and restarts if it stops:

pipx install cloakwall
mkdir -p ~/.config/cloakwall ~/.config/systemd/user
printf 'CLOAKWALL_UPSTREAM=https://api.openai.com/v1\nOPENAI_API_KEY=sk-...\n' > ~/.config/cloakwall/env
chmod 600 ~/.config/cloakwall/env
curl -fsSL https://raw.githubusercontent.com/Rishikesh-glitch/Cloakwall-v0.1/main/examples/run/cloakwall.service \
  | sed "s#__CLOAKWALL__#$(command -v cloakwall)#" > ~/.config/systemd/user/cloakwall.service
systemctl --user daemon-reload && systemctl --user enable --now cloakwall

macOS (launchd agent), with the key kept in the Keychain:

pipx install cloakwall
security add-generic-password -s cloakwall-upstream -a "$USER" -w     # prompts for the key
mkdir -p ~/.cloakwall ~/Library/LaunchAgents
B=https://raw.githubusercontent.com/Rishikesh-glitch/Cloakwall-v0.1/main/examples/run
curl -fsSL "$B/cloakwall-macos.sh" | sed "s#__CLOAKWALL__#$(command -v cloakwall)#" > ~/.cloakwall/run.sh
chmod 700 ~/.cloakwall/run.sh
curl -fsSL "$B/com.cloakwall.proxy.plist" | sed "s#__HOME__#$HOME#g" > ~/Library/LaunchAgents/com.cloakwall.proxy.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.cloakwall.proxy.plist

Windows (scheduled task at logon, no window), in PowerShell:

pip install cloakwall
[Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "sk-...", "User")
Invoke-WebRequest -OutFile windows-task.ps1 `
  https://raw.githubusercontent.com/Rishikesh-glitch/Cloakwall-v0.1/main/examples/run/windows-task.ps1
powershell -ExecutionPolicy Bypass -File windows-task.ps1

Docker, from a clone of this repository, reachable from this machine only, running as a non-root user:

docker build -t cloakwall-proxy -f examples/run/Dockerfile.proxy .
docker run -d --name cloakwall --restart unless-stopped \
  -p 127.0.0.1:8787:8787 -e OPENAI_API_KEY -v cloakwall-audit:/data \
  cloakwall-proxy --upstream https://api.openai.com/v1 --upstream-key-env OPENAI_API_KEY --restore

GitHub Codespaces / dev containers: copy devcontainer.json into the project's .devcontainer/, and add the key as a Codespaces secret named OPENAI_API_KEY. Every Codespace then starts with the proxy running and OPENAI_BASE_URL already pointing at it.

Each recipe uses --restore and OpenAI as the upstream; change either in the file. Check it is running with curl http://127.0.0.1:8787/cloakwall/health.

One proxy for a team or company. Run it once on a server inside your network rather than on every laptop, so there is one audit trail to send to your SIEM. The proxy has no client authentication of its own yet, so on a shared host: don't set --upstream-key-env (each app then sends its own provider key, which is passed through), and put the proxy behind your existing ingress or gateway for TLS and access control.

LiteLLM guardrail

LiteLLM's open-source guardrail framework supports custom guardrails plus Presidio, but Presidio means deploying and maintaining two extra containers, and audit logs, SIEM export and SSO sit behind the Enterprise licence. Cloakwall adds redaction and a configurable, tamper-evident audit trail as one plugin.

pip install "cloakwall[litellm]"

The litellm extra excludes 1.82.7, 1.82.8 and 1.107.0, LiteLLM releases that were hijacked to steal credentials in 2026. Pin the version you deploy.

guardrails:
  - guardrail_name: cloakwall
    litellm_params:
      guardrail: cloakwall.guardrail.Cloakwall
      mode: pre_call
      default_on: true
      redaction_mode: mask
      audit_path: /var/log/cloakwall/audit.log

To mask PII in model responses as well, add redact_output: true and set mode: [pre_call, post_call].

LiteLLM refuses to start without a master key (an API key for the proxy itself), so set one, then point LiteLLM at a config that enables the guardrail, such as examples/config.yaml:

export LITELLM_MASTER_KEY="sk-$(openssl rand -hex 32)"
litellm --config config.yaml

Clients then call the proxy with Authorization: Bearer $LITELLM_MASTER_KEY.

Or run the container, which installs the same packages from PyPI with LiteLLM pinned:

docker build -t cloakwall .
docker run -p 4000:4000 \
  -e LITELLM_MASTER_KEY="sk-$(openssl rand -hex 32)" \
  -e OPENAI_API_KEY=sk-... cloakwall

Run it on Linux in production. The audit log fsyncs every entry; that costs about 0.3 ms on Linux and about 100 ms on Windows (see Benchmarks).

What it detects

Personal and health data: email addresses, credit-card-shaped candidates, SSNs, IBANs, medical record numbers, NHS-shaped candidates, phone numbers including international formats, IPv4 addresses and dates of birth.

Secrets: API keys from OpenAI, Anthropic, Stripe, Google / Gemini, GitHub, Slack, Groq and Hugging Face, AWS access keys, JWTs and bearer tokens, PEM private keys (the whole block, even when it arrives in pieces over a stream), and the credentials in connection strings (postgres://user:password@host becomes postgres://<PASSWORD>@host). The whole key is always replaced: real keys contain - and _, and a redactor that stops at the first one leaves the tail of the key in the prompt.

Luhn and NHS modulus-11 checks are recorded as confidence signals, not used to decide whether a candidate is redacted. This prevents mistyped or truncated identifiers from reaching the model. Audit events include validation counts by detector, separating checksum_confirmed candidates from shape_only candidates; aggregating those counts gives per-detector checksum rates without recording the candidate values.

Missing a format you use? Open an issue with its shape (prefix and length), never a real key.

Where it looks

Every request field that can carry text, on every endpoint. The proxy and the guardrail check the same fields:

Request Fields redacted
Chat completions messages[].content (text parts), tool_calls[].function.arguments
Anthropic /v1/messages top-level system, text blocks, tool_use.input (every string inside), tool_result.content
Responses API input (string or items), function_call.arguments, function_call_output.output
Completions, image generation prompt
Embeddings input strings (token-id lists pass through)

Image, audio and file parts pass through untouched. Ids, roles, model names and function names are never rewritten.

With redact_output: true (--redact-output on the proxy), responses are redacted too: chat content and tool-call arguments, text completions, and Anthropic content blocks. Streamed responses go through a stream redactor that holds back the last 64 characters (stream_holdback) and only releases text at a boundary no entity spans, so a card number split across two chunks is still caught. The cost is that streamed text reaches the client 64 characters later; none of it is dropped. Output redaction is off by default, because it changes what callers receive.

Three redaction modes

Mode alice@corp.com becomes Use when
mask <EMAIL> the model has no business seeing the field
hash <EMAIL:7f3a91c2> you need to correlate a user across requests without storing the value
partial <CARD:****1111> support staff need the tail

hash is deterministic under a local HMAC secret set with CLOAKWALL_SECRET. Same person, same token, every request. Without a secret one is generated per boot, so tokens are stable within a run only.

Audit trail

Every entry carries a monotonic sequence number and commits to the previous entry. By default the chain uses an unkeyed SHA-256 digest. Optionally, set an HMAC key outside the audit files (for example through a secrets manager and an environment variable) when starting the proxy:

$env:CLOAKWALL_AUDIT_KEY = "load-a-long-random-key-from-your-secret-store"
python -m cloakwall serve --upstream https://api.openai.com/v1 `
  --audit-hmac-key-env CLOAKWALL_AUDIT_KEY

Verify a keyed log using the same key:

python -m cloakwall verify .\cloakwall-audit.log `
  --audit-hmac-key-env CLOAKWALL_AUDIT_KEY

HMAC detects edits by someone who can change the log but cannot access the key. Protect and back up that key separately; anyone who can access the key and rewrite the files can still forge entries. Do not put the key in the log, the command line, or beside the audit files.

The chain head is mirrored to a local .head file after every append, and checkpoint events are also sent to the configured SIEM periodically. The local log and local head are in the same trust domain: an attacker who can rewrite both can replace an unkeyed chain and its anchor. For an independent truncation check, retain a checkpoint in a separately controlled SIEM or system and export it as JSON before verification:

python -m cloakwall verify .\cloakwall-audit.log `
  --anchor-file .\trusted-checkpoint.json `
  --audit-hmac-key-env CLOAKWALL_AUDIT_KEY

The checkpoint JSON needs the seq and hash fields from the trusted anchor. An external anchor catches replacement or truncation back past that checkpoint; it does not prove that the local .head file itself is trusted.

That combination catches all four tamper modes:

chain intact, 10 entries, anchor matches
line 2: entry altered after writing
line 3: chain break
truncated: anchor records 10 entries, log holds 6

The last one is why the anchor exists. A bare hash chain does not survive truncation — a prefix of a valid chain is itself a valid chain, so dropping the final entries verifies clean. The anchor still knows how many there should be.

An anchor on the same filesystem detects accidental truncation and edits to the log alone, but is not an independent defense against someone who can rewrite both files. An anchor retained by your SIEM under separate access control gives the verifier an independent checkpoint. Supply it with --anchor-file (or use verify(anchor=...) in Python).

The log records which entity types were found and how many. It never records the values that were redacted — an audit log that quotes the PII it scrubbed is a second copy of the problem.

Appends and verification use an OS-level lock on a sibling .lock file, in addition to an in-process thread lock. This serializes separate worker processes that share the same audit path and protects the log-plus-anchor update as one transaction. Keep the log, anchor and lock file on a filesystem that supports the platform's file-locking semantics; do not assume locking is reliable on every network filesystem.

SIEM export

export CLOAKWALL_SIEM=splunk          # splunk | datadog | elastic
export CLOAKWALL_SIEM_URL=https://splunk.internal:8088/services/collector
export CLOAKWALL_SIEM_TOKEN=...

SIEM export runs on a worker thread and is best-effort; do not treat a send attempt as proof that the SIEM durably retained an anchor. Confirm the checkpoint is present and access-controlled before relying on it for verification. A failure writing the initial local request event stops the proxy request before it is forwarded. A failure writing a later response event cannot undo an upstream call that already happened; Cloakwall returns an error where possible, while a streaming response may already have reached the caller.

Air-gapped

The core is pure standard library. Nothing in the redaction, audit or hashing path opens a socket, so it runs unchanged in an isolated cluster. The proxy connects to exactly one place, the upstream you give it. Point that (or LiteLLM) at a local vLLM or Ollama endpoint and no data leaves your network at all.

Verify it yourself:

strace -f -e trace=connect python3 tests/test_cloakwall.py

Benchmarks

Measured, reproducible numbers are in BENCHMARKS.md: python3 benchmarks/bench.py. On a 2015 Core i3 under Linux, a clean request costs about 0.06 ms and a request with PII, including its fsynced audit entry, about 1.8 ms.

Tests

python3 tests/test_cloakwall.py     # 101 tests, no pytest required
python3 tests/test_proxy.py         # 42 proxy tests, fake upstream included
python3 tests/test_scan.py          # 22 scanner tests

Limitations

Regex detection does not catch contextual PII — a person's name in free text, or a medical condition described in prose. If you need that, Presidio's NLP models are better and Cloakwall is not a replacement for them. What Cloakwall gives up in recall it gains in having no dependencies, no sidecar and no model to ship into an air-gapped environment.

Redaction is one-way in the guardrail and in the proxy's default and --redact-output modes. Only --restore puts values back, and only into the reply to the request they came from, from a map held in memory for that request alone.

Cloakwall runs inside your process. If the process around it is compromised (a hijacked dependency, say), no in-process control can protect the data. Pin and verify what you deploy.

Status: working, tested, not yet run in production anywhere.

Security

Found a vulnerability, or a way PII gets past the redactor? Please report it privately; see SECURITY.md.

Licence

AGPL-3.0. Commercial licences for closed-source use are available.

Metadata

Release files for cloakwall 0.3.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cloakwall 0.3.2
File Size Uploaded Provenance Actions
cloakwall-0.3.2.tar.gz 72.2 kB Without provenance Details for cloakwall-0.3.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for cloakwall 0.3.2
File Interpreter ABI Platform Actions
cloakwall-0.3.2-py3-none-any.whl Python 3 none any Details for cloakwall-0.3.2-py3-none-any.whl

Total release size: 125.7 kB

Release files / cloakwall-0.3.2.tar.gz

Download URL cloakwall-0.3.2.tar.gz
Size 72.2 kB
Tags Source
SHA-256 checksum
How to use checksums
121d321fa8e52365b537e794aba9c150dbac90cad03962164eae1cf990a3e862
BLAKE2b-256 checksum
How to use checksums
a179fb26a0fd4283481081116a227a3bad83d95fe52ea4af9ba7577d55f483d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release files / cloakwall-0.3.2-py3-none-any.whl

Download URL cloakwall-0.3.2-py3-none-any.whl
Size 53.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
15f3b41406dacfba7ea45b930bea8e57c8f8a638e52cc729d0e2cf0e54c64343
BLAKE2b-256 checksum
How to use checksums
bc04d8e1bbda1b0a8096b597cdff68a5f10e73fe37c871c8e2cb9454cd17edf6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Security for cloakwall 0.3.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page