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.
Passing resolved values
Section titled “Passing resolved values”Import varlock/auto-load at the top of the orchestrator: varlock resolves your values (plugins, .env.local, etc.), validates them against your schema, and redacts them in the orchestrator’s logs.
import 'varlock/auto-load';import { ENV } from 'varlock/env';import { execSync } from 'node:child_process';import { Sandbox } from 'e2b';
// one blob with the resolved env, scoped by --filter to what// this sandbox should seeconst envBlob = execSync( 'varlock load --format json-full --compact --filter "STRIPE_*,SENTRY_DSN"', { encoding: 'utf8' },).trim();
const sandbox = await Sandbox.create({ envs: { // a Node app that imports varlock hydrates process.env, the ENV object, // and log redaction from the blob - no .env files or CLI in the sandbox __VARLOCK_ENV: envBlob, _VARLOCK_USE_INJECTED_ENV: '1', // for workloads that don't import varlock, enumerate plain vars instead: // STRIPE_SECRET_KEY: ENV.STRIPE_SECRET_KEY, },});This is the standard E2B posture: sandboxes hold real values, and the values transit E2B’s API. Consuming the blob needs either the varlock npm package (Node 22+) or the varlock CLI (varlock run -- <command> injects plain env vars from the blob, for any workload). See _VARLOCK_USE_INJECTED_ENV. From a shell, varlock run --inject blob --filter ... -- sh -c '...' exposes the blob as $__VARLOCK_ENV for forwarding.
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 not usable by whoever finds it. Be precise about what the tunnel service itself can see, though, because it terminates the outer TLS: it observes the WebSocket handshake, which carries that token, the CONNECT metadata naming each upstream host, and the placeholder env the agent bootstraps. What it does not see is your real secrets, or the contents of proxied HTTPS requests, which ride an inner TLS session between the agent and your proxy. Plain HTTP has no inner session: it crosses the tunnel in absolute form, so a terminating service reads those requests in full. The proxy fails closed rather than injecting a secret into a cleartext connection, so what is exposed there is traffic carrying no injected secret. Pick a tunnel service you would trust with the token.
The same pattern reaches a proxy on any infrastructure you run; see the topologies overview.