matchlock

module
v0.1.29 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 1, 2026 License: MIT

README

Matchlock

Experimental: This project is still in active development and subject to breaking changes.

Matchlock is a CLI tool for running AI agents in ephemeral microVMs - with network allowlisting, secret injection via MITM proxy, and VM-level isolation. Your secrets never enter the VM.

Why Matchlock?

AI agents need to run code, but giving them unrestricted access to your machine is a risk. Matchlock lets you hand an agent a full Linux environment that boots in under a second - isolated and disposable.

When you pass --allow-host or --secret, Matchlock seals the network - only traffic to explicitly allowed hosts gets through, and everything else is blocked. When your agent calls an API the real credentials are injected in-flight by the host. The sandbox only ever sees a placeholder. Even if the agent is tricked into running something malicious your keys don't leak and there's nowhere for data to go. Inside the agent gets a full Linux environment to do whatever it needs. It can install packages and write files and make a mess. Outside your machine doesn't feel a thing. Volume overlay mounts are isolated snapshots that vanish when you're done. Same CLI and same behaviour whether you're on a Linux server or a MacBook.

Quick Start

System Requirements
  • Linux with KVM support
  • macOS on Apple Silicon
Install
brew tap jingkaihe/essentials
brew install matchlock
Usage
# Basic
matchlock run --image alpine:latest cat /etc/os-release
matchlock run --image alpine:latest -it sh
matchlock run --image alpine:latest --no-network -- sh -lc 'echo offline'

# Network allowlist
matchlock run --image python:3.12-alpine \
  --allow-host "api.openai.com" python agent.py

# Keep interception enabled even with an empty allowlist,
# so hosts can be added/removed at runtime.
matchlock run --image alpine:latest --rm=false --network-intercept
matchlock allow-list add <vm-id> api.openai.com,api.anthropic.com
matchlock allow-list delete <vm-id> api.openai.com

# Secret injection (never enters the VM)
export ANTHROPIC_API_KEY=sk-xxx
matchlock run --image python:3.12-alpine \
  --secret ANTHROPIC_API_KEY@api.anthropic.com python call_api.py

# Long-lived sandboxes
matchlock run --image alpine:latest --rm=false   # prints VM ID
matchlock run --image nginx:latest -d             # same as above, detached
matchlock exec vm-abc12345 -it sh                # attach to it
matchlock port-forward vm-abc12345 8080:8080     # forward host:8080 -> guest:8080

# Publish ports at startup
matchlock run --image alpine:latest --rm=false -p 8080:8080

# Lifecycle
matchlock list | kill | rm | prune

# Build from Dockerfile (uses BuildKit-in-VM)
matchlock build -f Dockerfile -t myapp:latest .

# Pre-build rootfs from registry image (caches for faster startup)
matchlock build alpine:latest

# Image management
matchlock image ls                                           # List all images
matchlock image rm myapp:latest                              # Remove a local image
docker save myapp:latest | matchlock image import myapp:latest  # Import from tarball

SDK

Matchlock ships Go, Python, and TypeScript SDKs for embedding sandboxes directly in your application. You can launch VMs, execute commands, stream output, and manage files programmatically.

Go

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/jingkaihe/matchlock/pkg/sdk"
)

func main() {
	ctx := context.Background()

	client, err := sdk.NewClient(sdk.DefaultConfig())
	if err != nil {
		panic(err)
	}
	defer client.Close(0)
	defer client.Remove()

	sandbox := sdk.New("alpine:latest").
		AllowHost("dl-cdn.alpinelinux.org", "api.anthropic.com").
		AddSecret("ANTHROPIC_API_KEY", os.Getenv("ANTHROPIC_API_KEY"), "api.anthropic.com")
	if _, err := client.Launch(sandbox); err != nil {
		panic(err)
	}
	if _, err := client.Exec(ctx, "apk add --no-cache curl"); err != nil {
		panic(err)
	}
	// The VM only ever sees a placeholder - the real key never enters the sandbox
	result, err := client.Exec(ctx, "echo $ANTHROPIC_API_KEY")
	if err != nil {
		panic(err)
	}
	fmt.Print(result.Stdout) // prints "SANDBOX_SECRET_a1b2c3d4..."

	curlCmd := `curl -s --no-buffer https://api.anthropic.com/v1/messages \
  -H "content-type: application/json" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -d '{"model":"claude-haiku-4-5-20251001","max_tokens":1024,"stream":true,
       "messages":[{"role":"user","content":"Explain TCP to me"}]}'`
	if _, err := client.ExecStream(ctx, curlCmd, os.Stdout, os.Stderr); err != nil {
		panic(err)
	}
}

Go SDK private-IP behavior (10/8, 172.16/12, 192.168/16):

  • Default (unset): private IPs are blocked whenever a network config is sent.
  • Explicit block: call .WithBlockPrivateIPs(true) (or .BlockPrivateIPs()).
  • Explicit allow: call .AllowPrivateIPs() or .WithBlockPrivateIPs(false).
sandbox := sdk.New("alpine:latest").
	AllowHost("api.openai.com").
	AddHost("api.internal", "10.0.0.10").
	WithNetworkMTU(1200).
	AllowPrivateIPs() // explicit override: block_private_ips=false

// SDK network interception (request/response mutation, body shaping, SSE data-line transform)
sandbox = sandbox.WithNetworkInterception(&sdk.NetworkInterceptionConfig{
	Rules: []sdk.NetworkHookRule{
		{
			Phase:      sdk.NetworkHookPhaseBefore,
			Action:     sdk.NetworkHookActionMutate,
			Hosts:      []string{"api.openai.com"},
			SetHeaders: map[string]string{"X-Trace-Id": "trace-123"},
		},
		{
			Phase:  sdk.NetworkHookPhaseAfter,
			Action: sdk.NetworkHookActionMutate,
			Hosts:  []string{"api.openai.com"},
			BodyReplacements: []sdk.NetworkBodyTransform{
				{Find: "internal-id", Replace: "redacted"},
			},
		},
	},
})

If you use client.Create(...) directly (without the builder), set:

  • BlockPrivateIPsSet: true
  • BlockPrivateIPs: false (or true)

For fully offline sandboxes (no guest NIC / no egress), use:

  • CLI: --no-network
  • Go SDK builder: .WithNoNetwork()
  • Python SDK builder: .with_no_network()
  • TypeScript SDK builder: .withNoNetwork()

Python (PyPI)

pip install matchlock
# or
uv add matchlock
import os
import sys

from matchlock import Client, Sandbox

sandbox = (
    Sandbox("python:3.12-alpine")
    .allow_host(
        "dl-cdn.alpinelinux.org",
        "files.pythonhosted.org", "pypi.org",
        "astral.sh", "github.com", "objects.githubusercontent.com",
        "api.anthropic.com",
    )
    .add_secret(
        "ANTHROPIC_API_KEY", os.environ["ANTHROPIC_API_KEY"], "api.anthropic.com"
    )
)

SCRIPT = """\
# /// script
# requires-python = ">=3.12"
# dependencies = ["anthropic"]
# ///
import anthropic, os

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
with client.messages.stream(
    model="claude-haiku-4-5-20251001",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Explain TCP/IP."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
print()
"""

with Client() as client:
    client.launch(sandbox)
    client.exec("pip install --quiet uv")
    client.write_file("/workspace/ask.py", SCRIPT)
    client.exec_stream("uv run /workspace/ask.py", stdout=sys.stdout, stderr=sys.stderr)

client.remove()

TypeScript

npm install matchlock-sdk
import { Client, Sandbox } from "matchlock-sdk";

const SCRIPT = `import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

const stream = anthropic.messages
  .stream({
    model: "claude-haiku-4-5-20251001",
    max_tokens: 1024,
    messages: [{ role: "user", content: "Explain TCP/IP." }],
  })
  .on("text", (text) => {
    process.stdout.write(text);
  });

await stream.finalMessage();
process.stdout.write("\\n");
`;

const client = new Client();
try {
  const sandbox = new Sandbox("node:22-alpine")
    .allowHost("registry.npmjs.org", "*.npmjs.org", "api.anthropic.com")
    .addSecret("ANTHROPIC_API_KEY", process.env.ANTHROPIC_API_KEY ?? "", "api.anthropic.com");

  await client.launch(sandbox);
  await client.exec(
    "npm init -y >/dev/null 2>&1 && npm install --quiet --no-bin-links @anthropic-ai/sdk",
    { workingDir: "/workspace" },
  );
  await client.writeFile("/workspace/ask.mjs", SCRIPT);
  await client.execStream("node ask.mjs", {
    workingDir: "/workspace",
    stdout: process.stdout,
    stderr: process.stderr,
  });
} finally {
  await client.close();
  await client.remove();
}

More examples in the examples/ directory:

Description Example
Streams Anthropic API response with secret injection (Go) examples/go/basic/
Interactive terminal with PTY using ExecInteractive (Go) examples/go/exec_modes/
Injects API key via network interception hook (Go) examples/go/network_interception/
VFS interception hooks for file operation mutations (Go) examples/go/vfs_hooks/
Streams Anthropic API response (Python) examples/python/basic/
Stream, pipe, and interactive execution modes (Python) examples/python/exec_modes/
Injects API key via network interception hook (Python) examples/python/network_interception/
VFS interception hooks for file operation mutations (Python) examples/python/vfs_hooks/
Streams Anthropic API response (TypeScript) examples/typescript/basic/
Stream, pipe, and interactive execution modes (TypeScript) examples/typescript/exec_modes/
Injects API key via network interception hook (TypeScript) examples/typescript/network_interception/
Claude Code CLI in micro-VM with GitHub bootstrap examples/claude-code/
Claude Code with Docker inside sandbox via SDK examples/claude-code-with-docker/
OpenAI Codex CLI in micro-VM with GitHub bootstrap examples/codex/
Docker daemon inside sandbox with systemd examples/docker-in-sandbox/
Streamlit chatbot using Agent Client Protocol examples/agent-client-protocol/
Browser automation with Kodelet and Playwright MCP examples/playwright/

Architecture

graph LR
    subgraph Host
        CLI["Matchlock CLI"]
        Policy["Policy Engine"]
        Proxy["Transparent Proxy + TLS MITM"]
        VFS["VFS Server"]

        CLI --> Policy
        CLI --> Proxy
        Policy --> Proxy
    end

    subgraph VM["Micro-VM (Firecracker / Virtualization.framework)"]
        Agent["Guest Agent"]
        FUSE["/workspace (FUSE)"]
        Image["Any OCI Image (Alpine, Ubuntu, etc.)"]

        Agent --- Image
        FUSE --- Image
    end

    Proxy -- "vsock :5000" --> Agent
    VFS -- "vsock :5001" --> FUSE
Network Modes
Platform Mode Mechanism
Linux Transparent proxy nftables DNAT on ports 80/443
macOS NAT (default) Virtualization.framework built-in NAT
macOS Interception (with --allow-host/--secret) gVisor userspace TCP/IP at L4

Docs

License

MIT

Directories

Path Synopsis
cmd
guest-init command
guest-init is the unified guest runtime binary.
guest-init is the unified guest runtime binary.
matchlock command
examples
go/basic command
go/exec_modes command
go/vfs_hooks command
internal
guestruntime/agent
Guest agent runs inside the Firecracker VM and handles: 1.
Guest agent runs inside the Firecracker VM and handles: 1.
guestruntime/fused
Guest FUSE daemon using go-fuse library Connects to host VFS server over vsock and mounts at configurable workspace
Guest FUSE daemon using go-fuse library Connects to host VFS server over vsock and mounts at configurable workspace
pkg
api
net
rpc
sandbox
Package sandbox provides the core sandbox VM management functionality.
Package sandbox provides the core sandbox VM management functionality.
sdk
Package sdk provides a client for interacting with Matchlock sandboxes via JSON-RPC.
Package sdk provides a client for interacting with Matchlock sandboxes via JSON-RPC.
vfs
vm
vsock
wire.go provides net.Conn-based helpers for the vsock framed message protocol.
wire.go provides net.Conn-based helpers for the vsock framed message protocol.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL