3.5

sbx secret set, sbx secret import, and sbx secret set-custom

A secret never enters the VM: the agent sees a sentinel, the host keychain holds the value, and the proxy swaps it into requests to the bound host.

You exported ANTHROPIC_API_KEY in ~/.zshrc, as a 2026 tutorial said, and the agent inside the sandbox still has no key. Since v0.35.0 the host environment is not read (conflict C7). -e KEY passes a plain variable, not a secret, and the three sbx secret commands are the only way in.

When you finish this section, you can store a service secret, bind a custom one to a host, and read what the proxy sent.

Service secrets

Service secret: a value stored under one of 13 service names, anthropic, copilot, cursor, devin, droid, github, google, groq, mistral, nebius, openai, openrouter, xai, two more than the docs table (conflict C21). The value comes from -t, from stdin, from --command, or from --ref with a 1Password op:// reference or an AWS Secrets Manager ARN. --oauth is the fifth source, and locally it works only for openai at global scope (conflict C22). Anthropic OAuth comes from the agent's own login instead. Since v0.46.0, a command helper runs from a new temporary directory on the host, so a relative helper path no longer resolves (conflict C38). --refresh sets the cache time, default 55 minutes.

The scope is global unless --sandbox narrows it. secret ls filters with -g, --sandbox, --service, and --json, and secret rm takes --all, --placeholder, and --registry. The store is the macOS Keychain, the Windows Credential Manager, or the Linux Secret Service. Without a keyring, Linux uses a file under ~/.config/com.docker.sandboxes at mode 0700. Kit approvals live apart from the values, in ~/.config/sbx/credentials.yaml.

secret import: reads the host environment once, offers each variable with a last-4 preview, and takes --all, --force, and --dry-run. A service that already holds an OAuth token is skipped. On the capture host nothing was exported:

an import with nothing to importtextFull log
$ sbx secret import --dry-run
No credential env vars detected on the host. Set e.g. OPENAI_API_KEY in your shell and re-run, or use `sbx secret set` to enter a value directly.

Registry credentials are a third kind, "host-only by default", and reach a sandbox only with --all-sandboxes or --sandbox.

What the agent sees

Every service variable inside a sandbox is a sentinel, and the GitHub one is shaped like a token:

sentinels in the environment of m101-demotextFull log
ANTHROPIC_API_KEY=proxy-managed
…
GH_TOKEN=gho_sbxproxymanaged000000000000000000000
…
OPENAI_API_KEY=proxy-managed
…
SBX_CRED_ANTHROPIC_MODE=none
SBX_CRED_GITHUB_MODE=none
Every line that is not a sentinel or a credential mode is cut.

Three formats exist in v0.47.0 (conflict C35): proxy-managed for the eight provider keys, gho_sbxproxymanaged000000000000000000000 for GH_TOKEN, and sbx-cs-<rand> for custom secrets. The GitHub one is lower case, where a 2026-09-04 demo showed GHO_SBX_PROXY_MANAGED. No GitHub request was captured: that needs a real token.

Custom secrets and the receiver

set-custom: an experimental secret keyed to --host targets and an --env name instead of a service. The value comes from --value, --command, or --ref, and --placeholder sk-{rand} sets a chosen prefix. The --header and --format flags apply with --cloud only, as the help states for each (conflict C34). The capture bound M101_RECV_KEY to host.docker.internal and localhost, and started a receiver on the host at 127.0.0.1:18080. It allowed localhost:18080 for m101-secret and found M101_RECV_KEY=sbx-cs-<rand> inside (the capture). Then it sent three plain HTTP requests:

what the receiver on the host loggedtextFull log
GET /from-variable HTTP/1.1
Host: localhost:18080
User-Agent: curl/8.18.0
Accept: */*
Authorization: Bearer m101-dummy-receiver-0000
X-Demo: m101-dummy-receiver-0000
Accept-Encoding: gzip
…
GET /no-scheme HTTP/1.1
Host: localhost:18080
…
Authorization: m101-dummy-receiver-0000
The second request, sent with the placeholder typed as a literal, is cut. It matched the first.

The receiver saw the real value in every place the placeholder appeared: inside Bearer, in X-Demo, and as the whole Authorization value. Its Host was localhost:18080: the proxy maps host.docker.internal to localhost. So the swap happens on plain HTTP, and it is a substring replacement. That settles conflict C34 for custom secrets. The Hacker News claim that the secret must be the whole header describes service secrets, where the kit declares the header and its format.

The fourth request went to gateway.docker.internal:18080, a name in NO_PROXY, so it bypassed the forward proxy. The transparent proxy blocked it with Empty reply from server and swapped nothing (the capture). Figure 3.6 animates the four steps. The existing sandbox m101-demo did not receive M101_RECV_KEY (the capture), so a custom variable reaches new sandboxes only. Removal is immediate: sbx secret rm --placeholder sbx-cs-<rand> -f printed Applied secret updates for <n> running sandbox(es) (the capture).

The SSH agent

One credential path does cross the boundary. SSH agent forwarding is on by default, and the guest shows SSH_AUTH_SOCK=/run/ssh-agent.sock with SSH_AUTH_SOCK_GATEWAY=gateway.docker.internal:3129 (the capture). Keys stay on the host, and any process inside can ask that agent to sign. ssh.agentForwardingEnabled in sbx settings list turns it off, followed by sbx daemon restart. MCP secrets stay on the host too, under mcp:<server>:client_secret, as sbx mcp add explains.

Fig. 3.6one custom secret from the keychain to the receiversequence
One custom secret from the keychain to the receiver Five lifelines: the sbx CLI, the secret store, the sandbox m101-secret, the forward proxy on gateway.docker.internal:3128, and the receiver on 127.0.0.1:18080. The CLI stores M101_RECV_KEY for host.docker.internal and localhost and gets the placeholder sbx-cs-<rand>, which the new sandbox carries in its environment. curl inside sends the placeholder as a Bearer token to host.docker.internal:18080. The proxy reads the stored value and forwards the request to localhost:18080 with the real value, and the receiver answers 204. A direct connection to gateway.docker.internal:18080 bypasses the forward proxy and is blocked by the transparent proxy with no swap. Removing the secret updates running sandboxes at once. sbx CLI secret store m101-secret proxy :3128 receiver 1 set-custom --env M101_RECV_KEY hosts host.docker.internal, localhost 2 placeholder sbx-cs-<rand> 3 create m101-secret M101_RECV_KEY=sbx-cs-<rand> inside 4 GET host.docker.internal:18080/from-variable Authorization: Bearer sbx-cs-<rand> 5 value for the bound host 6 m101-dummy-receiver-0000 7 forward, Host: localhost:18080 Bearer m101-dummy-receiver-0000 8 204 9 direct to gateway.docker.internal:18080 NO_PROXY, transparent proxy: blocked, no swap 10 secret rm --placeholder sbx-cs-<rand> Applied secret updates for running sandboxes
The sandbox only ever holds sbx-cs-<rand>, the forward proxy swaps it for the stored value on the bound host, and a direct connection gets no swap. Read top down. Lifelines are the sbx CLI, the secret store, m101-secret, the forward proxy, and the receiver on 127.0.0.1:18080. Solid teal arrows are writes to the store. The dashed rose arrow is the direct connection that the transparent proxy blocks.

Docker Sandboxes (sbx) and Docker Agent (docker-agent) sbx 0.47.0, docker-agent 1.149.0 · bf4169c · 2026-10-07 · Edition 2026.10