Skip to content

Sandboxing

The credential proxy stops an agent from trivially reading secrets it needs to use. On its own it still runs as you on the same machine, so it raises the bar rather than drawing a hard boundary. Pair it with an OS sandbox and you get both: the sandbox contains the agent; varlock holds the real credentials and injects them only on verified upstream connections.

Each recipe wires the sandbox to the proxy in one of two ways. Most source the proxy’s environment after it starts (eval "$(varlock proxy env)"), so the default random port and temp CA dir are fine. A few tools need the proxy address or CA path in a config file before the proxy starts; for those, pin them up front with --port / --cert-dir. Each recipe below uses whichever fits.

On its own the proxy’s in-tree guards (placeholder isolation, the schema-fingerprint check) raise the bar but are not a hard boundary: a determined agent can spawn a process outside the proxy’s view (for example reparented via setsid) and reach a secret’s source directly. Wrapping the agent in an OS sandbox closes this. It removes the capability to reach a resolution source or egress off-loopback, so evading process detection buys nothing.

LayerJob
SandboxIsolate filesystem, processes, and (where configured) network from the rest of your machine
Credential proxy (varlock)Give the agent placeholders; swap in real secrets only toward allowlisted hosts

A sandbox without a credential broker often still puts real keys in the guest (env files, keychain pinholes, mounted config). A proxy without a sandbox leaves a determined agent on your uid with paths around the proxy. Use both.

Start here when you want isolation without installing a third-party sandbox. Add --sandbox to a self-owned proxy run (it does not apply when attaching to an existing proxy start session).

Terminal window
varlock proxy run --sandbox -- claude

Runs the child inside a minimal, built-in credential + egress jail (macOS sandbox-exec, no install). The jail keeps the agent, varlock, and integrations working while denying exactly the escape routes:

  • Egress: only loopback is reachable, so the proxy on 127.0.0.1 becomes the child’s sole path to the network. A process that scrubs its HTTPS_PROXY / HTTP_PROXY env to bypass the proxy simply cannot connect.
  • Credential material: reads of the user varlock dir (the encryption key, plugin/credential cache, and the proxy session/reload channel) are denied, and so are unix-socket connections into it. The daemon that decrypts local secrets listens on a socket under that dir; a file-read deny does not gate a socket connection, so that connection is blocked separately, otherwise a child could reach the warm daemon and have it decrypt for them.
  • The macOS keychain: mach-lookup to the keychain daemons (securityd / SecurityServer) is denied, so the agent can’t read keychain secrets directly (via security or SecItemCopyMatching). TLS trust evaluation (trustd) is left allowed, so HTTPS still works.
  • Warm credential agents: mach-lookup to the 1Password helper (and similar) is denied, so the agent cannot drive a warm op session directly.

It’s opt-in by design (a jail that’s too tight silently breaks an agent, so we don’t force it on). Bare --sandbox is macOS-only today; on Linux (or for stronger isolation) use --sandbox=docker below.

Terminal window
varlock proxy run --sandbox=docker --sandbox-image <image> -- claude

Runs the child in a container (docker, or --sandbox=podman) whose only network egress is the proxy, while your secrets and the resolution machinery stay on the host. varlock wires this up for you:

  • The agent container runs on an --internal network: it has no route to the internet at all, so raw egress fails closed.
  • A tiny forwarder container (a socat byte-relay holding no secrets) sits on that network as varlock-proxy and bridges to the host proxy. The agent’s HTTPS_PROXY points at it, so every request goes host proxy → policy check → wire injection.
  • Your cwd is mounted as the working directory and the proxy’s public CA is mounted read-only so the container trusts the MITM.

Secrets never enter any container: the host proxy resolves them (keeping your encryption key, keychain/enclave, and warm op session on the host) and injects the real value onto the wire; the container only ever holds placeholders. Both networks and the forwarder are torn down when the command exits.

--sandbox-image is required. It must be an image that contains your command (for example a devcontainer image with claude installed); varlock can’t know your toolchain. The first run pulls a small socat image for the forwarder.

The sections below cover open-source, easy-to-install sandboxes that do not ship their own credential broker, so varlock stays the wire injector. Prefer built-in --sandbox when it fits; use these when you already run that tool, need Windows, or want a different isolation model.

SandboxIsolationPlatforms
MinimalTask / microVM or process sandboxmacOS, Linux
FenceSeatbelt / bubblewrap + domain allowlistmacOS, Linux
yoloboxContainer (home dir stays on the host)macOS, Linux
Agent SafehouseDeny-first Seatbelt profilesmacOS
bubblewrapLinux namespaces (DIY)Linux
MXCWindows AppContainer (processcontainer)Windows 11

Tools that already broker credentials at the network boundary (Docker Sandboxes, microsandbox, Anthropic srt credential mask, and similar) are out of scope here: use those on their own, or wait for varlock host bridging if you want varlock as the broker inside a microVM that cannot reach host loopback.

  1. Varlock installed with a working .env.schema whose secrets resolve (plugin, encrypted local values, or similar).
  2. Familiarity with the credential proxy quick start: mark items with @proxy(domain=...).
  3. The sandbox CLI for the section you follow (install commands are inline below).
.env.schema
# @proxyConfig={egress="strict"}
# ---
# @sensitive
# @proxy(domain="api.anthropic.com")
# @placeholder=sk-ant-api03-000000000000000000000000
ANTHROPIC_API_KEY=yourPreferredPlugin()

Use @placeholder when the agent or SDK checks key shape at startup. See Placeholders and the Claude Code note on the proxy guide. Wire Claude through ANTHROPIC_API_KEY, not a subscription OAuth token.

For recipes that pass env into an external sandbox (rather than proxy run --sandbox), start a durable proxy session on the host first:

Terminal window
varlock proxy start

By default this binds a random loopback port and writes the CA cert to a temp dir; recipes that source eval "$(varlock proxy env)" after startup pick both up automatically. When a tool’s config file has to name the proxy address or CA path before the proxy starts (Fence’s upstreamProxy, MXC’s network.proxy, or a container that needs the CA at a mountable path), pin them instead:

Terminal window
varlock proxy start --port 54321 --cert-dir ./.varlock-proxy

--port fails closed if the port is busy, and --cert-dir is created if missing (only the cert files are removed on stop). Details: Pinning the port and CA location.

Minimal runs tasks in an isolated sandbox (task sandbox). Start varlock on the host, pass the proxy/CA/placeholder environment into the Minimal task, then run the agent inside Minimal so outbound HTTPS goes through varlock.

Terminal window
eval "$(varlock proxy env)"
minimal run claude # or: minimal run <your-agent-task>

varlock proxy env prints the child view for the active session: placeholder values for proxied secrets, plus HTTP_PROXY / HTTPS_PROXY / ALL_PROXY, NO_PROXY, and CA trust vars (NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, REQUESTS_CA_BUNDLE, and related). Sourcing it before minimal run forwards that environment when Minimal inherits the launching shell’s env.

Confirm the agent sees placeholders, not real secrets:

Terminal window
# inside the Minimal task / agent shell, after env was passed through
printenv ANTHROPIC_API_KEY
# expect something like sk-ant-api03-0000… (your @placeholder), not the vault value

Minimal’s isolation mode affects whether the guest can reach the host proxy:

  • Linux process sandboxing that still shares host loopback can use HTTP_PROXY=http://127.0.0.1:<port> as emitted by proxy env.
  • microVM isolation typically cannot reach the host’s 127.0.0.1. Varlock’s proxy currently binds loopback only; non-loopback “sandbox bridging” is not available yet. If minimal run cannot connect through the proxy on your platform, that is the gap to watch, not a misconfigured schema.

If HTTPS clients inside the sandbox fail TLS handshake, confirm the CA bundle vars from proxy env were actually present in the task environment (not only on the host shell that launched Minimal).

Minimal’s claude-code package can wire host paths such as ~/.claude into the task sandbox so Claude can keep state and find an API key on disk. That pinhole is useful for agent state, but it undercuts credential isolation if the real key lives there.

For secrets you mark with @proxy, prefer the placeholder + proxy path above, and avoid relying on a real key file inside the sandbox for those same credentials.

Fence wraps agents in Seatbelt (macOS) or bubblewrap (Linux), with filesystem rules, command denies, and a domain allowlist proxy. It does not inject credentials; that stays varlock’s job.

Terminal window
brew tap fencesandbox/tap
brew install fencesandbox/tap/fence

On Linux, also install bubblewrap and socat (for example sudo apt install bubblewrap socat).

Fence’s own proxy enforces domain policy. Important detail from Fence’s upstream proxy docs: traffic that matches allowedDomains goes direct to the origin and bypasses upstreamProxy. Hosts that need varlock injection must therefore sit in the grey zone (not listed in allowedDomains), with defaultAction: "proxy" and upstreamProxy pointed at varlock.

  1. Start varlock on a fixed port, so fence.json can name the proxy address without per-session editing:
Terminal window
varlock proxy start --port 54321
  1. Project fence.json (extends the code template; keep package registries on the allowlist so they stay direct; leave model API hosts for varlock):
fence.json
{
"$schema": "https://raw.githubusercontent.com/fencesandbox/fence/main/docs/schema/fence.schema.json",
"extends": "code",
"network": {
"allowedDomains": [
"github.com",
"*.npmjs.org",
"registry.yarnpkg.com",
"registry.npmjs.org"
],
"deniedDomains": ["169.254.169.254"],
"defaultAction": "proxy",
"upstreamProxy": "http://127.0.0.1:54321"
}
}

--port keeps that address stable across sessions, so fence.json can be committed as-is (match upstreamProxy to the port you chose). Only http:// upstream URLs are supported by Fence today.

  1. Export placeholders + CA trust, then launch:
Terminal window
eval "$(varlock proxy env)"
fence --settings ./fence.json -- claude

The agent talks to Fence’s local proxy first. Grey-zone hosts (including api.anthropic.com when it is not in allowedDomains) forward to varlock, which injects secrets under your @proxy rules. Keep @proxyConfig={egress="strict"} so unmatched grey-zone destinations die at varlock instead of leaking outbound.

yolobox runs the agent inside a container with your project mounted at its real path and your home directory left on the host. It has optional credential forwarding (real env into the box); skip that and use varlock placeholders instead.

Terminal window
brew install finbarr/tap/yolobox
# or: curl -fsSL https://raw.githubusercontent.com/finbarr/yolobox/master/install.sh | bash
Terminal window
eval "$(varlock proxy env)"
yolobox claude

By default yolobox can pass host env into the container. That is what you want for HTTP_PROXY, CA paths, and placeholders. Do not use yolobox flags that forward real API keys or mount host secret dirs for credentials you already marked @proxy.

For a tighter box, still exclude project secret files you do not want the agent to read:

Terminal window
eval "$(varlock proxy env)"
yolobox claude --exclude ".env*" --exclude "secrets/**"

Agent Safehouse is a macOS Seatbelt wrapper with deny-first, composable profiles for coding agents. It hardens filesystem access; it does not broker credentials.

Terminal window
brew install eugene1g/safehouse/agent-safehouse

Seatbelt shares the host network, so host loopback from proxy env works:

Terminal window
eval "$(varlock proxy env)"
safehouse claude --dangerously-skip-permissions

Or a shell alias that always sources the proxy session first:

~/.zshrc
safe-claude() {
eval "$(varlock proxy env)"
safehouse claude --dangerously-skip-permissions "$@"
}

Tune profiles with --add-dirs-ro, --append-profile, and the Safehouse docs so the agent can only read what it needs; keep real secrets out of those paths when they are proxied via varlock.

bubblewrap is the Linux namespace primitive behind Fence and many other sandboxes. Use it directly when you want a minimal DIY jail without Fence’s policy layer.

Terminal window
# Debian / Ubuntu
sudo apt install bubblewrap
# Fedora
sudo dnf install bubblewrap

For a higher-level wrapper around per-project bubblewrap configs, see sandbox-run.

Share the host network so the jail can reach varlock on loopback, bind your project read-write, and keep the rest of $HOME out:

Terminal window
eval "$(varlock proxy env)"
bwrap \
--die-with-parent \
--unshare-pid \
--share-net \
--ro-bind /usr /usr \
--ro-bind /bin /bin \
--ro-bind /lib /lib \
--ro-bind /lib64 /lib64 \
--ro-bind /etc /etc \
--bind "$PWD" "$PWD" \
--chdir "$PWD" \
--dev /dev \
--proc /proc \
--tmpfs /tmp \
-- claude

Adjust binds for your distro layout and tools (node, agent binaries under /home, etc.). Prefer Fence if you want packaged agent templates and domain allowlisting without maintaining a long bwrap line.

Microsoft eXecution Containers (MXC) is an open-source (MIT), policy-driven sandbox SDK. On Windows 11 24H2+ its default backend is processcontainer (AppContainer-style process isolation). It does not broker credentials; it can force outbound HTTP(S) through a localhost proxy, which is the hook for varlock.

Requires Windows 11 24H2+ (build 26100+) for processcontainer, and Node.js 18+.

Terminal window
npm install @microsoft/mxc-sdk
  1. Start the proxy on the host with a fixed port, so mxc-agent.json can name it up front:
Terminal window
varlock proxy start --port 54321
  1. In PowerShell, load placeholders + CA trust from JSON (bash-style eval "$(varlock proxy env)" is for Unix shells; on Windows prefer --format json):
Terminal window
$envMap = varlock proxy env --format json | ConvertFrom-Json
$envMap.PSObject.Properties | ForEach-Object { Set-Item -Path "Env:$($_.Name)" -Value $_.Value }
  1. Point MXC’s process-container network at that port. MXC’s external proxy example routes sandbox traffic to localhost:<port>:
mxc-agent.json
{
"script": "claude",
"timeout": 0,
"processContainer": {
"name": "varlock-agent",
"capabilities": ["internetClient"]
},
"filesystem": {
"readwritePaths": ["C:\\path\\to\\your\\project"]
},
"network": {
"proxy": { "localhost": 54321 }
}
}

Match the port to the --port you chose in step 1, and set readwritePaths to your project. Keep @proxyConfig={egress="strict"} in .env.schema so only declared hosts get secrets. Host allow/block lists are not enforced on Windows yet in MXC; varlock’s egress rules cover injection scope.

  1. Launch with a small Node script (or wxc-exec.exe if you build the native binary from the MXC repo):
run-sandboxed-agent.ts
import { readFileSync } from 'node:fs';
import { spawnSandboxFromConfig } from '@microsoft/mxc-sdk';
const proxyUrl = process.env.HTTPS_PROXY;
if (!proxyUrl) {
throw new Error('Load varlock proxy env first (HTTPS_PROXY missing)');
}
const port = Number(new URL(proxyUrl).port);
const config = JSON.parse(readFileSync('./mxc-agent.json', 'utf8'));
config.network = { proxy: { localhost: port } };
config.script = process.argv.slice(2).join(' ') || config.script;
const child = spawnSandboxFromConfig(config, { usePty: true });
child.on('exit', (code: number | null) => process.exit(code ?? 1));
Terminal window
npx tsx run-sandboxed-agent.ts claude

The child inherits the placeholder + CA environment you set in PowerShell. MXC’s network.proxy steers AppContainer egress at varlock on loopback, which the host process can reach.

For Apple container, microsandbox, or a full VM, run the agent inside it with egress routed through the proxy on a host-reachable address. That is the VM-grade tier and covers the filesystem and process table too.

MicroVM-style sandboxes that cannot reach host loopback (and products that already run their own credential proxy) need varlock reachable on the guest data plane. Until that bridging exists, prefer built-in --sandbox, the process / Seatbelt / AppContainer recipes above, or container tools only when you have confirmed a host-gateway path to the proxy.