Skip to content

Docker Sandboxes

Docker Sandboxes (the sbx CLI) runs agents in local microVMs. Each sandbox gets its own Docker daemon, filesystem, and network, and all egress is forced through a host-side gateway with a deny-by-default domain allowlist (sbx policy). That gateway is the seam varlock plugs into: point the agent at a varlock broker, allow the broker in policy, and the agent holds only placeholders while varlock injects real secrets at the wire.

The recommended shape for local development is a broker on your own machine: secrets, resolver plugins, biometric unlock, and the interactive request log all stay on the host, and the sandbox reaches the broker through the sbx gateway. To share one broker across a fleet, run it on infrastructure you operate and reach it at a public URL instead.

Three facts about the gateway shape the setup:

  • Everything goes through the gateway. Inside a sandbox, HTTP_PROXY / HTTPS_PROXY point at gateway.docker.internal:3128 and the sbx CA is in the trust store. Raw TCP, UDP, and ICMP are blocked at the network layer, DNS included, so names are resolved by the gateway and a request to a host with no policy rule comes back as a 403 from the gateway rather than a name-resolution failure. varlock proxy run --url honors those proxy env vars and dials its tunnel through the gateway automatically, so no shim is needed.
  • The gateway can reach a host-local service when you allow it in policy. This is what makes a host broker reachable from the microVM.
  • Traffic you route through a broker leaves from the broker’s machine, not the sandbox. Once the agent’s requests ride the varlock tunnel, sbx policy sees only the connection to the broker. Egress control moves to varlock, which is why the schema below sets egress="strict".

Mark the secrets your agent uses with @proxy(domain=...), give each an explicit @placeholder, and set @proxyConfig={egress="strict"} in the header:

.env.schema
# @proxyConfig={egress="strict"}
# ---
# @proxy(domain="api.anthropic.com")
# @placeholder=sk-ant-api03-000000000000000000000000
ANTHROPIC_API_KEY=yourPreferredPlugin()

Values are resolved by the broker on your host, so the right-hand side is whatever your setup already uses: a plugin call as shown, or a value the broker picks up from its own environment or a local .env file. An item left empty with no source fails validation and the broker will not start.

--expose binds off-loopback and serves the tunnel, minting a data-plane token. Pin the token so you can hand the same value to the agent:

Terminal window
export VARLOCK_PROXY_TOKEN=$(uuidgen)
varlock proxy start --expose --port 8080

Bare --expose binds 0.0.0.0, so the broker is reachable from your whole network, not just the sandbox. The data-plane token gates it, but on an untrusted network (a cafe, a conference) firewall the port or stay off that network while the broker runs. Serving the tunnel requires an off-loopback bind, so a loopback-only broker is not an option even though the gateway would reach it.

The gateway rewrites host.docker.internal to localhost before forwarding, and policy is matched against the destination it forwards to. So the rule that matches is for localhost, even though the agent connects to host.docker.internal:

Terminal window
sbx policy allow network "localhost:8080"

Install varlock in the sandbox, then wrap the agent command with proxy run --url. The agent connects to the broker at host.docker.internal, and varlock self-wires its placeholder env and CA certs over the tunnel:

Terminal window
# in a shell sandbox (sbx create --name my-sandbox shell . && sbx exec my-sandbox bash -lc '...'):
npm i -g varlock
VARLOCK_PROXY_TOKEN=$YOUR_TOKEN \
varlock proxy run --url ws://host.docker.internal:8080 -- your-agent-command

That is the whole path: the agent holds placeholders, the broker on your host injects real values only on verified TLS connections to hosts your schema allows, and every request is checked against your @proxy rules and recorded in the audit log.

To share one broker across machines or a fleet, run it on infrastructure you operate and expose it at a URL that carries WebSockets. Because sbx allows direct TLS to allowlisted domains, the agent reaches a public wss:// broker directly (no gateway asymmetry), so you only allow the broker’s domain:

Terminal window
sbx policy allow network "broker.example.com"
# in the sandbox:
VARLOCK_PROXY_TOKEN=$YOUR_TOKEN \
varlock proxy run --url wss://broker.example.com -- your-agent-command

The data-plane token gates the tunnel, and the gateway passes allowlisted TLS through without terminating it, so the tunnel’s TLS runs end to end and an intermediary sees only ciphertext. One caveat: sbx does terminate TLS for hosts it injects its own credentials into, and its CA is already in the sandbox trust store, so if your broker’s domain ever became such a host the tunnel would still connect while no longer being end to end. Keep the broker on a domain sbx has no reason to intercept. See the topologies overview and the E2B / Fly.io guides for the same broker shape on cloud providers.

Docker Sandboxes ships its own credential injection (sbx secret): the gateway substitutes a stored value into request headers for a matching host. It covers the basic case. Route through a varlock broker when you want:

  • Custody in your secret manager. Secrets come from wherever you already keep them through plugins (1Password, Vault, AWS, Doppler, …) and stay in your custody, instead of being copied into another store.
  • One schema. Your .env.schema describes every value, its type, and its routing in one declarative layer, legible to people and agents alike.
  • Response scrubbing. varlock scans response bodies and redacts injected secret values, so an allowlisted endpoint that echoes a request header cannot hand the real secret back to the agent.
  • Policy and audit. Match on path and method, hot-reload the schema, and keep your own audit log.

The two compose: sbx provides microVM isolation, and the varlock broker provides custody, injection, scrubbing, and (in strict mode) the egress boundary.

They also overlap on variable names: a sandbox starts with the env vars for sbx’s built-in services (ANTHROPIC_API_KEY, OPENAI_API_KEY, …) preset to the sentinel proxy-managed, whether or not you have stored a secret for them. varlock’s child env is layered on top, so a key your schema routes through @proxy reaches the agent as varlock’s placeholder and the varlock rule applies; keys your schema does not describe keep the sentinel and stay on the sbx path.

The broker holds real secrets on whatever machine runs it. For a host broker in local development, that is your own machine, the same place the secrets already live. The agent’s microVM never holds them: a compromised or prompt-injected agent yields placeholders and only the requests your rules and egress mode allow.

Note where each boundary applies. sbx policy governs what the sandbox reaches directly, so keep it tight: the broker, plus anything the agent legitimately fetches on its own (package registries, and varlock.dev if you install by script). Everything the agent sends through the broker is governed by varlock instead, which is what egress="strict" is for. Adding a host to sbx policy does not widen or narrow the proxied path, and leaving varlock permissive does not get caught by sbx policy.

Treat the data-plane token like any shared secret (rotate by restarting the broker with a new one), and keep the broker’s port off untrusted networks, since --expose binds all interfaces.