Skip to main content

Matchlock Python SDK

A Python client for Matchlock — a lightweight micro-VM sandbox for running AI-generated code securely with network interception and secret protection.

Requirements

  • Python 3.10+
  • The matchlock CLI binary installed and available on $PATH (or specify its path via Config)

Installation

pip install matchlock

Or install from source:

pip install -e sdk/python

Quick Start

from matchlock import Client, Sandbox

sandbox = Sandbox("python:3.12-alpine")

with Client() as client:
    client.launch(sandbox)

    result = client.exec("echo hello from the sandbox")
    print(result.stdout)  # "hello from the sandbox\n"

Usage

Builder API

The Sandbox class provides a fluent builder for configuring sandboxes:

import os
from matchlock import Client, Sandbox

sandbox = (
    Sandbox("python:3.12-alpine")
    .with_privileged()
    .with_cpus(2)
    .with_memory(512)
    .with_disk_size(2048)
    .with_timeout(300)
    .with_workspace("/home/user/code")
    .allow_host("api.openai.com", "pypi.org")
    .with_port_forward(18080, 8080)
    .with_port_forward_addresses("127.0.0.1")
    .with_network_mtu(1200)
    .block_private_ips()
    .add_secret("API_KEY", os.environ["API_KEY"], "api.openai.com")
)

with Client() as client:
    vm_id = client.launch(sandbox)
    result = client.exec("python3 -c 'print(1+1)'")
    print(result.exit_code)  # 0
    print(result.stdout)     # "2\n"

Streaming Output

Use exec_stream for real-time stdout/stderr streaming:

import sys
from matchlock import Client, Sandbox

with Client() as client:
    client.launch(Sandbox("alpine:latest"))

    result = client.exec_stream(
        "for i in 1 2 3; do echo $i; sleep 1; done",
        stdout=sys.stdout,
        stderr=sys.stderr,
    )
    print(f"Exit code: {result.exit_code}")
    print(f"Duration: {result.duration_ms}ms")

File Operations

Read, write, and list files inside the sandbox:

from matchlock import Client, Sandbox

with Client() as client:
    client.launch(Sandbox("alpine:latest"))

    # Write a file
    client.write_file("/workspace/hello.txt", "Hello, world!")
    client.write_file("/workspace/script.sh", "#!/bin/sh\necho hi", mode=0o755)

    # Read a file
    content = client.read_file("/workspace/hello.txt")
    print(content.decode())  # "Hello, world!"

    # List files
    files = client.list_files("/workspace")
    for f in files:
        print(f"{f.name} ({f.size} bytes, dir={f.is_dir})")

Network Policy & Secrets

Control network access and inject secrets securely:

import os
from matchlock import Client, Sandbox

sandbox = (
    Sandbox("python:3.12-alpine")
    # Only allow these hosts
    .allow_host("api.anthropic.com", "pypi.org", "files.pythonhosted.org")
    # Optional MTU override
    .with_network_mtu(1200)
    # Explicitly block or allow private IPs (10.x, 172.16.x, 192.168.x)
    .with_block_private_ips(True)
    # Inject secret — the MITM proxy replaces the placeholder with the real
    # value only when requests go to the specified host
    .add_secret("ANTHROPIC_API_KEY", os.environ["ANTHROPIC_API_KEY"], "api.anthropic.com")
)

with Client() as client:
    client.launch(sandbox)
    result = client.exec("python3 call_api.py")

    # Optional: forward local 18080 -> guest 8080 after VM creation
    bindings = client.port_forward("18080:8080")
    print(bindings[0].address, bindings[0].local_port, bindings[0].remote_port)

Private-IP behavior in the Python SDK:

  • Default (unset): private IPs are blocked whenever a network config is sent.
  • Explicit block: .block_private_ips() or .with_block_private_ips(True).
  • Explicit allow: .allow_private_ips() or .with_block_private_ips(False).
  • Reset to default behavior: .unset_block_private_ips().

If you use CreateOptions(...) directly instead of the builder, set both:

  • block_private_ips_set=True
  • block_private_ips=<True|False>

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

  • Builder: .with_no_network()
  • Direct options: CreateOptions(no_network=True)

Network Interception Rules

Use typed network hook rules for host-side request/response mutation:

from matchlock import (
    NetworkBodyTransform,
    NetworkHookRule,
    NetworkInterceptionConfig,
    Sandbox,
)

sandbox = Sandbox("alpine:latest").with_network_interception(
    NetworkInterceptionConfig(
        rules=[
            NetworkHookRule(
                phase="after",
                hosts=["api.example.com"],
                action="mutate",
                set_response_headers={"X-Intercepted": "true"},
                body_replacements=[NetworkBodyTransform(find="foo", replace="bar")],
            )
        ]
    )
)

Callback-based interception:

from matchlock import (
    NetworkHookRequest,
    NetworkHookResult,
    NetworkHookResponseMutation,
    NetworkHookRule,
    NetworkInterceptionConfig,
    Sandbox,
)

def after_hook(req: NetworkHookRequest) -> NetworkHookResult | None:
    if req.status_code != 200:
        return None
    return NetworkHookResult(
        action="mutate",
        response=NetworkHookResponseMutation(
            headers={"X-Intercepted": ["callback"]},
            set_body=b'{"msg":"from-callback"}',
        ),
    )

sandbox = Sandbox("alpine:latest").with_network_interception(
    NetworkInterceptionConfig(
        rules=[
            NetworkHookRule(
                phase="after",
                hosts=["api.example.com"],
                hook=after_hook,
                timeout_ms=1500,
            )
        ]
    )
)

Filesystem Mounts

Mount host directories or use in-memory/snapshot filesystems:

from matchlock import Client, Sandbox, MountConfig

sandbox = (
    Sandbox("alpine:latest")
    .mount_host_dir("/workspace/src", "/home/user/project/src")
    .mount_host_dir_readonly("/workspace/config", "/home/user/project/config")
    .mount_memory("/workspace/tmp")
    .mount_overlay("/workspace/data", "/home/user/project/data")
    # Or use the generic mount() method:
    .mount("/workspace/custom", MountConfig(type="host_fs", host_path="/tmp/custom"))
)

Pass owner_uid and/or owner_gid to have all files in the mount appear owned by a specific user inside the VM. This is useful when the sandbox runs as a non-root user (via .with_user()). Both values must be integers in [0, 4294967295]; a ValueError is raised otherwise. Only host_fs mounts support this option:

sandbox = (
    Sandbox("alpine:latest")
    .with_user("1000:1000")
    .mount_host_dir("/workspace/src", "/home/user/project/src", owner_uid=1000, owner_gid=1000)
    .mount_host_dir_readonly("/workspace/config", "/home/user/project/config", owner_uid=1000)
)

Custom Configuration

from matchlock import Client, Config

config = Config(
    binary_path="/usr/local/bin/matchlock",
    use_sudo=True,  # Required for TAP devices on Linux
)

with Client(config) as client:
    ...

The binary path can also be set via the MATCHLOCK_BIN environment variable.

Lifecycle Management

from matchlock import Client, Sandbox

client = Client()
client.start()

client.launch(Sandbox("alpine:latest"))
print(client.vm_id)  # e.g., "vm-abc12345"

result = client.exec("echo hello")

# Shut down the sandbox VM
client.close()

# Remove the stopped VM's state directory
client.remove()

Error Handling

from matchlock import Client, Sandbox, MatchlockError, RPCError

with Client() as client:
    try:
        client.launch(Sandbox("alpine:latest"))
        result = client.exec("exit 1")
        if result.exit_code != 0:
            print(f"Command failed: {result.stderr}")
    except RPCError as e:
        print(f"RPC error [{e.code}]: {e.message}")
        if e.is_vm_error():
            print("VM-level failure")
        elif e.is_exec_error():
            print("Execution failure")
        elif e.is_file_error():
            print("File operation failure")
    except MatchlockError as e:
        print(f"Matchlock error: {e}")

API Reference

Sandbox(image: str)

Fluent builder for sandbox configuration.

Method Description
.with_cpus(n) Set number of vCPUs (supports fractional values, e.g. 0.5)
.with_privileged() Enable privileged mode (skip in-guest seccomp/cap-drop/no_new_privs)
.with_memory(mb) Set memory in MB
.with_disk_size(mb) Set disk size in MB
.with_timeout(seconds) Set max execution time
.with_workspace(path) Set guest VFS mount point (default: /workspace)
.allow_host(*hosts) Add allowed network hosts (supports wildcards)
.block_private_ips() Block access to private IP ranges
.with_block_private_ips(enabled) Explicitly set private IP blocking true/false
.allow_private_ips() Explicitly allow private IP ranges
.unset_block_private_ips() Reset private IP behavior to SDK default semantics
.with_network_mtu(mtu) Override guest network stack/interface MTU
.with_no_network() Disable guest network egress entirely
.with_port_forward(local_port, remote_port) Add a host-to-guest port mapping
.with_port_forward_addresses(*addresses) Set bind addresses for configured port mappings
.with_network_interception(config=None) Force interception and optionally apply typed network hook rules
.add_secret(name, value, *hosts) Inject a secret for specific hosts
.mount(guest_path, config) Add a VFS mount with custom MountConfig
.mount_host_dir(guest, host, *, owner_uid=None, owner_gid=None) Mount a host directory (read-write); optionally override file ownership inside the VM
.mount_host_dir_readonly(guest, host, *, owner_uid=None, owner_gid=None) Mount a host directory (read-only); optionally override file ownership inside the VM
.mount_memory(guest_path) Mount an in-memory filesystem
.mount_overlay(guest, host) Mount an isolated snapshot of host path
.options() Return the built CreateOptions

Client(config: Config | None = None)

JSON-RPC client for interacting with Matchlock sandboxes. All public methods are thread-safe.

Method Description
.start() Start the matchlock RPC subprocess
.launch(sandbox) Create a VM and start image ENTRYPOINT/CMD in detached mode — returns VM ID
.create(opts) Create a VM from CreateOptions (does not auto-start ENTRYPOINT unless launch_entrypoint=True) — returns VM ID
.exec(command, working_dir="") Execute a command, returns ExecResult
.exec_stream(command, stdout, stderr, working_dir) Stream command output, returns ExecStreamResult
.log() Return the current buffered VM log as str
.log_stream(stdout=None) Stream VM log output until cancelled
.exec_pipe(command, stdin=None, stdout=None, stderr=None, working_dir="", timeout=None, user="") Bidirectional pipe-mode exec as an optional user (uid, uid:gid, or username), returns ExecPipeResult
.exec_interactive(command, stdin=None, stdout=None, working_dir="", rows=24, cols=80, resize=None, timeout=None, user="") Interactive PTY exec as an optional user (uid, uid:gid, or username), returns ExecInteractiveResult
.write_file(path, content, mode=0o644) Write a file into the sandbox
.read_file(path) Read a file from the sandbox — returns bytes
.list_files(path) List directory contents — returns list[FileInfo]
.port_forward(*specs) Apply one or more [LOCAL_PORT:]REMOTE_PORT mappings
.port_forward_with_addresses(addresses, *specs) Apply port mappings bound on specific host addresses
.close(timeout=0) Shut down the sandbox VM. timeout in seconds; 0 = kill immediately
.remove() Remove the stopped VM's state directory
.vm_id The current VM ID (property)

Types

Type Fields
Config binary_path: str, use_sudo: bool
CreateOptions image, privileged, cpus, memory_mb, disk_size_mb, timeout_seconds, allowed_hosts, block_private_ips, block_private_ips_set, no_network, force_interception, network_interception, mounts, env, vfs_interception, secrets, workspace, dns_servers, network_mtu, port_forwards, port_forward_addresses, image_config, launch_entrypoint
ExecResult exit_code: int, stdout: str, stderr: str, duration_ms: int
ExecStreamResult exit_code: int, duration_ms: int
ExecPipeResult exit_code: int, duration_ms: int
ExecInteractiveResult exit_code: int, duration_ms: int
FileInfo name: str, size: int, mode: int, is_dir: bool
MountConfig type: str, host_path: str, readonly: bool
PortForward local_port: int, remote_port: int
PortForwardBinding address: str, local_port: int, remote_port: int
Secret name: str, value: str, hosts: list[str]
MatchlockError Base exception for all Matchlock errors
RPCError RPC error with code: int and message: str

License

MIT

Metadata

Release files for matchlock 0.2.17

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for matchlock 0.2.17
File Size Uploaded
matchlock-0.2.17.tar.gz 35.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for matchlock 0.2.17
File Interpreter ABI Platform
matchlock-0.2.17-py3-none-any.whl Python 3 none any Details

Total release size: 60.0 kB

Release files / matchlock-0.2.17.tar.gz

Download URL matchlock-0.2.17.tar.gz
Size 35.7 kB
Tags Source
SHA-256 checksum
How to use checksums
ad0742c63653ad9bbef6be50afaabf5f5c11b41681e4d8c5b6c8ca820860e5a0
BLAKE2b-256 checksum
How to use checksums
16952b9bb32f555b3602f076c0e60a1e8e200b2f57bde4cf9b1258deeebbf4ce
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 26, 2026.

Transparency log

Release files / matchlock-0.2.17-py3-none-any.whl

Download URL matchlock-0.2.17-py3-none-any.whl
Size 24.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
65be1d27af06860503ec7909a5a99d9343b974cbe92c21e4a5f9bef544332ec2
BLAKE2b-256 checksum
How to use checksums
5b873e362b2ee31c4f763b8b5561ecff8347452c74ec233114e45360d29a1487
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.17 This release

2 release files

0.2.16

2 release files

0.2.15

2 release files

0.2.14

2 release files

0.2.13

2 release files

0.2.12

2 release files

0.2.11

2 release files

0.2.9

2 release files

0.2.8

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.28

2 release files

0.1.27

2 release files

0.1.26

2 release files

0.1.25

2 release files

0.1.24

2 release files

0.1.23

2 release files

0.1.22

2 release files

0.1.21

2 release files

0.1.20

2 release files

0.1.19

2 release files

0.1.12

2 release files

0.1.11

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page