A coding agent is asked to make a failing test pass. To do that it needs your repository, a package index, perhaps a GitHub token and a model API key. Run it on a CI runner the usual way and it gets far more: the runner's whole network, including the cloud metadata address and anything on the private network; every environment variable, keys included; and write access to the checkout, including the workflow files that decide what runs next.
Tracekit Box is an open-source runtime (github.com/Cygnux-Labs/Tracekit-box) that runs each agent job in its own Firecracker micro-VM with no network card. The only ways out of the VM (the network, secrets, model calls, commands and code changes) are doors on the host. A Tracekit signer the agent cannot reach decides each crossing, and every job ends as one .tkb file that anyone can verify offline. This write-up explains how Box is built, what it guarantees, and what it does not. The full paper, with every test, table and limit, is Tracekit Box: Running AI Agents in Micro-VMs Where Every Way Out Is Decided and Signed (arXiv:2610.38458).

A real recording of one box. Every terminal line on screen is from the recording.
The short version
- One VM per job, with no network card. The agent runs in a Firecracker micro-VM booted from an ordinary container image. The VM's only connections to the host are three vsock ports the host serves.
- The network door decides who, never what. Every connection goes through a proxy on the host. Allowed hosts pass, any other host waits for a person on the host, and private, link-local, cloud metadata and host addresses are always refused. Names are resolved on the host, so no DNS query ever leaves the box.
- Secrets never enter the VM. The box holds a placeholder. The proxy adds the real value on the way out, in one header, only for the hosts the secret is bound to, and blocks the placeholder or the value anywhere else it can see.
- Code changes come back as a patch.
tkbox applywrites nothing to your checkout unless the signer allows every file, and holds back workflow files and other files that steer the next run. - Every crossing is signed by a separate process, and Box fails closed. If the signer does not answer, nothing runs. If a record cannot be kept, the box is stopped and the loss is recorded.
- How it was tested. 13 of 13 end-to-end checks pass on real KVM in CI. A first review found 0 critical, 1 high, 4 medium and 14 low issues; fuzzing produced nine findings, all fixed (eight from the campaign that wrote the fuzz targets, one that the repository's own
make fuzzrun found later); the final launch check found no high or medium issue open. Each fix comes with a test that fails without it. - What it is not, yet. This is v0: built from source, for one Linux host with KVM, without Firecracker's jailer, and with the signer on the same host as the daemon.
Why this matters
Teams are moving agents from a developer's laptop into CI and production, where the agent's mistakes cost more. The agent reads issues, web pages, dependency READMEs and tool output, and any of them can carry instructions. A prompt-injected agent does not look different from a working one until it acts. So the boundary has to hold no matter what the agent intends, and the record of what crossed it has to be kept somewhere the agent cannot edit.
The usual setup gets neither. Containers share the host's kernel. Environment variables put the real keys inside the process the agent controls. Egress filtering can leak through DNS itself: OpenAI described an agent that reached an outside chatbot through a DNS-filtering gap in its training sandbox. On 22 October, METR showed that two headers the client controls could switch off OpenAI's monitoring of new instances of its internal coding agents: a record the agent's side can reach can be turned off. And the log of what the agent did is usually written by the same machine the agent runs on.
Box is built around one rule: everything that leaves the VM goes through a door on the host, every door asks a signer the agent cannot reach, and the signed record is the product of the job.
Three trust domains

Figure 1. The host serves the only ways out of the VM. The signer decides each crossing from its own trust domain.
- The host (trusted) runs
tkbox daemon: one Go binary that starts the VMs, owns the doors and keeps the secrets. It also builds the disks and serves an HTTP API on a Unix socket, which the CLI, the Python SDK, the MCP server and the GitHub Action all use. - The VM (untrusted) runs the agent and everything it starts, including Box's own guest agent,
tkbox-guest, which is PID 1. The threat model assumes the attacker controls everything in the VM, including its kernel's view of the world and every byte the guest agent sends. The host never trusts what the VM says about itself, only what passes through a door. - The Tracekit signer holds the signing key in its own trust domain. Box reaches it over a Unix socket on the same host, and the VM has no path to either socket. In production the signer runs as a separate user, so no other process of the daemon's user can reach its key, and a witness on another host cosigns its log.
What crosses between host and VM
Firecracker exposes the guest's vsock device as a Unix socket on the host. Box uses exactly three ports, with one connection per request:
- 1024, agent RPC (host to guest). The daemon asks the guest agent to run a command, read or write a file, or compute the workspace diff. Messages are length-prefixed frames of at most 1 MiB, and the daemon parses everything the guest sends as untrusted.
- 1025, egress (guest to host). The guest's egress forwarder on
127.0.0.1:3128hands each connection to the box's proxy on the host. Every tool in the VM is pointed at it through the proxy variables Box sets. - 1026, boot config (guest to host). At boot the guest reads its configuration: its environment with secret placeholders, the proxy address, the box's CA certificate, its disks, its limits and the host's clock. The port is served once and then closed. Because box-specific state arrives this way and is never baked into the image, images stay cacheable by digest.
Disks
| Disk | Mode | Built from |
|---|---|---|
| Root filesystem | read-only | Any public OCI image, its layers flattened into an ext4 disk with the guest agent added; cached by the image's manifest digest |
| Workspace base | read-only | Your checkout: tracked and untracked-but-not-ignored files, never .git, never credential files |
| Workspace upper | read-write | An empty ext4 disk, sized by the box's disk limit |
The root filesystem is mounted read-only under a tmpfs overlay, so writes to /usr or /tmp live in the VM's memory and vanish with it. /workspace is an overlay of the two workspace disks, so every change the agent makes to your code is on the upper disk, and the diff is exact and cheap.
The doors
Each box is one Tracekit run, and every door records into it.
| Door | What Box does | What the signed record shows |
|---|---|---|
| Commands | Runs a command started from the host only after the signer allows it | A Bash decision with the command line, working directory, environment and stdin digest, then the exit status |
| Network | Sends every connection through the host proxy | A WebFetch decision per connection, and tkbox.approval for a person's approvals |
| Secrets | Swaps the real value in on the way out, only for bound hosts | tkbox.secret.use and tkbox.secret.leak, with the secret's name and host, never its value |
| Model calls | Treats model APIs as egress to a model host, with the key bound as a secret | tkbox.model.call with host, path, status and size |
| Code changes | Returns a patch, and writes it only file by file after the signer agrees | A Write decision per file, with the patch digest and the file's content hash and mode |
| Stop | Pauses or kills the VM on a timeout, a stop, or a hold from the signer | The run's close reason |
The network door

Figure 2. The checks every connection passes, and what the demo box from the README does with six commands.
The VM has no network card, so the proxy on the host is the only route out. For each connection:
- The allowlist comes first. An entry
hostallows HTTPS on 443 and plain HTTP on 80;host:portallows only that port. Presets cover the common cases (pypi,npm,github,debian,apt-ubuntu,go-proxy,anthropic,openai,claude-code). Any other host waits up to two minutes for a person on the host to runtkbox approve. Nothing inside the VM can answer an approval. A box may have at most 16 approvals pending and open at most 30 a minute, so a flood of requests is refused at once rather than queued. - Names are resolved once, on the host. Loopback, private, CGNAT, link-local, multicast and other special ranges, the major clouds' metadata addresses, and the host's own addresses are refused, after unwrapping IPv4-mapped and NAT64 forms. The host's addresses are re-read at most every five seconds, so an address the host gains later is refused too. The proxy dials the address it checked, so a name cannot rebind between the check and the dial.
- The TLS server name must match. An HTTPS tunnel must start with a ClientHello naming exactly the host the tunnel was opened for, checked again after a HelloRetryRequest. Encrypted ClientHellos are refused, because Box must see the name. This stops fronting by SNI.
- Only HTTP and HTTPS leave. There is no UDP, no DNS from the box and no SSH. A tunnel that is not TLS is allowed only to an entry that names its port.
The proxy decides who the agent may talk to, never what it says to them. An agent allowed to reach api.github.com with a token can do anything that token permits there. And an allowed host that fetches URLs for its callers relays data further: through the go-proxy preset, the agent can make the module mirror contact a domain of its choosing.
The secrets door

Figure 3. A secret is bound to exact hosts and one header. The box only ever holds a placeholder.
A secret is given on the host, for example --secret GH=env:GH_TOKEN@api.github.com. The value is read when the box starts and stays in the daemon's memory. Inside the VM, $GH is a placeholder such as tkbox_GH_3f9c…: printing it, logging it or committing it leaks nothing.
For hosts with a bound secret, the proxy reads the traffic with a per-box ECDSA CA whose certificate is name-constrained to those hosts, so it cannot be used to impersonate anything else. It swaps the real value into the bound header (Authorization unless you name another) and nowhere else. On every request it reads, a placeholder anywhere else, or the real value anywhere it is not being swapped in (raw, percent-encoded, JSON-escaped, hex or base64, in any case), is blocked and recorded as tkbox.secret.leak. The value is redacted from response headers, bodies and trailers, and requests to bound hosts ask for unencoded responses so redaction can see them.
The checks also run before the box starts. Credential files (.env, *.pem, *.key, .netrc, .npmrc, .ssh/, .aws/ and more) are never copied into the workspace, ignored or not. A workspace file that holds a secret's value refuses the box. A command line, environment value or stdin that holds a value is refused, and every record shows the secret's name, never the value.
There are limits, and the documentation states them. On HTTPS hosts Box does not read, it sees only the host, port and server name, so a placeholder sent there is not detected; only the placeholder can leave, never the value, and inspect: all reads every allowed host at the cost of breaking certificate pinning. A bound host that echoes the credential back in an encoding Box does not know hands it to the guest, so bind secrets only to hosts that do not reflect request headers.
The command door
Only commands started from the host run through the guest agent: tkbox exec, the SDK, the MCP server and the GitHub Action. Each is decided by the signer before it starts, as a Bash decision with the shell-quoted command line, working directory, environment and a digest of its stdin, and its exit status is recorded after. The signer's own policy packs apply, so piping a downloaded script into an interpreter unread is refused.
A per-command environment may not set names that change what runs without showing in the command line: LD_PRELOAD, PATH, GIT_*, PYTHON*, NODE_* and many others. Processes that a command starts itself, such as the shell commands an agent runs, are not decided one by one. They show up where they cross another door: a connection, a secret, a file change.
The code-changes door

Figure 4. From the VM's writable disk to a file in your checkout, and then to the job's bundle.
When the box stops, the guest computes a git-format patch of /workspace from the writable layer alone, and the host saves it. tkbox apply works only on a stopped box, from that saved patch, before export, so the patch cannot change while it is decided. Then, in order:
- Read the patch strictly. Every path is validated: nothing outside the checkout, nothing in
.gitor the checkout's hooks directory, no copy or rename sections, no changes to files the checkout ignores. - Hold back sensitive files. Changes to
.github/workflows/**,.gitignore,.gitattributes,tkbox.yaml,.envrc,.husky/**,.devcontainer/**, credential files and the like need--allow-sensitive, because they steer the next run or the host's automation. - Make git agree. Git, in an empty repository, must read the patch with the same paths, binary flags, creations, deletions and modes as Box's own parser, with nothing on stderr.
- Build the result in a scratch copy.
git applynever runs on your checkout, and the scratch git runs with no hooks, no filters and no user or system configuration. - Ask the signer about every file before anything is written: one
Writedecision per file, with its path, content hash, mode and the patch's digest. If any file is refused, nothing is applied. - Check again, then write. Each result must still be what the signer decided, and the checkout must be unchanged since the decisions. Box then writes each file itself: a temporary file opened with
O_EXCLandO_NOFOLLOW, then a rename.
Model calls
Model APIs are egress like any other. A model host comes from a preset or an allow entry, and its key is bound to it as a secret, so the agent can call api.anthropic.com while holding only a placeholder for ANTHROPIC_API_KEY. Each request to a model host is recorded as tkbox.model.call with its host, path, status and size.
One run per box, and failing closed

Figure 5. The life of a box's Tracekit run, and what happens when recording fails.
A box's run opens when the box is created, with its image, allowlist, limits and environment recorded once. Commands and connections are decided before they happen and completed after. Stopping a box closes egress the moment the stop begins, whatever started it, and keeps the run open so that tkbox apply can record its file decisions in the same run. tkbox export closes the run and writes the bundle and the signer's trust file.
Box fails closed everywhere:
- No signer, no daemon.
tkbox daemonexits at start when no signer answers. The--unrecordedflag exists for debugging Box itself, and every box it starts says so. - A signer that stops answering refuses every later command, connection and file write.
- A record that cannot be kept stops the box. Secret uses and leaks, model calls, approvals and containment events are never dropped silently. If one cannot be queued or the signer refuses it, the box is stopped and the loss is recorded in the run as
tkbox.gap. An approval that cannot be recorded lets nothing through. - The signer can stop a box. A hold on the run pauses the VM and a quarantine kills it, within two seconds early in a box's life and about fifteen seconds later on.
Verifying a bundle
tkbox export demo -o demo.tkb writes the bundle and, next to it, demo.trust.json, which pins the signer. Anyone with both can check the run offline, with no access to your host:
pip install tracekit-ai==1.4.2
tracekit verify demo.tkb --trust demo.trust.json # Integrity: VERIFIED.
On the demo run, that prints Integrity: VERIFIED. over 102 signed records, with Assurance: dev, because that signer has no witness.
A verified bundle proves that the record is intact and was signed by the signer the trust file pins. How much that is worth depends on the signer's setup, and the report says so. isolation: same-user means another process of the same user could have reached the key; separate-user means it could not. Assurance: dev means no witness cosigned the signer's log; local means only witnesses you run did; witnessed means enough independent ones did. Pin the trust file you expect, by committing it or keeping it outside CI, rather than trusting the copy shipped next to a bundle.
How we tested it
- Every boundary has an interface and a fake: the VM launcher, the guest agent, the image builder, the egress decider and the Tracekit client. Packages are tested without KVM, and an end-to-end suite runs real boxes on KVM in CI. All 13 end-to-end checks pass, including the fail-closed ones.
- Fuzzing covers the proxy's request forwarding, every address form we could think of (decimal, octal, hex, IPv6, IPv4-mapped), the redacting reader and the secret flag parser.
- Review findings. A first review found 0 critical, 1 high, 4 medium and 14 low issues; fuzzing produced nine findings, all fixed (eight from the campaign that wrote the fuzz targets, one that the repository's own
make fuzzrun found later); the final launch check found no high or medium issue open. Every fix landed with a test that fails without it. Some of the findings, in the words of their commits: refuse a box whose workspace file holds a secret's value; redact a body read in pieces exactly as when it is read whole; refuse a secret value split over words that need quoting; show no part of a nested secret value in a refusal or an event.
Three real runs
- The demo box (the video at the top): pip allowed, example.com held for a person, the metadata address refused, the token only a placeholder, the change returned as a patch. Its bundle holds 102 signed records and verifies offline: "Integrity: VERIFIED."
- A production-mode signer. Tracekit 1.4.2 installed in system mode, running as its own user, with the daemon as another user. Every door behaved the same, and the bundle verified with "isolation: separate-user", the signer's own statement that the agent's side could not reach its key.
- A stranger's first try. A brand-new repository, one step of the GitHub Action on a free runner: the box ran, the change was applied, and the bundle verified, in 61 seconds.
What Box can and cannot establish
It establishes, for one job: which commands were started in the box from the host and how each ended; which connections were allowed, approved or refused; where each secret was used, and every leak it caught; which model calls were made; which files were written to your checkout, each allowed by the signer; and that the record of all of this has not been changed since it was signed.
It cannot establish:
- What happened inside the VM without crossing a door. Reading files in
/workspace, computing, and the processes the agent starts itself are not in the record. - What an allowed host received. Box decides who the agent may talk to, not what it says.
- That the agent's work is correct or its intent benign. A verified bundle proves the record, not the result.
- Anything about a host you do not trust. Root on the host can read secrets, change the daemon or approve anything, and an operator who controls the signer can sign any history unless a witness they cannot rewrite cosigns the log.
- Safety for code you run outside the box.
tkbox applywrites the agent's changes to your checkout. Building or testing them on the host runs them outside the box.
Limits of v0
Every limit is listed, with what Box says when you hit it and what to do, in the repository's limits page. The ones that matter most when deciding whether to use it:
- No Firecracker jailer. Firecracker runs as the daemon's user with its default seccomp filter, but without the jailer's chroot, namespaces and cgroups. A guest that escaped Firecracker through a VMM or KVM bug would have that user's privileges, so run the daemon as a dedicated unprivileged user.
- One host. The signer must be on the daemon's host, reached over a Unix socket. Hosts need Linux with
/dev/kvm; on a Mac, use the Lima VM in the development guide. - Public images only, from any HTTPS registry. No snapshots: a stopped box cannot be resumed.
- The host's public IP on a cloud VM is NAT'd and on no interface, so Box cannot see it. Pass it with
tkbox daemon --egress-block. - Export with a system-mode signer. The signer's store belongs to its own user, and Tracekit 1.4.2 has no export call, so
tkbox exportcan't run as the daemon's user. Export as the signer's user instead (sudo -u tracekit-signer tracekit export --v2 …). - The GitHub Action makes a dev signer and its trust file in the same job, so its bundles have dev assurance only: they show the record was not changed after the job, not that the runner was honest.
Where it sits next to Tracekit and Causeway
Tracekit is our flight recorder for agents: a policy gate and a signed, tamper-evident record of every tool call, verifiable offline by anyone. It answers can we prove what the agent did? when the record is taken inside the agent's harness.
Tracekit Box moves the same signer to the edge of a VM. Whatever the agent runs, it can only leave through a door the host controls, so the record no longer depends on the agent's harness reporting honestly. Causeway answers the next question, why did it happen?, by replaying the agents without each suspect input. Box's bundles are the evidence that kind of investigation starts from.
Box is built on Firecracker, the micro-VM monitor AWS built to run Lambda and Fargate. Isolating untrusted code in a VM or a user-space kernel such as gVisor is well established. Box's emphasis is on what happens at the edge of that isolation: each crossing decided by a separate signer, secrets kept out of the VM entirely, and a record of the job that anyone can verify.
Guidance for teams running agents
- Give each job its own VM. A container shares the host's kernel with the agent.
- Allow what the job needs and nothing more. In CI nobody answers approvals, so a missing entry simply fails.
- Bind secrets instead of injecting them. Use the narrowest token that works, bound to exact hosts that do not echo request headers back.
- Treat workflow files as code that runs with your permissions. Read those changes before passing
--allow-sensitive. - Test the agent's patch where it can do no harm. The repository's example workflow runs the box with a read-only token, tests the patch in a second job with no secrets, and opens the pull request from a third job that never runs repository code.
- Run the signer as a separate user, add a witness, and pin the trust file yourself.
- On a cloud VM, pass the host's public IP with
--egress-block, and bind host services to loopback. - Stop the daemon with SIGTERM, never SIGKILL. A crash leaves runs open, and those boxes can no longer be exported.
Use it, or work with us
Tracekit Box is open source under the MIT licence (github.com/Cygnux-Labs/Tracekit-box). There is no release yet: build it from source on a Linux host with KVM.
git clone https://github.com/Cygnux-Labs/tracekit-box && cd tracekit-box
make linux # then install bin/tkbox-*, start a signer and tkbox daemon (see the README)
tkbox run --image python:3.12-slim --allow pypi --workspace . --name demo \
--secret GH=env:GH_TOKEN@api.github.com
tkbox exec demo -- pip install -q requests # allowed: on the allowlist
tkbox exec demo -- sh -c 'echo "$GH"' # a placeholder, not your token
tkbox stop demo && tkbox export demo -o demo.tkb
The README walks through the install, a dev signer and the full demo. To run an agent in a box from GitHub Actions:
- uses: Cygnux-Labs/tracekit-box/action@main
with:
agent: claude-code
prompt: "Make the failing test in test/parser.test.js pass. Do not change the test."
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
The API key reaches only the daemon on the runner. Inside the box, Claude Code sees a placeholder that works only for api.anthropic.com.
For teams putting agents into CI or production, we also review how agents are isolated and recorded: what they can reach, where their keys live, what their records prove, and how to close the gaps, with every finding reproducible. Book a call or write to us with what your agents do and what they can touch.
What isn't built yet
- The Firecracker jailer, for chroot, namespaces and cgroups around each VM.
- A remote signer, so the signer can live on another host.
- Private registries, snapshots and restore.
- A Kubernetes RuntimeClass, and a gVisor fallback for hosts without KVM.
- More egress protocols: SSH and UDP are refused today.
Tracekit Box's code, documentation, limits and threat model are available on GitHub.
