The key never enters the command.
Substituting a key into a shell command works everywhere but has a ceiling: for the moment the command runs, the key is a real argv element. The broker removes that ceiling for HTTP APIs. Point the request at a local daemon; the daemon injects the real auth on the outbound call to the provider. The key never touches the command's argv, environment, or output.
Why the broker exists#
The PreToolUse substitution keeps a key out of the conversation, but
at execution time the substituted key is an argument of the shell command — briefly visible to a local ps,
and printable by any command that echoes its own arguments. That is inherent to injecting a secret into a command.
The broker sidesteps it. For an HTTP API you never put the key in the command at all. The agent sends its request to
a loopback daemon; the daemon resolves <tool>:<label> from the OS keychain and attaches the
real credential on the request it makes to the provider. The command the agent runs carries only a local,
loopback-only capability token — worthless off your machine.
Start the broker#
Run stm broker. It ensures the local daemon is up and prints the base URL, the capability-token header, and a ready-to-run curl example:
stm broker openai default
subscribetome broker
base URL : http://127.0.0.1:<port>/proxy/<tool>/<label>/<upstream-path>
auth : header x-stm-token: <broker-token>
curl http://127.0.0.1:<port>/proxy/openai/default/v1/models \
-H "x-stm-token: <broker-token>"
The [tool] and [label] arguments are optional — they only tailor the printed example. The broker can proxy any built-in target once the daemon is running.
The /proxy request model#
Every brokered request has the same shape:
http://127.0.0.1:<port>/proxy/<tool>/<label>/<upstream-path>
| Segment | Meaning |
|---|---|
<tool> | A built-in broker target (e.g. openai). Selects the upstream origin and the auth scheme. |
<label> | Which key under that tool to use (e.g. default). The (tool, label) pair is resolved from the keychain. |
<upstream-path> | The provider's own path, e.g. v1/chat/completions. Forwarded verbatim to the target's origin, with your query string preserved. |
The daemon looks up the target, resolves the credential, drops any client-supplied auth for the injected scheme, attaches the real auth on the outbound request to the target's own origin, and streams back the response with the key scrubbed out. Method, request body, and non-auth headers pass through.
A worked curl example#
Calling OpenAI's chat-completions endpoint through the broker — note there is no Authorization header and no key anywhere in the command:
curl http://127.0.0.1:<port>/proxy/openai/default/v1/chat/completions \
-H "x-stm-token: <broker-token>" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'
The broker resolves openai:default from your keychain and sends
Authorization: Bearer <real-key> on its own request to https://api.openai.com. Your
command — and anything that logs it — sees only the loopback token. In Claude Code you can simply ask the agent to
"call OpenAI chat completions through the stm broker"; the placeholder-free command is safe for the transcript.
The loopback capability token#
The x-stm-token value is deliberately different from the dashboard token:
- It authorizes
/proxyonly. It cannot read the key inventory, open the dashboard, or view metadata. - It exposes no secret. It is a capability to make brokered calls, not a key — so it is safe to appear in a command, a script, or the chat.
- It is loopback-only. The daemon binds to
127.0.0.1; the token is worthless to anyone off your machine. - It resets on restart. A fresh token is generated each time the daemon starts, so it is naturally short-lived.
Security invariants#
The broker enforces these on every request. They are the reason routing a key through a proxy is safe:
| Invariant | What it prevents |
|---|---|
| SSRF / origin guard | The upstream path must be server-relative and resolve to the target's own origin. A protocol-relative //host, an absolute URL, or a \ is rejected, and the constructed origin is re-checked as a backstop — so a crafted path can never redirect the key to another host. |
| No auto-follow redirects | The outbound fetch uses redirect: manual. A 3xx is surfaced to the caller, never followed — so the injected credential can't ride a redirect to an off-origin host. |
| Response scrub | Every occurrence of the real key — literal and URL-encoded — is replaced with [stm:redacted] in the response body and headers, and in any network-error message. An upstream that echoes the credential back cannot leak it into your output. |
| Client auth dropped | Any caller-supplied header for the injected scheme is stripped before the real auth is attached — the caller can neither override nor read the credential. |
| Response size cap | The upstream body is bounded (16 MiB). A hostile or compromised provider can't OOM the daemon with an unbounded response. |
| Never logs a key | The broker never writes a resolved key value to a log. A brokered call is audited by method, path, and status only. |
The broker keeps the key off the command and out of your output. It does not defend a compromised local machine — a process running as your user could talk to the loopback daemon directly. That is the same endpoint-trust boundary described on the security page.
Built-in targets#
Targets are a data-driven registry — each is an origin plus an auth scheme. The launch set:
| Tool | Upstream origin | Auth attached on the outbound request |
|---|---|---|
openai | https://api.openai.com | Authorization: Bearer |
anthropic | https://api.anthropic.com | x-api-key header |
groq | https://api.groq.com | Authorization: Bearer |
openrouter | https://openrouter.ai | Authorization: Bearer |
replicate | https://api.replicate.com | Authorization: Token <key> |
fal | https://fal.run | Authorization: Key <key> |
stripe | https://api.stripe.com | Authorization: Bearer |
The registry is extensible — adding a provider is a single data entry (origin + auth scheme).
Auditing brokered calls#
Every brokered request is recorded as a first-class broker audit event — the method, the path, and the
response status, never the key. Filter for them with:
stm audit --event broker
See the CLI reference for the full audit filter set, and the security page for how the broker fits the overall model.