Local secret redaction

PromptLatch

PromptLatch runs locally and redacts credentials from LLM request content before sending it to a provider.

Start Verify GitHub
Artifacts Open source, MIT

Releases include Docker, Helm, and Homebrew packages. Release tags are signed.

Detection Deterministic local rules

Provider patterns, bc-detect-secrets, and custom tail or regex rules.

Logging Redacted audit logs

Logs omit matched values and include counts and rule names.

API OpenAI-compatible

Chat Completions, Responses API, legacy completions, models, and pass-through routes.

Verify

Check what leaves your machine.

Send a fixture token to a public echo endpoint and inspect the forwarded body. A model reply cannot verify the forwarded request.

brew tap bvolpato/tap
brew install promptlatch
promptlatch init
promptlatch serve
FAKE_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" \
  -d '{"messages":[{"content":"GEMINI_API_KEY='"$FAKE_KEY"'"}]}' \
  | jq -r '.data.messages[0].content'

# GEMINI_API_KEY=[REDACTED_SECRET]

Install

Install and run locally.

Use PromptLatch as a local proxy for agents, or import it before direct SDK calls.

brew tap bvolpato/tap
brew install promptlatch
promptlatch init --target-base-url https://api.openai.com/v1
export OPENAI_API_KEY="<openai-upstream-key>"
promptlatch serve
uv tool install promptlatch
promptlatch doctor
uv add promptlatch

Proxy mode

Choose upstream in config or per request.

PromptLatch redacts JSON and text bodies, forwards the original route, and leaves provider-specific fields intact. Set one default target, or send per-request target headers.

OpenAI default

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":"Reply exactly: ok"}' \
  | jq -r '.output[0].content[0].text'

Config default

target:
  default_base_url: https://api.openai.com/v1
  api_key: ${OPENAI_API_KEY}
  api_key_header: authorization
redaction:
  engine: detect-secrets
  redact_mode: full

OpenRouter request

# config.yaml
target:
  allowed_base_urls:
    - https://openrouter.ai/api/v1

curl http://127.0.0.1:8000/v1/responses \
  -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","input":"Reply exactly: ok"}'

Anthropic-compatible

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":64,"messages":[{"role":"user","content":"Reply exactly: ok"}]}'

Local Ollama/vLLM

target:
  default_base_url: http://127.0.0.1:11434/v1
  allowed_base_urls:
    - http://127.0.0.1:11434/v1
  block_private_targets: false

Routes

/v1/chat/completions
/v1/responses
/v1/completions
/v1/models
/v1/messages

Library

Redact before SDK calls.

Import PromptLatch helpers before OpenAI, LiteLLM, LangChain, Anthropic, or custom HTTP client calls.

OpenAI

from openai import OpenAI
from promptlatch import redact_messages

client = OpenAI()
client.chat.completions.create(
  model="gpt-5.5",
  messages=redact_messages(messages),
)

LiteLLM

from litellm import completion
from promptlatch import redact_params

messages = [{"role": "user", "content": "API_KEY=<secret>"}]

completion(**redact_params(
  model="openai/gpt-5.5",
  messages=messages,
))

Responses API

from promptlatch import redact_params

client.responses.create(**redact_params(
  model="gpt-5.5",
  input="OPENAI_API_KEY=<secret>",
))

LangChain

from langchain_openai import ChatOpenAI
from promptlatch import redact_messages

llm = ChatOpenAI(model="gpt-5.5")
llm.invoke(redact_messages([
  ("human", "token=<secret>"),
]))

Anthropic

from anthropic import Anthropic
from promptlatch import redact_messages

Anthropic().messages.create(
  model="claude-opus-4-8",
  max_tokens=1024,
  messages=redact_messages(messages),
)

LlamaIndex

from llama_index.llms.openai import OpenAI
from promptlatch import redact_messages

llm = OpenAI(model="gpt-5.5")
llm.chat(redact_messages(messages))

Agent configs

Point coding agents at local proxy.

Codex

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" }

# ~/.codex/openrouter-promptlatch.config.toml
# codex -p openrouter-promptlatch
cp examples/promptlatch-openrouter.config.yaml \
  ~/.config/promptlatch/config.yaml
cp examples/codex-openrouter-promptlatch.config.toml \
  ~/.codex/openrouter-promptlatch.config.toml
export OPENROUTER_API_KEY="<openrouter-upstream-key>"
promptlatch serve
codex -p openrouter-promptlatch

Use a Responses-capable backend. Current Codex custom tools do not fit the Chat Completions bridge.

OpenCode

{
  "$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"
}
export OPENROUTER_API_KEY="<openrouter-upstream-key>"
opencode run \
  -m promptlatch-openrouter/openai/gpt-oss-120b \
  "Reply with exactly: promptlatch-opencode-ok"

Claude Code

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
claude

Limits

Request redaction has a clear boundary.

PromptLatch scans request bodies and query parameters before forwarding. Some credential flows are outside that boundary.

Deploy

Keep personal proxy on localhost.

For network access, set a target allowlist and a local proxy API key.

Docker

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

Helm

kubectl create secret generic promptlatch-env \
  --from-env-file="$HOME/.config/promptlatch/kubernetes.env"

helm install promptlatch \
  https://github.com/bvolpato/promptlatch/releases/download/v0.2.1/promptlatch-0.2.1.tgz \
  --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
curl -fsS http://127.0.0.1:8000/healthz

Coverage

Tested credential formats.

Deterministic patterns cover provider keys, personal access tokens, JWTs, signed URLs, private key blocks, URL credentials, and assignment-style secrets.

GitHub PATs Atlassian OpenAI Gemini Anthropic OpenRouter Z.AI MiniMax DeepSeek Codex Grok/xAI Fireworks Cloudflare AWS Signed URLs PGP keys Slack Stripe JWTs PEM keys Custom rules

Security

Security properties.