E2B
E2B runs code in cloud microVM sandboxes, commonly as the execution layer for AI agents. Sandboxes get their env vars from the code that creates them (envs at sandbox creation or per command), which makes that orchestrator code the natural place for varlock to do its job.
For workloads you trust with the credentials they use, pass resolved values. For agentic workloads, the recommended shape is the broker sandbox: one sandbox runs the credential proxy and holds the real secrets, and agent sandboxes route through it holding only placeholders. The other topologies get a brief mention at the end.
Passing resolved values
Section titled “Passing resolved values”Import varlock/auto-load at the top of the orchestrator. Varlock resolves your values (from plugins, .env.local, etc.), validates them against your schema, and sets up log redaction, with no wrapper command; E2B delivery stays plain envs:
import 'varlock/auto-load';import { ENV } from 'varlock/env';import { Sandbox } from 'e2b';
const sandbox = await Sandbox.create({ envs: { STRIPE_SECRET_KEY: ENV.STRIPE_SECRET_KEY, },});(Running the orchestrator under varlock run -- node orchestrate.ts works too, and is the option when the orchestrator is not a Node program. From a shell, keep any $VAR expansion inside a sh -c '...' wrapper so varlock resolves it first.)
This is the standard E2B posture: sandboxes hold real values, and the values transit E2B’s API. If the sandboxed project is itself a varlock project (it has a .env.schema), you can instead install varlock in the sandbox and use varlock run there, the same way you would in a Docker container: values come in via envs (the schema validates them at load), or the sandbox resolves its own secrets with a plugin and a service-account token as the only bootstrap credential.
Credential proxy: the broker sandbox
Section titled “Credential proxy: the broker sandbox”One long-lived sandbox (the broker) runs varlock proxy start --expose, which serves the built-in WebSocket tunnel on its proxy port (E2B’s public sandbox URLs carry it). Agent sandboxes reach it with varlock proxy run --url, which self-wires their placeholder env and CA certs from the broker over the tunnel. The proxy injects real values into requests at the wire, on verified TLS connections to hosts your schema allows, with every request checked against your @proxy rules and recorded in the audit log. A compromised or prompt-injected agent can exfiltrate nothing but placeholders.
[agent sandbox] [broker sandbox] varlock proxy run --url ── ws ──▶ proxy :8080 + tunnel (loopback proxy + agent) (public getHost URL)Schema setup
Section titled “Schema setup”Mark the secrets your agents use with @proxy(domain=...) and give each one an explicit @placeholder:
# @proxy(domain="api.anthropic.com")# @placeholder=sk-ant-api03-000000000000000000000000ANTHROPIC_API_KEY=
# @proxy(domain="api.stripe.com")# @placeholder=sk_test_00000000000000000000000000STRIPE_SECRET_KEY=An explicit @placeholder is optional (the sandbox pulls whatever the schema produces from the broker), but worth setting when an SDK checks the key format client-side: a realistic-looking placeholder passes that check where a generic vlk_placeholder_… would not.
Egress is permissive by default: proxied requests to hosts without a rule pass through untouched, which is usually fine because agents hold only placeholders. If the broker should refuse anything that does not match a rule, set @proxyConfig={egress="strict"} in the schema header.
You will also need an E2B API key for the orchestrator that creates sandboxes. It can be a varlock-managed secret like any other.
Start the broker
Section titled “Start the broker”import 'varlock/auto-load';import { ENV } from 'varlock/env';import { Sandbox } from 'e2b';
// One data-plane token, shared by the broker and every agent; generate it// yourself (or let the broker mint one and read it back with `varlock proxy// token`). It is the credential to USE the broker over the tunnel, not to read// its secrets.const PROXY_TOKEN = crypto.randomUUID();const VARLOCK = '/home/user/.config/varlock/bin/varlock';
const broker = await Sandbox.create({ timeoutMs: 60 * 60 * 1000 });
await broker.commands.run('curl -sSfL https://varlock.dev/install.sh | sh -s');
// upload the schema (plus any other .env files your project loads);// real values arrive via envs below insteadawait broker.files.write('/home/user/proj/.env.schema', envSchemaContents);
// start the proxy bound off-loopback so the tunnel is reachable; the token is// pinned via VARLOCK_PROXY_TOKEN so we already know it. --persist-ca reuses the// CA across broker restarts, so agents that already trust it keep working.// --allow-reload lets you apply schema edits later without a restart; the// reload channel is only reachable from inside the broker, not by agents.// Schema keys resolve from the process env, so envs carries the bootstrap:// usually just your plugin's secret-zero (shown: a 1Password service account).await broker.commands.run( `cd /home/user/proj && ${VARLOCK} proxy start --expose --port 8080 --cert-dir /home/user/proj/.varlock-ca --persist-ca --allow-reload > /home/user/proxy.log 2>&1`, { background: true, timeoutMs: 0, envs: { VARLOCK_PROXY_TOKEN: PROXY_TOKEN, OP_SERVICE_ACCOUNT_TOKEN: ENV.OP_SERVICE_ACCOUNT_TOKEN, }, },);// ready once the port answers (a bare GET returns 400, which is fine; we only// need the listener up, so no -f).await broker.commands.run("until curl -s -o /dev/null http://127.0.0.1:8080 --proxy ''; do sleep 0.2; done", { timeoutMs: 30_000 });
const brokerHost = broker.getHost(8080); // e.g. 8080-<id>.e2b.appThe envs block carries whatever bootstraps your schema. With secrets resolved from a manager via a plugin (the usual setup), that is one service-account token, the secret zero, and the schema resolves everything else inside the broker. If some values exist only on your side (like the minimal example schema above), enumerate them instead (ANTHROPIC_API_KEY: ENV.ANTHROPIC_API_KEY, …), so they are the orchestrator’s own resolved values passing through. Either way agent sandboxes hold no real secrets at all.
Start agent sandboxes
Section titled “Start agent sandboxes”An agent needs nothing but varlock and proxy run --url. It pulls its placeholder env and CA certs from the broker over the tunnel, so there is no env or cert plumbing to pass:
const agent = await Sandbox.create({ timeoutMs: 60 * 60 * 1000 });await agent.commands.run('curl -sSfL https://varlock.dev/install.sh | sh -s');
// connect to the broker and run the workload through the tunnel. The agent only// ever holds placeholders; the broker injects real values at the wire.await agent.commands.run( `${VARLOCK} proxy run --url wss://${brokerHost} -- your-agent-command`, // the token rides the env rather than the command line, so it stays out of // process listings and E2B's command logs { envs: { VARLOCK_PROXY_TOKEN: PROXY_TOKEN } },);To see what agents are doing, run varlock proxy audit (or proxy status --watch) inside the broker: every request records its host, path, decision, and which keys were injected.
Lock down agent egress
Section titled “Lock down agent egress”So far the proxy governs proxied traffic, but an agent could still make direct connections that bypass the tunnel. Those requests carry placeholders at worst, so no secrets are at stake, but E2B can close the gap entirely with its network egress rules: restrict the agent to the broker’s URL and every request has to go through varlock policy.
Apply the restriction after provisioning, not at Sandbox.create. The agent needs open egress briefly to install varlock (skip that by baking it into your template), and the clamp does not disturb an established tunnel:
await agent.updateNetwork({ allowOut: [brokerHost], denyOut: ['0.0.0.0/0'],});This is enforced by E2B’s infrastructure outside the VM, so nothing the agent does from inside can lift it. Domain-based allowOut rules match by SNI and Host header, per E2B’s docs; IP-literal traffic needs CIDR rules.
Trust model
Section titled “Trust model”Be clear-eyed about what this shape protects against. The broker holds real secrets inside E2B’s cloud, so E2B’s infrastructure is inside your trust boundary, same as it would be for secrets passed to any sandbox. What changes is the blast radius on your side: agents never hold secrets, so a compromised agent sandbox yields placeholders and only whatever requests your rules and egress mode allow. Rotation, policy, and audit live in one place instead of N sandboxes.
Two practical notes:
- No human is attached to the broker, so its policy must run unattended: allow rules,
blockrules,@proxy=omit, and strict egress. To change policy, write the edited schema withfiles.writeand run${VARLOCK} proxy reloadviacommands.run: the proxy validates the edit in its own context before applying, and a broken edit is refused and reported back. Rule changes apply to agent traffic immediately; a newly added key shows up for newly startedproxy runcommands. - The token authenticates the tunnel and, over it, unlocks the placeholder env an agent adopts. Agents hold it deliberately; it is the credential to use the broker, not to read its secrets, which never leave it. Treat it like any shared secret (rotate by restarting the broker with a new one).
- A broker sandbox is a single point of failure for its fleet. Manage its lifetime explicitly (
timeoutMs, or E2B’s pause/resume);proxy run --urlopens a fresh tunnel per connection, so transient blips recover, and--persist-caabove keeps the CA stable across a broker restart. Reserve that flag for brokers: it writes the CA private key to disk, which is only reasonable because that machine already holds your real secrets.
Other topologies
Section titled “Other topologies”For local development, run the proxy on your machine instead: secrets, resolver plugins, biometric unlock, and the interactive request log stay local. Expose it through any tunnel service that carries WebSockets and reuse the same agent-side command:
export VARLOCK_PROXY_TOKEN=$(uuidgen)varlock proxy start --expose --port 8080ngrok http 8080 # or cloudflared, Tailscale funnel, ...# agents: VARLOCK_PROXY_TOKEN=… varlock proxy run --url wss://abc123.ngrok.app -- <command>The data-plane token gates the tunnel, so a public URL is safe, and the tunnel carries TLS end to end between the agent and your proxy, so the tunnel service only ever sees ciphertext. The same pattern reaches a proxy on any infrastructure you run; see the topologies overview.