PromptLatch
Redact secrets before prompts reach an LLM provider.
PromptLatch is a local proxy and Python library for coding agents, SDKs, and OpenAI-compatible backends. It scans request bodies and query parameters, replaces detected credentials and custom matches, then forwards the request.
Scanning stays local. PromptLatch has no telemetry or phone-home behavior.
Website: https://bvolpato.github.io/promptlatch/
Agent integration prompt: PROMPT.md
Choose a mode
| Need | Use |
|---|---|
| Protect coding agents and IDEs | Run promptlatch serve and point OpenAI-compatible clients at http://127.0.0.1:8000/v1. |
| Protect SDK calls in your app | Import redact_messages, redact_params, or redact_payload. |
Coverage
Default rules cover provider keys, personal access tokens, passwords, JWTs,
signed URLs, URL credentials, PEM/PGP private keys, and common secret fields such
as api_key, token, authorization, password, signed_url, and
credentials.
Detection uses deterministic provider rules plus your exact-tail or regex rules. Entropy-only matching is disabled to avoid unpredictable false positives.
Security boundary
- Request bodies and query parameters are scanned before they leave your machine.
- Audit logs record redaction counts and rule names without storing secret values.
- PromptLatch strips cookies and secret-named client headers. Provider credentials are
added from config or dedicated
X-Target-*headers after that filtering step. - Unknown private token formats need a custom exact-tail or regex rule.
See SECURITY.md for deployment defaults and remaining limits.
Install
Homebrew:
brew tap bvolpato/tap
brew install promptlatch
promptlatch version
uv:
uv tool install promptlatch
promptlatch doctor
Source:
git clone https://github.com/bvolpato/promptlatch.git
cd promptlatch
uv sync --extra dev --locked
uv run promptlatch doctor
ASGI servers can load promptlatch.asgi:app directly. Importing CLI or proxy
helpers does not load user config until a command or app requests it.
Upgrading from PromptCloak
Version 0.2 renamed package, command, environment variables, config directory, container image, and Helm chart. Old Python imports, environment variables, and default config path remain compatible through 0.2.x and emit migration warnings.
Remove old uv tool so stale promptcloak command cannot shadow new install:
uv tool uninstall promptcloak
Move local config before switching services:
mv ~/.config/promptcloak ~/.config/promptlatch
Existing Helm releases can upgrade in place without changing immutable selectors or rotating chart-managed proxy key:
helm upgrade <existing-release-name> ./charts/promptlatch \
--set migration.preserveLegacyNames=true
Keep migration flag on later upgrades for that release. Fresh installs should omit it
and use release name promptlatch. Existing external Secrets may keep
PROMPTCLOAK_SERVER_API_KEY for this upgrade, then rename key to
PROMPTLATCH_SERVER_API_KEY.
Run proxy
Configure an upstream, keep its key in your shell, and start PromptLatch:
promptlatch init --target-base-url https://api.openai.com/v1
export OPENAI_API_KEY="<openai-upstream-key>"
promptlatch serve
Point clients at http://127.0.0.1:8000/v1. For example:
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"messages": [{
"role": "user",
"content": "Here is my .env: OPENAI_API_KEY=<api-key-like-value>"
}]
}'
Provider receives OPENAI_API_KEY=[REDACTED_SECRET] in request content.
Verify redaction
A model reply cannot verify the forwarded request. Send a fixture token to an echo endpoint and inspect the echoed body:
FAKE_GEMINI_KEY="AI""zaSyFixtureToken000000000000000000000"
curl --compressed -fsS http://127.0.0.1:8000/post \
-H "X-Target-Base-URL: https://postman-echo.com" \
-H "Content-Type: application/json" \
--data "$(jq -nc --arg key "$FAKE_GEMINI_KEY" \
'{messages:[{role:"user",content:("GEMINI_API_KEY=" + $key)}]}')" \
| jq -r '.data.messages[0].content'
Expected output:
GEMINI_API_KEY=[REDACTED_SECRET]
Audit logs omit matched values and include counts and rule names. Homebrew users can run PromptLatch as a background service after configuring its environment:
brew services start bvolpato/tap/promptlatch
Use as a library
PromptLatch can run without running the proxy service. Import redaction helpers and filter request values before passing them to any SDK. PromptLatch does not install OpenAI, LiteLLM, LangChain, or Anthropic SDKs; examples assume those are already in your app.
uv add promptlatch
from promptlatch import redact_messages, scan_messages
messages = [
{
"role": "user",
"content": "Debug this .env: OPENAI_API_KEY=<api-key-like-value>",
}
]
safe_messages = redact_messages(messages)
result = scan_messages(messages)
assert result.stats.redactions >= 1
For custom tail-only rules:
from promptlatch import PromptLatch
from promptlatch.config import RedactionConfig, RuleConfig
latch = PromptLatch(
RedactionConfig(rules=[RuleConfig(type="exact", value="abcd1234", name="tail-only")])
)
safe_messages = latch.messages(messages)
OpenAI Python
from openai import OpenAI
from promptlatch import redact_messages, redact_params
client = OpenAI()
messages = [{"role": "user", "content": "API key: <api-key-like-value>"}]
response = client.chat.completions.create(
model="gpt-5.5",
messages=redact_messages(messages),
)
response_api = client.responses.create(
**redact_params(
model="gpt-5.5",
input="Summarize this config: OPENAI_API_KEY=<api-key-like-value>",
)
)
LiteLLM
from litellm import completion
from promptlatch import redact_params
messages = [{"role": "user", "content": "GEMINI_API_KEY=<api-key-like-value>"}]
response = completion(
**redact_params(
model="openai/gpt-5.5",
messages=messages,
)
)
LangChain
Tuple-style messages:
from langchain_openai import ChatOpenAI
from promptlatch import redact_messages
llm = ChatOpenAI(model="gpt-5.5")
response = llm.invoke(
redact_messages(
[
("system", "You are concise."),
("human", "Here is my token: <api-key-like-value>"),
]
)
)
LangChain message objects:
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
from promptlatch import redact_messages
llm = ChatOpenAI(model="gpt-5.5")
response = llm.invoke(
redact_messages(
[
HumanMessage(content="Here is my token: <api-key-like-value>"),
]
)
)
Anthropic Python
from anthropic import Anthropic
from promptlatch import redact_messages
client = Anthropic()
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
messages=redact_messages(
[{"role": "user", "content": "ANTHROPIC_API_KEY=<api-key-like-value>"}]
),
)
LlamaIndex
from llama_index.core.llms import ChatMessage
from llama_index.llms.openai import OpenAI
from promptlatch import redact_messages
llm = OpenAI(model="gpt-5.5")
response = llm.chat(
redact_messages(
[
ChatMessage(role="user", content="Here is my token: <api-key-like-value>"),
]
)
)
Raw HTTP or custom clients
import httpx
from promptlatch import redact_payload
payload = {
"model": "gpt-5.5",
"messages": [{"role": "user", "content": "secret=<api-key-like-value>"}],
}
response = httpx.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": "Bearer <provider-api-key>"},
json=redact_payload(payload),
)
Configuration
Default config: ~/.config/promptlatch/config.yaml
server:
host: 127.0.0.1
port: 8000
api_key: null
max_request_body_bytes: 33554432
target:
default_base_url: https://api.openai.com/v1
api_key: ${OPENAI_API_KEY}
api_key_header: authorization
forward_client_authorization: false
timeout_seconds: 180
allowed_base_urls: []
block_private_targets: true
redaction:
enabled: true
engine: detect-secrets
redact_mode: full
encrypted: false
max_extra_rules: 20
max_extra_rule_chars: 1024
allow_extra_regex_rules: false
rules:
- type: exact
value: abcd1234
name: tail-only-example
- type: regex
value: sk-[A-Za-z0-9_-]{20,}
name: openai-style-token
Store only key tails in exact rules. Full masking is default; partial masking is
available through redact_mode: partial.
Supported routes
PromptLatch forwards any path, with first-class tests for:
/v1/chat/completions/v1/responses/v1/completions/v1/models/v1/messagesfor Claude-compatible gateways
Tests cover streaming responses, tool payloads, and vision payloads. PromptLatch redacts recursively without reshaping JSON request schemas.
Provider targets
Set default backend in config, or choose one per request:
curl http://127.0.0.1:8000/v1/responses \
-H "X-Target-Base-URL: https://api.openai.com/v1" \
-H "X-Target-API-Key: $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-5.5","input":"scan this <api-key-like-value>"}'
Set X-Target-API-Key-Header: x-api-key for Anthropic-style upstream authentication.
Configured target keys are bound to target.default_base_url. A dynamic target that
requires authentication must receive its key through X-Target-API-Key or
X-Target-Authorization; PromptLatch never reuses configured key for another host.
An empty target.allowed_base_urls permits any public target. Add URLs to restrict
dynamic routing. Set block_private_targets: false only for trusted local targets.
Per-request rules are exact matches by default. Regex rules remain available in trusted config.
Set redaction.allow_extra_regex_rules: true only for authenticated clients you trust.
PromptLatch forwards routes without reshaping provider payloads.
| Target | Base URL | Auth header | Notes |
|---|---|---|---|
| OpenAI | https://api.openai.com/v1 |
authorization |
Native Chat Completions, Responses API, models, tools, streaming. |
| OpenRouter | https://openrouter.ai/api/v1 |
authorization |
Native Chat Completions and Responses. Use provider-prefixed model names. |
| Anthropic / Claude-compatible | https://api.anthropic.com |
x-api-key |
Forward /v1/messages; PromptLatch does not translate OpenAI JSON into Anthropic JSON. |
| Local Ollama or vLLM | http://127.0.0.1:11434/v1 or another local /v1 endpoint |
provider-specific | Set block_private_targets: false only for local-only configs. |
OpenRouter per request:
target:
allowed_base_urls:
- https://openrouter.ai/api/v1
curl http://127.0.0.1:8000/v1/chat/completions \
-H "X-Target-Base-URL: https://openrouter.ai/api/v1" \
-H "X-Target-API-Key: $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"openai/gpt-oss-120b","messages":[{"role":"user","content":"scan this <api-key-like-value>"}]}'
Anthropic-compatible target:
curl http://127.0.0.1:8000/v1/messages \
-H "X-Target-Base-URL: https://api.anthropic.com" \
-H "X-Target-API-Key: $ANTHROPIC_API_KEY" \
-H "X-Target-API-Key-Header: x-api-key" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"claude-opus-4-8","max_tokens":256,"messages":[{"role":"user","content":"scan this <api-key-like-value>"}]}'
Local OpenAI-compatible target:
target:
default_base_url: http://127.0.0.1:11434/v1
api_key: null
allowed_base_urls:
- http://127.0.0.1:11434/v1
block_private_targets: false
Codex with OpenRouter
OpenRouter accepts native Responses requests, so no compatibility bridge is needed.
Keep OpenRouter key in environment and send it through PromptLatch's dedicated target
header. Generic client Authorization is not forwarded.
Start PromptLatch:
mkdir -p ~/.config/promptlatch
cp examples/promptlatch-openrouter.config.yaml ~/.config/promptlatch/config.yaml
export OPENROUTER_API_KEY="<openrouter-upstream-key>"
promptlatch serve
The checked-in PromptLatch config restricts dynamic routing to OpenRouter and leaves
forward_client_authorization and responses_to_chat disabled.
Install Codex profile:
mkdir -p ~/.codex
cp examples/codex-openrouter-promptlatch.config.toml \
~/.codex/openrouter-promptlatch.config.toml
Profile contents:
model = "openai/gpt-oss-120b"
model_provider = "promptlatch-openrouter"
[model_providers.promptlatch-openrouter]
name = "PromptLatch OpenRouter"
base_url = "http://127.0.0.1:8000/v1"
wire_api = "responses"
env_http_headers = { "X-Target-API-Key" = "OPENROUTER_API_KEY" }
http_headers = { "X-Target-Base-URL" = "https://openrouter.ai/api/v1" }
request_max_retries = 0
stream_max_retries = 0
Run interactive Codex:
codex -p openrouter-promptlatch
Non-interactive smoke test:
codex exec -p openrouter-promptlatch --strict-config \
--sandbox read-only --ephemeral --cd "$PWD" \
"Reply with exactly: promptlatch-openrouter-ok"
Use any OpenRouter Responses-capable model by changing profile model. Current Codex
requests include Responses-only custom tool descriptors, so Codex needs a backend with
native Responses support. compat.responses_to_chat remains available for simpler
Responses clients limited to text, messages, and standard function tools.
OpenCode
Current stable OpenCode config supports custom Chat Completions providers through
@ai-sdk/openai-compatible. Copy checked-in example into project, or merge provider
block into existing opencode.json:
cp examples/opencode-openrouter-promptlatch.json opencode.json
export OPENROUTER_API_KEY="<openrouter-upstream-key>"
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"promptlatch-openrouter": {
"npm": "@ai-sdk/openai-compatible",
"name": "PromptLatch OpenRouter",
"options": {
"baseURL": "http://127.0.0.1:8000/v1",
"headers": {
"X-Target-Base-URL": "https://openrouter.ai/api/v1",
"X-Target-API-Key": "{env:OPENROUTER_API_KEY}"
}
},
"models": {
"openai/gpt-oss-120b": {
"name": "gpt-oss via PromptLatch"
}
}
}
},
"model": "promptlatch-openrouter/openai/gpt-oss-120b"
}
Run:
opencode run -m promptlatch-openrouter/openai/gpt-oss-120b \
--format json --dir "$PWD" \
"Reply with exactly: promptlatch-opencode-ok"
For another Chat Completions target, replace base URL, environment variable, and
model ID. Use PROMPTLATCH_TARGET_BASE_URL and PROMPTLATCH_TARGET_API_KEY instead
when PromptLatch owns one fixed upstream.
Claude Code
Claude Code sends Anthropic Messages requests. Configure provider key on PromptLatch, then use separate local bearer token for proxy authentication:
export ANTHROPIC_UPSTREAM_API_KEY="<anthropic-upstream-key>"
export PROMPTLATCH_TARGET_BASE_URL="https://api.anthropic.com"
export PROMPTLATCH_TARGET_API_KEY="$ANTHROPIC_UPSTREAM_API_KEY"
export PROMPTLATCH_TARGET_API_KEY_HEADER="x-api-key"
export PROMPTLATCH_SERVER_API_KEY="<local-proxy-key>"
promptlatch serve
export ANTHROPIC_BASE_URL="http://127.0.0.1:8000"
export ANTHROPIC_AUTH_TOKEN="$PROMPTLATCH_SERVER_API_KEY"
export DISABLE_TELEMETRY=1
export DO_NOT_TRACK=1
claude
PromptLatch validates local bearer token, removes it, then adds upstream x-api-key.
It forwards /v1/messages without translating between OpenAI and Anthropic schemas.
Redaction engine
PromptLatch uses bc-detect-secrets, provider token patterns, and user-defined
exact-tail or regex matches. It does not load or call a model.
Coverage includes fixture-shaped examples for:
- AI provider keys: OpenAI/Codex, Anthropic, Gemini, OpenRouter, Z.AI, MiniMax, DeepSeek, xAI/Grok, and Fireworks.
- Developer and cloud credentials: GitHub, GitLab, Atlassian, AWS, Cloudflare, Slack, Stripe, Google Cloud, Azure, npm, PyPI, and other common service tokens.
- Structured credentials: JWTs, signed URLs, URL userinfo, PEM keys, encrypted PEM keys, and PGP private keys.
- Labeled values and JSON fields such as
password,token,api_key,authorization,credentials,signed_url, andsas_token. - User-defined exact-tail and regex rules for private formats.
JSON is scanned structurally. Query parameters and unencoded non-JSON bodies, including multipart requests, are scanned without changing unrelated bytes. Encoded request bodies are rejected while redaction is enabled; decompress them before sending.
Every scan runs locally without an LLM. Entropy-only matching is disabled; use custom rules for opaque internal formats.
Encrypt rules at rest
uv run promptlatch encrypt-rules
This creates ~/.config/promptlatch/key with mode 0600, encrypts
redaction.rules with AES-GCM, writes redaction.encrypted_rules, and clears
plain rules.
You can also provide key material through:
export PROMPTLATCH_CONFIG_KEY="base64-url-safe-32-byte-key"
Docker
Published image:
# ~/.config/promptlatch/provider.env, mode 0600
PROMPTLATCH_TARGET_BASE_URL=https://api.openai.com/v1
PROMPTLATCH_TARGET_API_KEY=<openai-upstream-key>
docker run -d --name promptlatch --rm \
-p 127.0.0.1:8000:8000 \
--env-file "$HOME/.config/promptlatch/provider.env" \
ghcr.io/bvolpato/promptlatch:0.2.1
curl --retry 10 --retry-connrefused --retry-delay 1 \
-fsS http://127.0.0.1:8000/healthz
docker stop promptlatch
Build current checkout:
docker build -t promptlatch:local .
Compose:
export OPENAI_API_KEY="<openai-upstream-key>"
docker compose up --build
Helm
Local chart:
kubectl create secret generic promptlatch-env \
--from-env-file="$HOME/.config/promptlatch/kubernetes.env"
helm install promptlatch ./charts/promptlatch \
--set env.PROMPTLATCH_TARGET_DEFAULT_BASE_URL=https://api.openai.com/v1 \
--set existingSecret=promptlatch-env
kubectl wait deployment/promptlatch --for=condition=Available --timeout=90s
export PROMPTLATCH_SERVER_API_KEY="$(
kubectl get secret promptlatch-env \
-o jsonpath='{.data.PROMPTLATCH_SERVER_API_KEY}' | base64 --decode
)"
kubectl port-forward svc/promptlatch 8000:8000
In another shell:
curl -fsS http://127.0.0.1:8000/healthz
helm uninstall promptlatch
Release asset:
helm pull https://github.com/bvolpato/promptlatch/releases/download/v0.2.1/promptlatch-0.2.1.tgz
helm install promptlatch ./promptlatch-0.2.1.tgz \
--set env.PROMPTLATCH_TARGET_DEFAULT_BASE_URL=https://api.openai.com/v1 \
--set existingSecret=promptlatch-env
kubernetes.env must contain PROMPTLATCH_TARGET_API_KEY and
PROMPTLATCH_SERVER_API_KEY; keep file outside repository with mode 0600.
Without existingSecret, chart generates proxy key and stores secretEnv values in
chart-managed Secret. Send Authorization: Bearer $PROMPTLATCH_SERVER_API_KEY on
proxied requests. Health probes remain unauthenticated.
Emergency request tracing
promptlatch serve --debug-requests logs raw request bodies before redaction. Restrict it to local fixture data and cases where an echo target is insufficient. Auth, target-key, and redaction-rule headers are masked; body text is visible.
Development
uv sync --extra dev
uv run scripts/audit_secrets.py
uv run pytest
uv run ruff check .
uv build
uv run promptlatch scan 'OPENAI_API_KEY=<api-key-like-value>'
Fixtures are split in source so no real or contiguous fake keys are committed. Release and test commands live in CONTRIBUTING.md. Report security problems through the private path in SECURITY.md, without posting real secrets.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file promptlatch-0.2.1.tar.gz.
File metadata
- Download URL: promptlatch-0.2.1.tar.gz
- Upload date:
- Size: 1.6 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c1080ea91d945480c4d788622a5b95e69342dbbe7573145aae47180c101c5e2f
|
|
| MD5 |
25e33be98ca5af44b41e45227ffc29f0
|
|
| BLAKE2b-256 |
d7537e7038314213ac8d4fd01cc0f2cf1d488b80e909dd60501a2469c49f26d4
|
Provenance
The following attestation bundles were made for promptlatch-0.2.1.tar.gz:
Publisher:
release.yml on bvolpato/promptlatch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
promptlatch-0.2.1.tar.gz -
Subject digest:
c1080ea91d945480c4d788622a5b95e69342dbbe7573145aae47180c101c5e2f - Sigstore transparency entry: 2468317764
- Sigstore integration time:
-
Permalink:
bvolpato/promptlatch@f5f4c9dca3174097ba3e864b0afc462f950d36f4 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/bvolpato
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f5f4c9dca3174097ba3e864b0afc462f950d36f4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file promptlatch-0.2.1-py3-none-any.whl.
File metadata
- Download URL: promptlatch-0.2.1-py3-none-any.whl
- Upload date:
- Size: 35.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9caf97f3d9a0abcb3337528d800215162ad7241833d1a9291b937eb28e613994
|
|
| MD5 |
432c7fe7c4a3af3733b26ba4974a987b
|
|
| BLAKE2b-256 |
184cd071364c436aac31c64c008d81ed53661477ecc126da6eca65d7d032f979
|
Provenance
The following attestation bundles were made for promptlatch-0.2.1-py3-none-any.whl:
Publisher:
release.yml on bvolpato/promptlatch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
promptlatch-0.2.1-py3-none-any.whl -
Subject digest:
9caf97f3d9a0abcb3337528d800215162ad7241833d1a9291b937eb28e613994 - Sigstore transparency entry: 2468317772
- Sigstore integration time:
-
Permalink:
bvolpato/promptlatch@f5f4c9dca3174097ba3e864b0afc462f950d36f4 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/bvolpato
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f5f4c9dca3174097ba3e864b0afc462f950d36f4 -
Trigger Event:
push
-
Statement type: