| name | matchlock |
| description | Run AI agents and arbitrary code in ephemeral micro-VMs with VM-level isolation, network allowlisting, and secret injection. Use when users want to create sandboxes, run code in isolated VMs, manage sandbox lifecycles, use the matchlock CLI, or integrate with the matchlock Go/Python SDK. Triggers on mentions of matchlock, sandbox, micro-VM, VM isolation, network interception, secret injection, or ephemeral environments. |
Matchlock
CLI tool and SDK for running AI agents in ephemeral micro-VMs with VM-level isolation, network allowlisting, and secret injection via MITM proxy.
Repository: github.com/jingkaihe/matchlock
For deeper dives, use the Matchlock repo as the source of truth and reference these locations: Go SDK (pkg/sdk), Python SDK (sdk/python), TypeScript SDK (sdk/typescript), and core source code (cmd/, internal/, pkg/).
Key principle: Secrets never enter the VM. The VM only sees placeholder values; a host-side MITM proxy replaces them in-flight when HTTP requests go to explicitly allowed hosts.
When software inside the VM expects a specific token shape, Matchlock also supports caller-defined placeholders via CLI flags (--secret-placeholder, --secret-file) and SDK builder helpers.
Architecture
┌──────────── Host ────────────┐ ┌──── Micro-VM ────┐
│ Matchlock CLI / SDK │ │ Guest Agent │
│ Policy Engine │──────│ (vsock :5000) │
│ Transparent Proxy + TLS MITM │ │ │
│ VFS Server │──────│ /workspace (FUSE) │
└──────────────────────────────┘ └───────────────────┘
Platforms: Linux (Firecracker/KVM) and macOS (Apple Silicon via Virtualization.framework).
CLI Reference
Run a Command in a New Sandbox
matchlock run --image <image> [flags] -- <command>
| Flag | Short | Default | Description |
|---|
--image | | (required) | Container image |
--allow-host | | | Allowed hosts (repeatable; supports *.example.com wildcards) |
--secret | | | Secret injection: NAME=VALUE@host1,host2 or NAME@host1,host2 |
--secret-placeholder | | | Override the in-VM placeholder value: NAME=PLACEHOLDER |
--secret-file | | | Load JSON secrets map: name -> {value, placeholder, hosts} |
--no-network | | false | Fully offline sandbox |
--network-intercept | | false | Force interception proxy even with empty allow-list |
--detach | -d | false | Run detached in background; prints VM ID, implies --rm=false, incompatible with -t/-i |
-it | | | Interactive TTY mode |
--rm | | true | Remove sandbox after exit (--rm=false to keep alive) |
-p | | | Publish port [LOCAL:]REMOTE |
--cpus | | 2 | Number of CPUs (supports fractional values, e.g. 0.5) |
--memory | | 512 | Memory in MB |
--disk-size | | 2048 | Disk size in MB |
--timeout | | 300 | Timeout in seconds |
-e | | | Environment variable KEY=VALUE |
--env-file | | | Env file path |
-v | | | Volume mount host:guest[:overlay|host_fs|ro] |
--disk | | | Attach raw ext4 disk host_path:guest_mount[:ro] |
--add-host | | | Custom host-to-IP mapping host:ip |
--dns-servers | | 8.8.8.8,8.8.4.4 | DNS servers |
--privileged | | false | Skip in-guest seccomp/cap-drop |
-w | | image WORKDIR | Working directory |
-u | | image USER | Run as user |
--entrypoint | | image ENTRYPOINT | Override entrypoint |
--graceful-shutdown | | 5s | Graceful shutdown duration |
--pull | | false | Always pull image (ignore cache) |
--workspace | | | Guest mount point for VFS |
--hostname | | sandbox ID | Guest hostname |
--mtu | | 1500 | Network MTU |
--address | | 127.0.0.1 | Bind address for published ports |
--cpus accepts finite values greater than 0. Fractional values are implemented as: guest vCPU count = ceil(cpus) (for scheduler/topology), and guest CPU usage is additionally constrained with cgroup cpu.max to approximately the requested fraction (for example, 0.5 => 1 visible vCPU with ~50% of one CPU time budget).
Common CLI Examples
matchlock run --image alpine:latest -- echo "hello from sandbox"
matchlock run --image ubuntu:latest -it -- bash
matchlock run --image python:3.12-alpine --rm=false -- python3 -c "print('ready')"
matchlock run --image nginx:latest -d
matchlock run --image alpine:latest -d -- sh -c "echo started; sleep 300"
matchlock run --image python:3.12-alpine \
--allow-host api.openai.com \
--secret "OPENAI_API_KEY@api.openai.com" \
-- python3 script.py
matchlock run --image ubuntu:24.04 \
--allow-host github.com \
--allow-host api.github.com \
--secret "GH_TOKEN@github.com,api.github.com" \
--secret-placeholder "GH_TOKEN=gho_sandbox_placeholder" \
-- sh -lc 'printf "%s\n" "$GH_TOKEN"'
matchlock run --image node:22-alpine \
-v /home/user/project:/workspace:host_fs \
-w /workspace \
-- npm test
matchlock run --image nginx:latest -p 8080:80 --rm=false -- nginx -g "daemon off;"
matchlock run --image alpine:latest --no-network -- echo "no network"
matchlock run --image python:3.12-alpine --cpus 4 --memory 2048 --disk-size 10240 --timeout 600 -- python3 heavy_task.py
matchlock run --image alpine:latest --cpus 0.5 -- echo "half-cpu request"
Custom Secret Placeholders
Use a custom placeholder when the guest-side tool validates the token format before making a request. A common example is GH_TOKEN, where tools may expect a gho_, ghp_, or github_pat_-shaped value.
--secret-file accepts JSON in this format:
{
"GH_TOKEN": {
"value": "gho_xxxxxxxxx_therealone",
"placeholder": "gho_sandbox_placeholder",
"hosts": ["github.com", "api.github.com"]
}
}
Placeholder values must not overlap with each other, or with Matchlock's generated placeholder format (SANDBOX_SECRET_<hex>), otherwise Matchlock rejects the config.
Exec into a Running Sandbox
matchlock exec <vm-id> -- <command>
matchlock exec <vm-id> -it -- bash
matchlock exec <vm-id> -w /workspace -- ls -la
Lifecycle Management
matchlock list
matchlock list --running
matchlock get <vm-id>
matchlock inspect <vm-id>
matchlock kill <vm-id>
matchlock kill --all
matchlock rm <vm-id>
matchlock rm --stopped
matchlock prune
Runtime Network Policy
matchlock allow-list add <vm-id> host1,host2
matchlock allow-list delete <vm-id> host1
Port Forwarding
matchlock port-forward <vm-id> 8080:8080
matchlock port-forward <vm-id> --address 0.0.0.0 8080:8080
Image Management
matchlock build -f Dockerfile -t myapp:latest .
matchlock build alpine:latest
matchlock pull alpine:latest
matchlock pull --force alpine:latest
matchlock image ls
matchlock image rm myapp:latest
docker save img | matchlock image import img
matchlock image gc
Volume Management
matchlock volume create mydata
matchlock volume create mydata --size 5120
matchlock volume ls
matchlock volume rm mydata
matchlock volume cp mydata mydata-backup
Resource Cleanup
matchlock gc
matchlock gc <vm-id>
matchlock gc --force-running
Host Diagnostics
matchlock diagnose --json
matchlock diagnose runs host preflight checks before you try to launch sandboxes.
Examples include host virtualization support, required artifacts, and key runtime dependencies.
Setup (Linux, requires root)
sudo matchlock setup linux --user $USER
Python SDK
Install: pip install matchlock
Reference examples are in the references/python/ directory.
Sandbox Builder
from matchlock import Client, Sandbox
sandbox = (
Sandbox("python:3.12-alpine")
.with_cpus(0.5)
.with_memory(1024)
.with_disk_size(5120)
.with_timeout(300)
.with_workspace("/workspace")
.with_privileged()
.allow_host("api.openai.com", "*.npmjs.org")
.add_host("api.internal", "10.0.0.10")
.add_secret("API_KEY", os.environ["API_KEY"], "api.openai.com")
.add_secret_with_placeholder(
"GH_TOKEN",
os.environ["GH_TOKEN"],
"gho_sandbox_placeholder",
"github.com",
"api.github.com",
)
.block_private_ips()
.with_dns_servers("1.1.1.1")
.with_network_mtu(1200)
.with_no_network()
.with_env("FOO", "bar")
.mount_host_dir("/workspace/src", "/home/user/src")
.mount_host_dir_readonly("/workspace/cfg", "/etc/cfg")
.mount_memory("/workspace/tmp")
.mount_overlay("/workspace/data", "/home/user/data")
.with_user("1000:1000")
.with_entrypoint("python3")
.with_port_forward(8080, 8080)
)
Client Lifecycle
from matchlock import Client, Config, Sandbox
config = Config(binary_path="matchlock")
with Client(config) as client:
vm_id = client.launch(sandbox)
result = client.exec("echo hello")
result = client.exec_stream("long-running-cmd", stdout=sys.stdout, stderr=sys.stderr)
result = client.exec_pipe("cat", stdin=io.BytesIO(b"input\n"), stdout=out_buf, stderr=err_buf)
result = client.exec_interactive("sh", stdin=stdin_reader, stdout=sys.stdout, rows=24, cols=80)
client.write_file("/workspace/hello.txt", "hello")
client.write_file("/workspace/script.sh", "#!/bin/sh\necho hi", mode=0o755)
data = client.read_file("/workspace/hello.txt")
files = client.list_files("/workspace")
client.allow_list_add("api.openai.com", "api.anthropic.com")
client.allow_list_delete("api.openai.com")
client.port_forward(8080, 8080)
client.remove()
Network Interception (Python)
Callback hooks run on the host side — secrets never enter the VM:
from matchlock import (
Client, Sandbox,
NetworkHookRule, NetworkInterceptionConfig,
NetworkHookRequest, NetworkHookResult, NetworkHookRequestMutation,
)
api_key = os.environ["ANTHROPIC_API_KEY"]
def before_hook(req: NetworkHookRequest) -> NetworkHookResult:
headers = {k: list(v) for k, v in (req.request_headers or {}).items()}
headers["X-Api-Key"] = [api_key]
return NetworkHookResult(
action="mutate",
request=NetworkHookRequestMutation(headers=headers),
)
sandbox = (
Sandbox("python:3.12-alpine")
.allow_host("api.anthropic.com")
.with_network_interception(
NetworkInterceptionConfig(rules=[
NetworkHookRule(
name="inject-api-key",
phase="before",
hosts=["api.anthropic.com"],
hook=before_hook,
)
])
)
)
VFS Interception (Python)
Block, mutate, or audit file operations inside the sandbox:
from matchlock import (
VFS_HOOK_ACTION_BLOCK, VFS_HOOK_OP_CREATE, VFS_HOOK_OP_WRITE,
VFS_HOOK_PHASE_BEFORE, VFS_HOOK_PHASE_AFTER,
Sandbox, VFSHookRule, VFSInterceptionConfig,
VFSHookEvent, VFSMutateRequest, VFSActionRequest,
)
def mutate_write(req: VFSMutateRequest) -> bytes:
return b"mutated-by-hook"
def after_write(event: VFSHookEvent) -> None:
print(f"wrote {event.path} ({event.size} bytes)")
sandbox = (
Sandbox("alpine:latest")
.with_workspace("/workspace")
.mount_memory("/workspace")
.with_vfs_interception(VFSInterceptionConfig(rules=[
VFSHookRule(
phase=VFS_HOOK_PHASE_BEFORE, ops=[VFS_HOOK_OP_CREATE],
path="/workspace/blocked.txt", action=VFS_HOOK_ACTION_BLOCK,
),
VFSHookRule(
phase=VFS_HOOK_PHASE_BEFORE, ops=[VFS_HOOK_OP_WRITE],
path="/workspace/mutated.txt", mutate_hook=mutate_write,
),
VFSHookRule(
phase=VFS_HOOK_PHASE_AFTER, ops=[VFS_HOOK_OP_WRITE],
path="/workspace/*", hook=after_write, timeout_ms=2000,
),
]))
)
Go SDK
Import: go get github.com/jingkaihe/matchlock/pkg/sdk
Reference examples are in the references/go/ directory.
Sandbox Builder
sandbox := sdk.New("python:3.12-alpine").
WithCPUs(0.5).
WithMemory(1024).
WithDiskSize(5120).
WithTimeout(300).
WithWorkspace("/workspace").
WithPrivileged().
AllowHost("api.openai.com", "*.npmjs.org").
AddHost("api.internal", "10.0.0.10").
AddSecret("API_KEY", os.Getenv("API_KEY"), "api.openai.com").
AddSecretWithPlaceholder("GH_TOKEN", os.Getenv("GH_TOKEN"), "gho_sandbox_placeholder", "github.com", "api.github.com").
BlockPrivateIPs().
WithDNSServers("1.1.1.1").
WithNetworkMTU(1200).
WithNoNetwork().
WithEnv("FOO", "bar").
MountHostDir("/workspace/src", "/home/user/src").
MountHostDirReadonly("/workspace/cfg", "/etc/cfg").
MountMemory("/workspace/tmp").
MountOverlay("/workspace/data", "/home/user/data").
WithUser("1000:1000").
WithEntrypoint("python3").
WithPortForward(8080, 8080)
Client Lifecycle
cfg := sdk.DefaultConfig()
client, err := sdk.NewClient(cfg)
defer client.Remove()
defer client.Close(0)
vmID, err := client.Launch(sandbox)
result, err := client.Exec(ctx, "echo hello")
streamResult, err := client.ExecStream(ctx, "long-running-cmd", os.Stdout, os.Stderr)
pipeResult, err := client.ExecPipe(ctx, "cat", stdinReader, stdoutWriter, stderrWriter)
ttyResult, err := client.ExecInteractive(ctx, "sh", &sdk.ExecInteractiveOptions{
WorkingDir: "/workspace", Rows: 24, Cols: 80,
Stdin: os.Stdin, Stdout: os.Stdout, Resize: resizeCh,
})
client.WriteFile(ctx, "/workspace/hello.txt", []byte("hello"))
client.WriteFileMode(ctx, "/workspace/script.sh", []byte("#!/bin/sh\necho hi"), 0755)
data, err := client.ReadFile(ctx, "/workspace/hello.txt")
files, err := client.ListFiles(ctx, "/workspace")
client.AllowListAdd(ctx, "api.openai.com", "api.anthropic.com")
client.AllowListDelete(ctx, "api.openai.com")
client.PortForward(ctx, 8080, 8080)
Network Interception (Go)
sandbox := sdk.New("python:3.12-alpine").
AllowHost("api.anthropic.com").
WithNetworkInterception(&sdk.NetworkInterceptionConfig{
Rules: []sdk.NetworkHookRule{
{
Name: "inject-api-key",
Phase: sdk.NetworkHookPhaseBefore,
Hosts: []string{"api.anthropic.com"},
Hook: func(_ context.Context, req sdk.NetworkHookRequest) (*sdk.NetworkHookResult, error) {
headers := maps.Clone(req.RequestHeaders)
headers["X-Api-Key"] = []string{apiKey}
return &sdk.NetworkHookResult{
Action: sdk.NetworkHookActionMutate,
Request: &sdk.NetworkHookRequestMutation{Headers: headers},
}, nil
},
},
},
})
VFS Interception (Go)
sandbox := sdk.New("alpine:latest").
WithWorkspace("/workspace").
MountMemory("/workspace").
WithVFSInterception(&sdk.VFSInterceptionConfig{
Rules: []sdk.VFSHookRule{
{
Phase: sdk.VFSHookPhaseBefore,
Ops: []sdk.VFSHookOp{sdk.VFSHookOpCreate},
Path: "/workspace/blocked.txt",
Action: sdk.VFSHookActionBlock,
},
{
Phase: sdk.VFSHookPhaseBefore,
Ops: []sdk.VFSHookOp{sdk.VFSHookOpWrite},
Path: "/workspace/mutated.txt",
MutateHook: func(ctx context.Context, req sdk.VFSMutateRequest) ([]byte, error) {
return []byte("mutated-by-hook"), nil
},
},
{
Phase: sdk.VFSHookPhaseAfter,
Ops: []sdk.VFSHookOp{sdk.VFSHookOpWrite},
Path: "/workspace/*",
TimeoutMS: 2000,
Hook: func(ctx context.Context, event sdk.VFSHookEvent) error {
fmt.Printf("wrote %s (%d bytes)\n", event.Path, event.Size)
return nil
},
},
},
})
TypeScript SDK
Install: npm install matchlock-sdk
Reference examples are in the references/typescript/ directory.
Sandbox Builder
import { Client, Sandbox } from "matchlock-sdk";
const sandbox = new Sandbox("node:22-alpine")
.withCPUs(0.5)
.withMemory(1024)
.withDiskSize(5120)
.withTimeout(300)
.withWorkspace("/workspace")
.withPrivileged()
.allowHost("registry.npmjs.org", "*.npmjs.org", "api.anthropic.com")
.addHost("api.internal", "10.0.0.10")
.addSecret("API_KEY", process.env.API_KEY ?? "", "api.anthropic.com")
.addSecretWithPlaceholder(
"GH_TOKEN",
process.env.GH_TOKEN ?? "",
"gho_sandbox_placeholder",
"github.com",
"api.github.com",
)
.blockPrivateIPs()
.withDNSServers("1.1.1.1")
.withNetworkMTU(1200)
.withNoNetwork()
.withEnv("FOO", "bar")
.mountHostDir("/workspace/src", "/home/user/src")
.mountHostDirReadonly("/workspace/cfg", "/etc/cfg")
.mountMemory("/workspace/tmp")
.mountOverlay("/workspace/data", "/home/user/data")
.withUser("1000:1000")
.withEntrypoint("node")
.withPortForward(8080, 8080);
Client Lifecycle
const client = new Client();
try {
const vmId = await client.launch(sandbox);
const result = await client.exec("echo hello");
const stream = await client.execStream("long-running-cmd", {
stdout: process.stdout,
stderr: process.stderr,
});
const pipe = await client.execPipe("cat", {
stdin: [Buffer.from("input\n")],
stdout: (chunk) => process.stdout.write(chunk),
stderr: (chunk) => process.stderr.write(chunk),
});
const tty = await client.execInteractive("sh", {
stdin: process.stdin,
stdout: process.stdout,
rows: 24,
cols: 80,
});
await client.writeFile("/workspace/hello.txt", "hello");
const data = await client.readFile("/workspace/hello.txt");
const files = await client.listFiles("/workspace");
await client.allowListAdd("api.openai.com", "api.anthropic.com");
await client.allowListDelete("api.openai.com");
await client.portForward(8080, 8080);
} finally {
await client.close();
await client.remove();
}
Network Interception (TypeScript)
import { Client, type NetworkHookRequest, type NetworkHookResult, Sandbox } from "matchlock-sdk";
const apiKey = process.env.ANTHROPIC_API_KEY ?? "";
const sandbox = new Sandbox("python:3.12-alpine")
.allowHost("api.anthropic.com")
.withNetworkInterception({
rules: [
{
name: "inject-api-key",
phase: "before",
hosts: ["api.anthropic.com"],
hook: async (req: NetworkHookRequest): Promise<NetworkHookResult> => {
const headers = { ...(req.requestHeaders ?? {}) };
headers["X-Api-Key"] = [apiKey];
return { action: "mutate", request: { headers } };
},
},
],
});
JSON-RPC Wire Protocol
All SDKs communicate via matchlock rpc (JSON-RPC over stdin/stdout):
| Method | Description |
|---|
create | Create a sandbox VM |
exec | Buffered command execution |
exec_stream | Streaming stdout/stderr |
exec_pipe | Bidirectional stdin/stdout/stderr (no PTY) |
exec_tty | Interactive PTY |
write_file | Write file into sandbox |
read_file | Read file from sandbox |
list_files | List directory |
allow_list_add | Add hosts to allowlist at runtime |
allow_list_delete | Remove hosts from allowlist |
port_forward | Forward host port → guest port |
cancel | Abort in-flight execution |
close | Shut down the sandbox VM |
Environment Variables
| Variable | Description |
|---|
MATCHLOCK_BIN | Path to matchlock binary (used by SDKs) |
MATCHLOCK_RUN_IMAGE | Default image for matchlock run |
References
Complete working examples are in the references/ directory. Each subdirectory has a README with full setup instructions.
SDK Examples
| Directory | Language | Covers |
|---|
references/python/ | Python | basic, exec_modes, network_interception, port_forward, vfs_hooks |
references/go/ | Go | basic, exec_modes, network_interception, vfs_hooks |
references/typescript/ | TypeScript | basic, exec_modes, network_interception |
AI Agent Examples
| Directory | Description |
|---|
references/claude-code/ | Claude Code in a micro-VM with GitHub repo bootstrap and secret injection |
references/claude-code-with-docker/ | Claude Code + Docker daemon in a privileged sandbox (Python SDK) |
references/codex/ | OpenAI Codex in a micro-VM with GitHub repo bootstrap and secret injection |
Infrastructure Examples
| Directory | Description |
|---|
references/docker-in-sandbox/ | Full Docker daemon in a privileged micro-VM (systemd + vfs driver) |
references/agent-client-protocol/ | Streamlit chatbot running a Kodelet agent over ACP via stdin/stdout |