Credential broker

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:

shell
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>
SegmentMeaning
<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:

shell
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 /proxy only. 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:

InvariantWhat it prevents
SSRF / origin guardThe 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 redirectsThe 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 scrubEvery 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 droppedAny 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 capThe upstream body is bounded (16 MiB). A hostile or compromised provider can't OOM the daemon with an unbounded response.
Never logs a keyThe broker never writes a resolved key value to a log. A brokered call is audited by method, path, and status only.
Same boundary as the rest of stm

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:

ToolUpstream originAuth attached on the outbound request
openaihttps://api.openai.comAuthorization: Bearer
anthropichttps://api.anthropic.comx-api-key header
groqhttps://api.groq.comAuthorization: Bearer
openrouterhttps://openrouter.aiAuthorization: Bearer
replicatehttps://api.replicate.comAuthorization: Token <key>
falhttps://fal.runAuthorization: Key <key>
stripehttps://api.stripe.comAuthorization: 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:

shell
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.