privllm is a self-hosted, OpenAI-compatible privacy gateway for AI agents. It transparently redacts sensitive data before it reaches upstream LLM providers, then restores the original values in the response so the client never sees aliases, and the provider never sees secrets.
privllm is not a one-way scrubber. It performs a bidirectional rewrite:
- Outbound: Every string in the request is scanned. Detected values are replaced with stable, realistic aliases and stored in an in-memory mapping table.
- Inbound: Every string in the response is scanned. Known aliases are replaced with their original values before the client sees them.
- Stable aliases: The same original value always gets the same alias within a namespace/TTL window, multi-turn conversations stay consistent.
- Namespace isolation: Each client API key gets its own isolated mapping (SHA-256 hash of the bearer token). No cross-tenant leakage.
- TTL-bounded: Mappings expire after a configurable TTL (default 12 hours).
- Schema-aware: For chat completions,
contentin all messages (user/system/developer/tool/assistant) is redacted.
| Category | What It Matches | Example Alias |
|---|---|---|
| URLs | https?://, ftp://, s3://, postgresql://, redis://, ssh://, git://, ws://, wss://, gs://, file://, mysql://, mongodb:// |
http://localhost-a1b2c3.example/x/x |
| Emails | Standard email patterns | user-a1b2c3@example.local |
| IP Addresses | IPv4 and IPv6 (validated; rewritten to looback-range addresses) | 127.42.1.1, ::1:a1b2 |
| Phone Numbers | Common formatted phone numbers with optional country code, separators, parentheses, extensions, and spaced 4-3-3 mobile formats |
phone-a1b2c3 |
| Credit Card Numbers | 13-19 digit card numbers that pass Luhn, plus strict grouped 4-4-4-4 card-like sequences with spaces or hyphens |
card-a1b2c3 |
| Social Security Numbers | Hyphen- or space-delimited U.S. SSN patterns with invalid ranges rejected | ssn-a1b2c3 |
| Domains | Domain names preceded by domain:, host=, hostname:, site: |
localhost-a1b2c3.example |
| Paths | Unix absolute paths, Windows paths (C:\...), UNC paths (\\server\share) |
/x42/x42/file, C:\x42\x42 |
| Names | Person names via curated wordlists (35K first names, 42K surnames, 463K English words for false-positive suppression) | person-a1b2c3, surname-a1b2c3 |
| Addresses | Multilingual street addresses (Latin, CJK, Cyrillic, Arabic, Devanagari scripts; 300+ street keywords across EN/DE/FR/ES/PT/IT/NL/SL/TR/ID); PO Box, postal codes, multi-line block detection | address-a1b2c3 |
| Headers | Header-style text: Authorization:, Cookie:, X-Api-Key:, Api-Key:, X-Auth-Token: (parses Basic/Bearer credentials and cookie pairs) |
auth-a1b2c3, cookie-a1b2c3 |
| Secrets | OpenAI/Anthropic API keys, AWS AKIA/ASIA keys, GitHub tokens (all gh*_ variants), JWT tokens (starts with eyJ), NTLM hashes, inline Bearer/Basic auth, password/secret assignments |
sk-a1b2c3, ghp-a1b2c3, jwt-a1b2c3 |
| Blacklist | Custom words from blacklist.txt (one per line, # comments) |
blacklist-a1b2c3 |
privllm proxies to a single upstream provider defined in the config file. Clients point directly at privllm's address and pass their own real API key — it is forwarded unchanged.
export OPENAI_BASE_URL="http://127.0.0.1:8080/v1"
export OPENAI_API_KEY="sk-your-real-api-key"This works with any OpenAI-compatible SDK, CLI, or agent framework.
Configure opencode (~/.config/opencode/opencode.jsonc) to route through privllm:
opencode sends requests → privllm redacts → upstream responds → privllm restores → opencode sees originals. Privllm logs the alias mappings locally — the upstream provider never sees the real values.
GOOSE_PROVIDER=openai \
GOOSE_OPENAI_BASE_URL=http://127.0.0.1:8080/v1 \
GOOSE_OPENAI_API_KEY=$OPENAI_API_KEY \
goose runcurl http://127.0.0.1:8080/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "My email is john.doe@acmecorp.com"}]
}'The response will contain john.doe@acmecorp.com restored, the provider saw only user-a1b2c3@example.local.
privllm --bind 127.0.0.1:8080 --config privllm.toml| Flag | Description |
|---|---|
--bind <ADDR> |
Bind address (default 127.0.0.1:8080) |
--config <PATH> |
TOML config file |
--print-redacted |
Print redact/restore pairs to stderr (originals visible locally — do not use in shared logs) |
--print-rewritten-prompts |
Print the sanitized request body with aliases highlighted in yellow |
Default config (OpenAI upstream, 12h TTL, all categories enabled, blacklist.txt):
blacklist_path = "blacklist.txt"
[upstream]
base_url = "https://api.deepseek.com/v1"
[mapping]
ttl_hours = 12
[redaction]
urls = true
domains = true
emails = true
paths = true
ips = true
phone_numbers = true
credit_card_numbers = true
social_security_numbers = true
names = true
addresses = true
headers = true
secrets = true
blacklist = trueDisable any category by setting it to false. Use DeepSeek, Anthropic, or any OpenAI-compatible provider by changing upstream.base_url.
GET /v1/modelsGET /v1/models/{model}POST /v1/chat/completions(streaming + non-streaming)POST /v1/responses(streaming + non-streaming)POST /v1/embeddings(non-streaming)
{ "$schema": "https://opencode.ai/config.json", "model": "privllm/deepseek-v4-flash", "provider": { "privllm": { "npm": "@ai-sdk/openai-compatible", "name": "PrivLLM", "options": { "baseURL": "http://127.0.0.1:8080/v1", "apiKey": "{env:OPENAI_API_KEY}" // your API key for the provider defined in privllm.toml }, "models": { "deepseek-v4-flash": { "name": "DeepSeek V4 Flash" } } } } }