# Show HN: Secret Guard – a local LLM gateway; keys never leave your machine

> Source: <https://github.com/luochen1990/secret-guard>
> Published: 2026-10-18 15:24:48+00:00

**A lightweight local LLM gateway: outbound requests have their Secrets replaced with
equal-length realistic Mocks; responses are restored back to real values on the way in —
the LLM never touches a real Secret, while your Agent tools keep working.**

[Docs](https://secret-guard.lambda.lc/en/) · [Download](https://secret-guard.lambda.lc/en/download/) · [Quick Start](https://secret-guard.lambda.lc/en/tutorial-quick-start/) · [Protocol Support](#protocol-support) · [简体中文](https://github.com/luochen1990/secret-guard/blob/master/README.zh-CN.md)

The local Agent Harness (claude-code / opencode / hermes-agent and friends) does your work by stuffing material wholesale into the prompt — code, configuration, terminal output, along with the passwords, private keys, tokens, and cookies in them, all delivered to the LLM vendor's servers. Once that data leaves your machine, it is out of your control: it may end up in LLM vendor logs, be used for model training, or leak in a breach.

And "never put Secrets in the context" is simply not viable in Agent workflows —
credentials are exactly what an Agent tool needs to act on real systems on your behalf.
Secret Guard resolves this tension: **your Agent tools keep working with your Secrets,
while the LLM never touches a real value.**

The only integration cost is one line: point your Agent tool's API base URL at the local gateway, and traffic flows through it to the LLM vendor:

```
Outbound:  Agent tool ──real Secret──►  secret-guard  ──realistic Mock──►  LLM vendor
Return:    local tools ◄──real Secret──  secret-guard  ◄──Mock reply────  LLM vendor
                        (Redact / Restore both happen locally, transparent in both directions)
```

- **Outbound Redact** : Secrets in requests bound for the LLM are replaced with
equal-length, same-charset realistic Mocks (default strategy) — the LLM keeps working
with them, none the wiser.
- **Return Restore** : when LLM responses come back (tool-call arguments included), Mocks
are restored to real values — a curl command carrying a Secret lands on your machine
with authentication passing as usual.
- **Per-secret strategy** : redact/restore above is the default; a secret can instead be
configured`strategy = "reject"` to refuse forwarding (503) the moment it appears in a
request — for credentials that must never leave the machine — or`strategy = "placeholder"` to swap it for an explicit`[REDACTED:{id}]` token that is never
restored. Every placeholder secret doubles as a canary: if its real value ever shows
up in an LLM response, you get an alert (WARN log + WebUI timeline badge, no secret
values in the alert) while the response itself passes through untouched.
- **Built-in protection for Secret Guard's own credentials** : the sg API tokens it
issues (and the provider API keys you register with it) are protected automatically —
zero configuration, effective the instant a token is created. If a prompt injection
talks your Agent into pasting its own gateway token into the conversation, the LLM
vendor only ever sees an equal-length Mock.

The same request, before and after it leaves the machine (captured from a real run; only the token value is fictional):

```
Agent Harness sends:  GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstuvwxyz
LLM vendor receives:  GITHUB_TOKEN=2_9rr_61dr1lmv2wf15aotyyt262l3t9_2pt4uih
```

A quick tour of the WebUI — session timeline, the LLM's-eye request view with the Mock highlighted, and the restored real value on the response side:

Every forward can be inspected in the WebUI's Records page (session timeline + request detail, with Secret locations highlighted as the Mock — i.e. exactly what the LLM saw):

```
# Binary archives (Linux x86_64 / ARM64, fully static musl, with SHA256 checksums):
#   https://github.com/luochen1990/secret-guard/releases
#   archive name pattern: secret-guard-<version>-<triple>.tar.gz
#   (e.g. secret-guard-0.1.0-x86_64-linux-musl.tar.gz)

# Nix (flake):
nix run github:luochen1990/secret-guard -- --help
# or wire up the overlay / nixosModule into your flake and use pkgs.secret-guard

# Cargo (builds from source, requires rustc >= 1.96):
cargo install --git https://github.com/luochen1990/secret-guard
# or after cloning the repository: cargo install --path .
```

NixOS users should prefer the `services.secret-guard.*` structured-options deployment
(config validated at eval time, credentials injected via files) — see
[docs/deployment-nixos.md](https://github.com/luochen1990/secret-guard/blob/master/docs/deployment-nixos.md).

**Optional authentication (OIDC login) is a cargo feature**: the default build ships
without the OIDC login chain (smaller dependency graph). Binary archives and
`cargo install` therefore default to single-user mode; setting `[auth] enabled = true`
there fails at startup with a "rebuild with --features oidc" hint. To get OIDC:
build from source with `cargo install --path . --features oidc`, or use the NixOS
module (its default package enables the feature). The local API-key path is always
compiled in. See [docs/configuration.md](https://github.com/luochen1990/secret-guard/blob/master/docs/configuration.md) `[auth]`.

Create `secret-guard.toml` somewhere (one upstream provider + one secret to protect is
all it takes):

```
[[providers]]
id = "openai-main"
kind = "direct"                         # direct upstream (use "router" for routing endpoints, "pool" for plan pools)
api_key = "sk-your-upstream-key"        # entry-level credential shared by all endpoints — must come BEFORE [[providers.endpoints]] (TOML sub-table switch)
[[providers.endpoints]]
protocol = "openai"                     # at most one [[providers.endpoints]] per protocol;
base_url = "https://api.openai.com"     # unmatched ingress protocols fall back to the first endpoint

[[secrets.entries]]
id = "my-github-token"
value = "ghp_0123456789abcdefghijklmnopqrstuvwxyz"   # the secret to protect (never sent upstream)
```

Mind the nesting: secrets live under `[[secrets.entries]]` while providers are flat
`[[providers]]` (the two styles differ). Full field reference:
[docs/configuration.md](https://github.com/luochen1990/secret-guard/blob/master/docs/configuration.md).

```
secret-guard run --port 18787
curl http://127.0.0.1:18787/o/openai-main/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"My token is ghp_0123456789abcdefghijklmnopqrstuvwxyz, please summarize."}]}'
```

The LLM vendor receives a realistic Mock — `redactions=1` in the forward-completion log is
the replacement count; open `http://127.0.0.1:18787/` in a browser to review the request
in the Records page.

Examples use port `18787` throughout; the default listen address is `127.0.0.1:8787`
(changeable via `[server] port` or the `SG_PORT` environment variable).

For daily use, just point your Agent tool's `base_url` at the gateway — no curl needed:

``` python
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:18787/o/openai-main/v1", api_key="ignored")
```

URLs take the form `/{proto_short}/{provider_id}/*rest`, encoding both the **ingress
protocol** and the **target provider**:

| proto_short | Protocol | SDK example ( `base_url` ) | 
|---|---|---|
| `o` | OpenAI | `http://127.0.0.1:18787/o/<provider-id>/v1` | 
| `a` | Anthropic | `http://127.0.0.1:18787/a/<provider-id>` | 
| `g` | Gemini | `http://127.0.0.1:18787/g/<provider-id>` | 
| `l` | Ollama | `http://127.0.0.1:18787/l/<provider-id>` | 
| `r` | Responses (OpenAI Responses API) | `http://127.0.0.1:18787/r/<provider-id>/v1` | 

SDK notes: the OpenAI SDK needs the `/v1` prefix included yourself; the Anthropic SDK
appends `/v1/messages` on its own, so `http://.../a/<provider-id>` is enough.

The real API key lives **only** in your local `secret-guard.toml` — the harness itself holds a
dummy value. secret-guard injects the real key when forwarding upstream, so even a fully
compromised harness (or a breach of its cloud storage) never exposes your key.

**opencode** — custom provider via the OpenAI-compatible route, in `opencode.json`:

```
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "secret-guard": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "secret-guard (local)",
      "options": {
        "baseURL": "http://127.0.0.1:18787/o/<provider-id>/v1",
        "apiKey": "dummy-key-harness-never-sees-the-real-one"
      },
      "models": {
        "<model-id>": { "name": "<any display name>" }
      }
    }
  }
}
```

Model IDs come from the upstream list — the gateway passes `GET /v1/models` straight
through, so `curl http://127.0.0.1:18787/o/<provider-id>/v1/models` shows what you can use.

**claude-code** — two env vars, zero config:

```
export ANTHROPIC_BASE_URL=http://127.0.0.1:18787/a/<provider-id>
export ANTHROPIC_AUTH_TOKEN=dummy-key-harness-never-sees-the-real-one
```

Cross-protocol translation is built in (e.g. an OpenAI-form `o` route to an Anthropic
upstream, streaming included), so pick the route that matches your harness — not your
upstream.

- `secret-guard.toml` — declarative config, hand-written, read-only in the process.
- `secret-guard.state.toml` — dynamic state the WebUI persists on edit; delete it to
reset. A dynamic source can override the static one for the same entity.

**Sensitive data warning**: `state.toml` contains **plaintext** secrets (WebUI-created
secret values, explicitly written provider api_keys) at the same sensitivity level as
`secret-guard.toml` — keep it in `.gitignore`; an accidental commit leaks Secrets into
version history.

⚠️ 

Every completed forward logs a one-line INFO summary
(`forward ... status=... elapsed_ms=... redactions=N provider=...`), so you can confirm
traffic passes through secret-guard from the command line; upstream failures (502/504)
carry readable causes in the error body. Lower the verbosity with
`RUST_LOG=secret_guard=warn` (default `info`) when it gets noisy.

Handing production traffic to an intermediary process requires proof it won't get in the way. The following properties are pinned in the repository by property-based tests and end-to-end integration tests, fully verified on every release:

- **Transparent relay** : without Redact, byte-exact pass-through; with Redact, nothing
changes semantically except the real↔mock swap (only serialization noise like field
order) — behavior is indistinguishable from a direct connection.
- **Strictly reversible** : every Mock maps one-to-one to a real value, and Restore is the
exact inverse of Redact — tool calls carrying Secrets are never corrupted.
- **Deterministic Mocks** : a Secret's Mock is stable across turns, preserving the LLM
vendor's prefix-cache hits — token cost is unaffected in normal sessions (the
mock-escaped-and-replayed exception is documented).
- **No collisions in context** : a Mock never coincides with other content in the request,
so responses are never wrongly replaced.
- **Multi-provider routing** : multiple providers (OpenAI / Anthropic / Gemini / Ollama /
Responses) with per-request model-wildcard routing to different upstreams.
- **Plan-pool failover (Pool)** : chain multiple coding-plan subscriptions (each with its
own credential) behind one endpoint; when one plan hits its usage-window limit, traffic
fails over to the next one automatically and switches back once the window resets —
zero manual intervention.
- **On-demand verbose logging** : by default the gateway stays memory-lean (the session
timeline keeps working); flip one toggle in the WebUI and subsequent requests capture
full request/response bodies for troubleshooting.

A source-level comparison with LiteLLM / LLM Guard / PasteGuard — seven
secret-protection dimensions, including where the alternatives are stronger:
[docs/comparison.md](https://github.com/luochen1990/secret-guard/blob/master/docs/comparison.md).

| Protocol | Forwarding | Secret Redact | Cross-protocol translation | 
|---|---|---|---|
| OpenAI (Chat Completions) | ✅ | ✅ | ✅ ⇄ Anthropic (incl. streaming) / ⇄ Responses (incl. streaming) | 
| Anthropic (claude) | ✅ | ✅ | ✅ ⇄ OpenAI (incl. streaming) / ⇄ Responses (incl. streaming) | 
| OpenAI Responses | ✅ | ✅ (incl. streaming) | ✅ ⇄ Chat Completions / Anthropic (incl. streaming) | 
| Gemini / Ollama | ✅ byte pass-through | 🚧 [Roadmap](#roadmap--contributing) | 🚧 [Roadmap](#roadmap--contributing) | 

- Gemini / Ollama are transparent byte pass-through (forwarding itself fully works);
the codec does not cover them yet, so Redact does not apply — with secrets configured,
requests on those protocols are **rejected by default** (503,`[redact] on_unsupported_protocol = "fail_closed"` ; the error message carries a hint).
They can still be created via hand-written config / the REST API (fine for secret-free
pure forwarding); the WebUI creation form does not offer them, but editing an existing
entry or a Detect probe recommendation supports them via "(experimental)" options.
- **Fail-safe degradation** : on Redact's abnormal paths (unparsable upstream responses /
non-2xx), Mocks are passed through by default — real values never land in failure
bodies;`on_fallback_restore = "restore"` opts into restoration. Full semantics of the
three degradation switches:[docs/configuration.md](https://github.com/luochen1990/secret-guard/blob/master/docs/configuration.md) .
- Protocol behavior is pinned by property-based tests and simulated-upstream integration
tests; real-Provider field validation for Anthropic / Gemini / Ollama has not happened
yet (see [Roadmap](#roadmap--contributing) ) — when first adopting those protocols,
verify one real request in the WebUI Records page before going full speed.

The following directions are in progress — issues and PRs welcome at the main repository
[github.com/luochen1990/secret-guard](https://github.com/luochen1990/secret-guard);
the full development guide lives in [AGENTS.md](https://github.com/luochen1990/secret-guard/blob/master/AGENTS.md):

- **Gemini / Ollama codec** : bring Redact and cross-protocol support to these families.
A new protocol only needs a Reader + Writer trait implementation (~200 lines) with no
dispatch changes — see`src/codec/AGENTS.md` for the architecture.
- **Responses streaming fidelity** : close the residual known losses of Responses
streaming translation (hosted tools / refusal parts dropped with WARN, reasoning
delta event-type normalization, multi content part folding) — streaming itself,
including the Redact restore path, now works for every codec-covered pair; tracked
in[docs/known-limitations.md](https://github.com/luochen1990/secret-guard/blob/master/docs/known-limitations.md) .
- **Unknown-secret detection (NER / regex / entropy) as an alerting layer** : flag
suspected undeclared secrets in the WebUI without auto-redacting (zero false-positive
replacements stay guaranteed). See[#257](https://git.lambda.lc/lc-studio/secret-guard/issues/257) .
- **Real-Provider field validation** : integration-test profiles against real upstreams
for the protocol matrix.

You might be looking for:

- Your first Redact-protected request in three minutes → [Quick Start (site)](https://secret-guard.lambda.lc/en/tutorial-quick-start/)
- Full configuration field reference → [docs/configuration.md](https://github.com/luochen1990/secret-guard/blob/master/docs/configuration.md)
- WebUI page-by-page guide → [WebUI Guide (site)](https://secret-guard.lambda.lc/en/manual-webui/)
- NixOS deployment and credential injection → [docs/deployment-nixos.md](https://github.com/luochen1990/secret-guard/blob/master/docs/deployment-nixos.md)
- Architecture and dataflow contracts → [docs/design/](https://github.com/luochen1990/secret-guard/blob/master/docs/design) (contracts.md)
- Source-level comparison with alternatives → [docs/comparison.md](https://github.com/luochen1990/secret-guard/blob/master/docs/comparison.md)
- Development workflow / test strategy / module contracts → [AGENTS.md](https://github.com/luochen1990/secret-guard/blob/master/AGENTS.md)

MIT — see [LICENSE](https://github.com/luochen1990/secret-guard/blob/master/LICENSE).
