Credential proxy
The credential proxy lets you run an AI agent (or any untrusted component) so that it never sees real secrets. Instead it receives placeholders and real values are swapped in at the network boundary.
This is useful any time a process you don’t fully trust needs to use a secret without being allowed to read it: coding agents, MCP servers, third-party CLIs, or scripts.
This pattern is known as a credential broker: a trusted component holds the real credentials and swaps them in at the network boundary, so a compromised or prompt-injected agent never holds a secret it can leak. It is increasingly recommended for agent security, for example by the SANS Institute and in Anthropic’s managed-agents architecture.
Most other credential brokers require you to store secrets in their tool, or work only with a limited subset of secure storage locations (system keychain, 1Password). Our proxy works with the existing varlock suite - so you can use any of our plugins, and still take advantage of the rest of varlock’s features.
# @sensitive# @proxy(domain="api.stripe.com") @placeholder=sk_test_00000000000000000000000000STRIPE_SECRET_KEY=yourPreferredPlugin() # from a plugin or built-in encryption — never a plaintext secretvarlock proxy run -- claudeThe agent launched above sees STRIPE_SECRET_KEY=sk_test_0000…, a placeholder shaped like a real key. When it makes a request to api.stripe.com, the proxy swaps the placeholder for the real key on the wire. If the agent prints the variable, exfiltrates its env, or sends it anywhere else, all it has is a useless placeholder.
The @placeholder keeps the placeholder valid-looking so the Stripe SDK’s client-side key-format check passes. See Placeholders for when you need it.
How it works
Section titled “How it works”- You mark which secrets are proxied with
@proxy(domain=...). - The child process is launched with placeholders in place of those secrets, plus the standard proxy/CA environment variables pointing at a local proxy.
- The proxy intercepts HTTPS traffic. For a request that matches a rule, it verifies the upstream’s real TLS identity, then substitutes the placeholder for the real secret only on that connection.
- Responses are scrubbed so real values can’t leak back into the child’s output, and every request is recorded to an audit log.
Secrets and the proxy’s signing key live only in memory on your machine; they are never written to disk and never handed to the child.
Quick start
Section titled “Quick start”This assumes you already have varlock installed and a working .env.schema whose secrets resolve (committed encrypted values, a plugin, or env values you can load).
1. Route a secret through the proxy
Section titled “1. Route a secret through the proxy”Mark the item @sensitive and add @proxy(domain=...) to the item you want to protect. The @proxy tells the proxy to inject the item’s real value into requests to that host:
# @sensitive# @proxy(domain="api.stripe.com")# @placeholder=sk_test_00000000000000000000000000STRIPE_SECRET_KEY=yourPreferredPlugin()
# @sensitive# @proxy(domain="api.github.com")GITHUB_TOKEN=yourPreferredPlugin()@proxy already implies @sensitive (and varlock treats items as sensitive by default), so the @sensitive line is optional. We show it explicitly here because being clear about what is a secret is good practice.
The @placeholder on STRIPE_SECRET_KEY makes the placeholder the agent sees look like a real key, so SDK key-format checks pass. Leave it off (like GITHUB_TOKEN above) and the item gets a generic vlk_placeholder_…, which varlock warns about because it can fail a client-side key-format check (see Placeholders).
That is all the proxy needs: there is no separate “enable” step. It runs in permissive mode by default (hosts that don’t match a rule pass through untouched). Add @proxyConfig={egress="strict"} to your schema header only when you want to block everything that isn’t explicitly routed.
2. Check your config (optional)
Section titled “2. Check your config (optional)”varlock proxy rules # prints the effective @proxy rules + per-secret mode, no proxy started3. Run your agent through it
Section titled “3. Run your agent through it”varlock proxy run -- claude# or any command:varlock proxy run -- node agent.jsvarlock proxy run -- python tool.pyThat’s it. The child inherits everything it needs (proxy address + CA trust) automatically.
More in this section
Section titled “More in this section”- Routing rules: match domains, paths, methods; placeholders and egress modes
- Running modes: one-shot, daemon, sessions, auditing, sandboxing
Limitations
Section titled “Limitations”The proxy is a local, same-machine tool. Know what it does and doesn’t cover before relying on it:
- Same host, same user. The proxy runs as you, and so does the agent. It stops the agent from trivially reading a secret it’s using, but on its own it is not a sandbox: it doesn’t isolate the filesystem, other processes, or memory. A determined agent can spawn a process outside the proxy’s view (e.g. reparented via
setsid) and resolve secrets directly from their source; the in-tree guards (placeholder isolation, the schema-fingerprint check) raise the bar but are not a hard boundary. Add--sandbox(or a container/VM; see Sandboxing) for a real isolation boundary. - Response scrubbing is best-effort. Responses are scrubbed back to placeholders only for small, uncompressed text bodies. Compressed (gzip/br) or large (>2 MB) response bodies are passed through unscrubbed: the proxy can’t scan into them. This only matters if an upstream reflects your secret back in its response (uncommon); the primary protection (the agent only ever holds a placeholder) is unaffected.
- Injection requires a verified public-TLS host. Secrets are injected only onto connections whose TLS identity is verified against public CAs. The proxy refuses to inject over cleartext
http://or into a self-signed/local upstream, by design. - Default egress is not containment.
egress=permissive(the default) does not restrict where the agent can connect; it just never injects to unmatched hosts. Useegress=strict(and ideally a sandbox) if you want to constrain egress. - Hot-reload is human-applied.
proxy reload(or pressingrin an interactiveproxy start) re-resolves the schema and swaps the live policy; a reload requested from inside the proxied agent is refused and logged so the agent can’t self-approve a schema edit. Availability follows@proxyConfig={reload=...}(defaultauto: on for an interactive daemon, off for headless or one-shot runs). But the agent-refusal is a self-reported signal, not authentication: an agent that strips its proxy markers (or runs out-of-tree) can still trigger a reload on a shared uid. A real out-of-band approver is planned to close this; until then, prefer a sandbox. - Only proxy-aware clients are covered. The child is pointed at the proxy via standard
HTTP(S)_PROXY/ CA env vars. A client that ignores those (or pins its own CA) bypasses the proxy entirely; it just won’t get a working secret. See client compatibility for the status of common clients. - TLS interception is HTTP/1.1 only. Normal clients negotiate down via ALPN and work fine, but protocols that require HTTP/2 (gRPC) or a connection upgrade (WebSockets) are unsupported on hosts with a
@proxyrule. Hosts without a rule tunnel through untouched.