# matchlock: ephemeral microVMs where the credential never crosses the boundary

> Matchlock runs AI agents in disposable Linux microVMs with a network allowlist and a MITM proxy that injects real credentials in flight while the guest sees a placeholder. The parts worth reading closely are the private-IP default, which behaves differently between the builder and the raw constructor, and the package naming, which is amd64 only.

**jingkaihe/matchlock** — Matchlock secures AI agent workloads with a Linux-based sandbox.

- Repository: https://github.com/jingkaihe/matchlock
- Stars: 621 · Forks: 38
- Language: Go
- License: MIT
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/jingkaihe-matchlock

## The secret never enters the VM, and that is the whole product

Matchlock is a CLI tool for running AI agents in ephemeral microVMs, with network allowlisting, secret injection through a MITM proxy and VM-level isolation. The one-line summary of the design is that your secrets never enter the VM.

The mechanism has two halves and they work together. When you pass `--allow-host` or `--secret`, Matchlock seals the network, so only traffic to explicitly allowed hosts gets through and everything else is blocked. Then, when the agent calls an API, the real credentials are injected in flight by the host, and the sandbox only ever sees a placeholder.

The security argument follows directly. Even if the agent is tricked into running something malicious, the keys do not leak and there is nowhere for data to go, because the egress path is already closed.

What the agent gets inside is a full Linux environment that boots in under a second, and it can install packages, write files and make a mess. Volume mounts are overlay snapshots isolated from the host and they vanish when the run is over. The same command line and the same behaviour apply whether the host is a Linux server or a MacBook.

## Fifteen direct dependencies, one per mechanism

The dependency list is the clearest statement of how this works, because each library corresponds to exactly one part of the design.

`Code-Hex/vz` is the macOS virtualization framework, and it is why Apple Silicon is the only supported Mac. `gvisor.dev/gvisor` provides the userspace kernel used inside the Linux microVM, and it is pinned to a specific pseudo-version rather than a tag. `google/nftables` is the host firewall, which is how the network seal is enforced. `hanwen/go-fuse` provides the overlay volume mounts. `google/go-containerregistry` handles image resolution and pulls. `fxamacker/cbor` is the binary encoding. `modernc.org/sqlite` is a pure-Go SQLite, chosen so the binary needs no cgo. `creack/pty` and `golang.org/x/term` cover interactive terminals, `spf13/cobra` and `spf13/viper` the command line and configuration, and `stretchr/testify` the tests.

Two details sit underneath those. The module declares `go 1.25.5`, a patch-level floor rather than a minor one, so contributors are pinned to one exact toolchain release. And no cgo appears anywhere in the direct list, which is consistent with a binary meant to cross package formats.

## Private ranges are blocked by default, and the two constructor paths differ

The private address ranges in question are `10/8`, `172.16/12` and `192.168/16`, and the default is to block them whenever a network config is sent.

From there the documentation splits by how you build a sandbox. Using the builder, an explicit block is `.WithBlockPrivateIPs(true)` or `.BlockPrivateIPs()`, and an explicit allow is `.AllowPrivateIPs()` or `.WithBlockPrivateIPs(false)`.

Using `client.Create(...)` directly, without the builder, the requirement is different: you must set `BlockPrivateIPsSet: true` and then `BlockPrivateIPs` to false or true.

That asymmetry is the thing to notice. The `Set` flag exists because the zero value of a boolean cannot tell you whether the caller chose or never spoke. A caller who constructs the struct by hand and omits `BlockPrivateIPsSet` gets the unset path rather than an explicit decision, and the documented default only covers the case where a network config is sent.

For an agent sandbox, allowing the private ranges means the guest can reach services on your LAN, so this is the setting to review rather than inherit.

## Two supported platforms, and amd64-only packages

The system requirements list two rows: Linux with KVM support, and macOS on Apple Silicon.

The macOS row is not a preference. The virtualization dependency is Apple's own framework, and it is what the entitlements file at the repository root exists for.

The Linux row requires KVM, so a virtual machine without nested virtualisation will not qualify, and a cloud instance without hardware virtualisation will not either.

Package naming then narrows things further. The Debian and Ubuntu instructions use a file named `matchlock_<version>_linux_amd64.deb`, installed with dpkg and followed by an apt-get install to fix dependencies and then `matchlock diagnose`. The Fedora, RHEL and CentOS Stream instructions use `matchlock_<version>_linux_amd64.rpm` with dnf, again followed by diagnose.

So the distributed artefacts are amd64. Homebrew is the alternative route, and the README states it is supported on both macOS and Linux, which is the path for an arm64 Mac. If diagnose reports missing host setup, the documented fix is `sudo matchlock setup linux`, and a specific user can be enrolled with `sudo matchlock setup user <name>`.

## Three network states, and an allowlist that can stay open

The basic usage is a run command with an image and a program:

```bash
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'
```

Those three lines cover the range. A plain run with no network flags, an interactive shell, and a fully offline sandbox.

The third line uses `--no-network`, and the documentation is explicit that this means no guest NIC and no egress at all, which is different from a sealed network with nothing on the allowlist. In the SDKs the same state has four spellings: `--no-network` on the command line, `.WithNoNetwork()` in Go, `.with_no_network()` in Python and `.withNoNetwork()` in TypeScript.

The middle state is the interesting one. A run can keep interception enabled with an empty allowlist so hosts can be added and removed at runtime, using `--rm=false` together with `--network-intercept`, after which `matchlock allow-list add` and `matchlock allow-list delete` take a VM id and a comma-separated host list. That is a live network policy on a running VM.

One more detail explains why the Python example allowlists seven hosts, including the Alpine CDN, pypi.org, files.pythonhosted.org, astral.sh and github.com: the guest installs its own packages, so package indexes have to be reachable from inside.

## Ten example directories, four named after real coding agents

The examples directory holds ten subdirectories, and the names are the clearest signal of who this is for.

Four are agent-oriented: `claude-code`, `claude-code-with-docker`, `claude-danger` and `codex`. There is also `agent-client-protocol` and `docker-in-sandbox`, which cover running an agent inside a container that is itself inside the sandbox. The remaining four are the SDK languages, `go`, `python`, `typescript`, plus `playwright` for a browser dependency.

The README's example table names what each Go example demonstrates: streaming an Anthropic API response with secret injection, an interactive terminal with a PTY using the interactive exec mode, injecting an API key through a network interception hook, and VFS interception hooks for file operations.

The interception surface is broader than headers. Network hooks are configured per phase and per action, and can mutate requests and responses, shape bodies and transform SSE data lines, with rules keyed by host and able to set headers.

That combination, a real coding agent running in a VM with a credential injected at the proxy, is the use case the examples are built around.

## A version file, goreleaser, decision records, and an example that lags

The repository root holds 25 entries. Release mechanics are visible in three of them: a `VERSION.txt` file, a `.goreleaser.yaml` configuration and a `RELEASE.md`.

There is also an `adrs/` directory, which is where architecture decision records live, and that is the right first place to look for why the dependency set is the way it is. `guest/` holds the guest image side, `sdk/` is a top-level directory separate from `pkg/`, and `matchlock.entitlements` at the root is the macOS side of the virtualization framework. Tool versions are managed through `mise.toml`.

Two small mismatches are visible. The quick install example pins a specific release with `--version 0.2.4`, while the newest tag in the list is v0.2.17, so the documented example trails the current release by thirteen patch versions. And the README labels the project experimental and warns that it is subject to breaking changes.

The release history is regular patch cadence: v0.2.15 on 2026-06-13, v0.2.16 on 2026-06-28 and v0.2.17 on 2026-07-26. The last push was on 2026-09-26, two months after that tag, and the repository is not archived.

## Conclusion

Use Matchlock if you are letting an agent install packages and run commands against credentials you care about, and you have KVM on Linux or an Apple Silicon Mac. Do not use it if you need Windows, non-Apple-Silicon macOS, or an arm64 Linux build, since the packaged artefacts are amd64. Before you deploy, decide how you will construct sandboxes, because the builder form and the direct client call set the private-IP policy with different fields, and treat the allowlist as part of your threat model rather than a convenience.

## FAQ

### What does Matchlock do for AI agents?

It runs them in ephemeral microVMs with network allowlisting, secret injection through a MITM proxy and VM-level isolation. The host injects real credentials in flight while the sandbox only ever sees a placeholder, so an agent that installs packages and runs commands inside a full Linux environment cannot leak the key, and the network is sealed unless you allow specific hosts.

### What platforms does Matchlock support?

Linux with KVM support and macOS on Apple Silicon. The macOS requirement follows from the Apple virtualization framework it depends on, and the packaged `.deb` and `.rpm` artefacts are named for linux_amd64, so Homebrew is the route to use on an arm64 Mac. Installation is otherwise a script that detects the OS and picks Homebrew, deb or rpm.

### How do I give a Matchlock sandbox an API key?

Add the secret scoped to a host, so the host injects the real credential while the guest sees a placeholder. From the command line that is the `--secret` flag, and from the SDKs it is `AddSecret`. Note that if you call `client.Create(...)` directly instead of the builder you also need to set `BlockPrivateIPsSet` alongside `BlockPrivateIPs`.

### How do I run a Matchlock sandbox with no network at all?

Pass `--no-network`, which gives the sandbox no guest NIC and no egress. The SDK equivalents are `.WithNoNetwork()` in Go, `.with_no_network()` in Python and `.withNoNetwork()` in TypeScript. That is a different state from a sealed network with an empty allowlist, which can be kept open for hosts to be added at runtime.

## Sources

- [Issues](https://github.com/jingkaihe/matchlock/issues)
- [jingkaihe/matchlock on GitHub](https://github.com/jingkaihe/matchlock)
- [License: MIT](https://github.com/jingkaihe/matchlock/blob/main/LICENSE)
- [README](https://github.com/jingkaihe/matchlock/blob/main/README.md)
- [Releases](https://github.com/jingkaihe/matchlock/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jingkaihe-matchlock
