AI agents now install packages, call model APIs with their operators' keys and change code in repositories that deploy to production. The network, credentials and write access they need are also how they can leak data, steal a key or change a system nobody approved. Sandboxes contain an agent but leave little evidence of what crossed their boundary; tracing records what happened but contains nothing. We present Tracekit Box, an open-source system (MIT, v0) that does both. Each job runs in its own Firecracker micro-VM with no network card. The only ways out are doors on the host, served over vsock: commands, outbound connections, secrets, model calls and the workspace patch. At every door a Tracekit signer the agent cannot reach decides what passes and signs it into one run per box. Names are resolved on the host; private and cloud-metadata addresses are always refused; secrets never enter the VM, which sees placeholders the host proxy swaps for real values only in one header for bound hosts; and changes come back as a patch applied file by file after the signer decides each file. Box fails closed, and each job ends as a .tkb bundle anyone can verify offline. All 13 end-to-end checks pass on a real KVM runner (the test that runs them takes 28.5 s, including image pull and boot); 15 fuzz targets on the trust boundary produced nine findings, all fixed; and the demo run's 102 signed records verify. Box does not yet run Firecracker's jailer, needs a co-located signer, and records processes inside the VM only where they cross a door.
Table 1: What each approach provides for an agent job. ✓ provided, ∘ partial, – not provided. Rows describe the approach as typically deployed.
Domain
Trust
What runs there
Host
trusted
tkbox daemon , the egress proxy, the image builder, the secrets passed in, the user’s checkout
Guest (micro-VM)
untrusted
the agent, everything it runs, and the guest agent tkbox-guest itself
Signer
trusted, separate from the agent
the Tracekit 1.4.2 signer and its signing key, on the same host as the daemon, reached only over a Unix socket
Table 2: Trust domains.
Goal
What stops it
Run code on the host
the KVM boundary and Firecracker’s small device model with its default seccomp filter (no jailer in v0); the daemon parses guest input as untrusted (bounded frames, strict JSON, path checks)
Reach the network directly
the VM has no network card; the only route out is the egress proxy on vsock
Reach internal services or cloud metadata
names resolved once on the host; loopback, private, link-local, metadata, multicast and the host’s own addresses refused; the proxy dials the address it checked, so a name cannot rebind
Exfiltrate to an arbitrary host
the allowlist; any other host waits for a person on the host; TLS server name must equal the requested host; encrypted ClientHellos refused; a non-TLS tunnel only to an entry that names its port
Steal a secret
the value never enters the VM; the proxy swaps it into one bound header for exact bound hosts, blocks the placeholder or value elsewhere on traffic it reads, and redacts the value from responses
Table 3: Attacker goals and what stands in the way (abridged from the project’s security policy).
Figure 2: The life of a job and what each step records in the box’s Tracekit run (bottom row).
Port
Direction
What travels
1024
host → guest
agent RPC: exec , diff , files, shutdown , in framed messages ( [type u8][len u32][payload] , payload at most 1 MiB)
1025
guest → host
egress: raw HTTP proxy protocol from the guest’s forwarder on 127.0.0.1:3128 to the box’s proxy
1026
guest → host
boot config: box id, environment, proxy address, the box’s CA certificate, devices, limits, the host’s clock; served once, then closed
Table 4: The vsock ports: the only channels between guest and host.
Figure 3: The box’s disks. The patch is computed from the workspace’s upper layer, so it contains exactly what the job changed.
Figure 4: Deciding one outbound connection, in the order the proxy checks. A name on the allowlist (or an IP literal) is resolved once on the host and checked before the signer is asked; a name off the list is not resolved until it is allowed, because the lookup itself would carry whatever the guest encoded in the name to that domain’s DNS servers. The proxy dials the address it checked, so a name cannot rebind between the check and the connect, and it checks the TLS ClientHello before it forwards any byte of the tunnel. An allowlisted name skips the person, never the signer.
api.anthropic.com ; api.openai.com ; api.anthropic.com and statsig.anthropic.com
debian , apt-ubuntu
Debian and Ubuntu package mirrors
go-proxy
proxy.golang.org , sum.golang.org (these fetch on the caller’s behalf, so they can relay workspace data)
Table 5: Allow presets.
Figure 5: A secret’s two paths. To its bound host, in its bound header, the proxy swaps the placeholder for the real value and records the use. Anywhere else on traffic the proxy reads, the placeholder (or the value) is blocked and the attempt is recorded.
Figure 6: The tkbox apply pipeline. Nothing is written unless the signer allows every file, and what is written is what was decided.
.npmrc , .netrc , *.pem , *.key and the rest of the credential-file list
Table 6: Files a patch may change only with --allow-sensitive .
Figure 7: A box’s life and its Tracekit run. The run stays open after tkbox stop so that tkbox apply records its per-file Write decisions in the same run; export, removal, quarantine, a failure or daemon shutdown close it.
Door
Kind
Arguments or fields
When
Commands
decide Bash
command (the shell-quoted line, with per-exec env as a leading env K=V ), cwd , env (placeholders shown as tkbox_NAME_… ), stdin ; up-front stdin as stdin_sha256 , stdin_bytes and, if UTF-8 and ≤ 4 KiB, stdin_text
before the process starts; complete with the exit status (and streamed stdin’s digest)
Network
decide WebFetch
url : https://host:443 , http://host:port or tcp://host:port
per connection, before it is dialled
Approvals
observe tkbox.approval (trusted)
{approval_id, target, decision, approver}
synchronously, before any connection uses an approval
at the refusal; at most 20/s per box, then a summary
Secrets
observe tkbox.secret.use
{name, host}
on each swap into a bound header
observe tkbox.secret.leak
{name, host, reason}
on each blocked placeholder or value
Table 7: What each door sends to the signer. Decisions are made before the action and completed after it; observations are signed but not decided. No record ever holds a secret’s value.
Situation
What Box does
No signer at daemon start
no daemon, unless --unrecorded , which warns and records nothing
Signer stops answering
every later decision is refused; nothing runs, nothing connects
Must-keep observe cannot be queued (queue of 4096) or is refused
the box is stopped and the loss is recorded as tkbox.gap
Approval record refused
the connection is refused, the box stopped, the loss in tkbox.gap ; if even that fails, the run is closed with the loss as its reason
A complete is refused or unanswered
counted in tkbox.gap with kind complete ; the box keeps running
Box’s own refusals above 20/s
a summary observation counts the rest
Table 8: Fail-closed rules.
Figure 8: Stopping, exporting and verifying the demo run, cut from the session’s screen recording (lines over 104 columns wrapped). tracekit verify 1.4.2 checks the bundle offline: 102 signed records, a checkpoint at tree size 5,978, Integrity: VERIFIED , Assurance: dev . The coverage matrix is Tracekit’s generic view: it does not know that in a box, the network and file writes have no other way out.
Deployment
Verifier says
Who could forge a record
Dev signer ( tracekit init --dev ), as the daemon’s user
Assurance: dev , same-user
not the agent (it is in the VM), but any process of that user
Signer as its own user on the daemon’s host
separate-user
root, or whoever controls the signer’s user
Plus a witness on another host
above dev
a rewrite after a checkpoint is cosigned would be caught by the witness
GitHub Action
Assurance: dev
the job itself; the trust file shows only that the bundle was not changed after the job
OCI pull and cache, layer flattening, ext4 builds, the workspace image
2,929
1,867
cmd/tkbox
CLI and MCP server
2,192
1,073
internal/tracekit
signer RPC client, run lifecycle, decision gate, export
2,199
1,811
Table 10: Packages. Lines of non-test and test Go ( wc -l ) at commit 473723b .
Figure 9: Code and test lines by package. The proxy ( egress ) is tested about line for line.
Command
What it does
tkbox daemon
run the daemon: boxes, egress proxies, the API socket
tkbox run
check the settings, then create and start a box
tkbox exec <box> -- argv
run a command in a box (decided first)
tkbox ls / stop / rm
list; stop and save the patch; delete
tkbox diff <box>
print the changes as a git-format patch
tkbox apply <box>
apply a stopped box’s patch, file by file through the signer
Table 11: The tkbox command line.
Figure 10: The three-job shape of the example workflow: only the box runs agent code while a model key is present, the job that runs the agent’s code holds no secrets, and the job that holds the write token runs no repository code.
Check
Door
What it does and what it requires
Time (s)
exec
commands
runs a command through the guest agent, decided by the signer
0.06
unread-stdin-interrupt
commands
a command with 2 MB of stdin it never reads still stops when the client sends Ctrl-C
2.05
unread-stdin-kill
commands
…and when the client is killed outright
2.11
signer-deny
commands
a command that would kill the signer ( pkill tracekitd ) is denied by the signer’s policy before it starts: exit 126, rule TK-D007
0.01
workspace-excludes-ignored
workspace
the checkout’s .env and .git are not in /workspace ; its tracked files are
0.01
no-network-card
network
/proc/net/dev in the guest lists no interface but lo
0.01
Table 12: The end-to-end checks of TestBox in CI run #16. All passed. Times are as go test -v printed them.
Figure 11: Duration of each end-to-end check in CI run #16. The long bars are deliberate waits (a client giving up on a held host; commands that must be seen running before they are interrupted), a real package install, and a full stop–apply–export–verify.
Figure 12: The demo run, as recorded. Each panel is cut from the screen recording of the session that produced the bundle in table 13 ; lines longer than 104 columns are wrapped, and nothing else is changed except the redaction in (c). Figure 8 shows the same session’s stop, export and verify.
Record
Count
policy.decision (Bash and WebFetch)
46
tool.result (43 ok, 3 not ok)
46
input.observed : tkbox.box
3
tkbox.egress (Box’s own refusals)
2
tkbox.secret.use
1
tkbox.approval
1
Table 13: The demo run’s signed record ( runs/by-hand/readme-demo ).
Figure 13: The demo run’s 46 signer decisions. Every WebFetch was flagged by rules TK-F002 and TK-F012 of the signer’s default packs; a flagged call is allowed and carries the finding in its record. The 39 include example.com : the signer decided it first, then Box held it for a person, and when no one approved it Box refused it and recorded tkbox.egress . The metadata address is not among them: Box refused it before any decision.
Target
Package
Property
FuzzAddrForms
egress
any textual form of a blocked address is refused wherever the proxy parses a host; metadata addresses are refused even under AllowPrivate ; block prefixes hold in every form
FuzzHostText
egress
host text accepted as an IP literal is checked as that literal; text accepted as a name is not one inet_aton reads as IPv4
FuzzClientHello
egress
an admitted ClientHello names the CONNECT host and only it, in every reading; exactly one ClientHello is forwarded
FuzzForward
egress
the upstream gets the path and query Box checked and recorded; the guest never sees a secret value a bound host echoed
FuzzRedactReader
egress
a body carrying secret values in any claimed form, split at any read boundary, comes out the same length and with none of them; gzip and deflate too
FuzzReadFrame
proto
any byte stream decodes to bounded frames, allocates at most one bounded payload per frame, and re-encodes exactly
Table 14: The 15 fuzz targets and the property each one checks.
ID
Area
Finding and fix
F1
patch parser
/dev/null on a --- or +++ line without its new file mode or deleted file mode line is a file named dev/null to git, not a creation or deletion; now refused
F2
egress
a response status below 100 had its body relayed without redaction; now a 502
F3
egress
an opaque request target inside a TLS tunnel could carry a swapped secret to another host; now refused
F4
egress
a redaction marker could rebuild a value with the bytes around it; markers can no longer take part in a match
F5
commands
a program name starting with % is a job spec to bash and zsh, so the decided command line would not name what runs; now refused
F6
config
a refused --secret flag could quote part of a pasted value in its error; the source is now checked first and never quoted
Table 15: Fuzzing findings, all fixed, with the labels used in the repository’s tests and commits.
Pass
Examples of fixes
review-1
a CONNECT to 443 under an entry naming 443 recorded as tcp:// ; base64 values that are JSON-escaped or percent-encoded redacted
re-review
gaps in apply, stdin, the observer, events and MCP limits
verify-3, 3b
documentation claims corrected; apply stated plainly to have no rollback
verify-5
more per-exec env names that run code refused; apply writes into a global core.hooksPath refused; an approval lost after a stopping box wrote its gap record now recorded
final checks
a run not closed under an approval record still in flight; no part of a nested secret value shown in a refusal or event; three guarantees backed only by code given tests
Table 16: Review passes, as labelled in the commit history, with examples of what each fixed.
Item
Result
End-to-end checks on KVM (CI run #16)
13 / 13 pass
TestBox (the 13 checks), including pull and boot
28.5 s
Unit tests; end-to-end test functions
340; 2
Fuzz targets; findings fixed
15; 9
Demo bundle: records; decisions; verifies
102; 46; yes
Assurance of every bundle reported
dev
Table 17: The evaluation at a glance.
Not in v0
Today
Firecracker jailer
Firecracker runs as the daemon’s user with its default seccomp filter, without the jailer’s chroot, namespaces and cgroups; the VMM interface already takes a jailer config
Remote signer
the signer must be on the daemon’s host (Unix socket only)
Private registries
public images only
Snapshots and restore
each box boots from its image; a stopped box cannot be resumed
Kubernetes RuntimeClass
run tkbox daemon on the node or runner directly
gVisor fallback
no /dev/kvm , no daemon
Table 18: Not in v0, and what happens today.
Appendix figures & tables1 asset
Supplementary material from the paper’s appendix.
Appendix
Limit
Default
vCPUs; memory; workspace disk
2; 2 GiB; 4 GiB
Box lifetime; each command
1 h; 30 min
Boxes per daemon; concurrent execs per box
8; 64
Open connections per box
256
Pending approvals per box; new approvals per minute
16; 30
Approval wait
2 min
Appendix
Table 19: Selected defaults (the full list, with each message Box prints, is in the repository’s docs/limits.md ).
Modern AI agents execute real-world side effects through tool calls such as file operations, shell commands, HTTP requests, and database queries. A single unsafe action, including accidental deletion, credential exposure, or data exfiltration, can cause irreversible harm. Existing defenses are incomplete: post-hoc benchmarks measure behavior after execution, static guardrails miss obfuscation and multi-step context, and infrastructure sandboxes constrain where code runs without understanding what an action means. We present AgentTrust, a runtime safety layer that intercepts agent tool calls before execution and returns a structured verdict: allow, warn, block, or review. AgentTrust combines a shell deobfuscation normalizer, SafeFix suggestions for safer alternatives, RiskChain detection for multi-step attack chains, and a cache-aware LLM-as-Judge for ambiguous inputs. We release a 300-scenario benchmark across six risk categories and an additional 630 independently constructed real-world adversarial scenarios. On the internal benchmark, the production-only ruleset achieves 95.0% verdict accuracy and 73.7% risk-level accuracy at low-millisecond end-to-end latency. On the 630-scenario benchmark, evaluated under a patched ruleset and not claimed as zero-shot, AgentTrust achieves 96.7% verdict accuracy, including about 93% on shell-obfuscated payloads. AgentTrust is released under the AGPL-3.0 license and provides a Model Context Protocol server for MCP-compatible agents.
AI coding agents are untrusted system components, yet they require autonomy on the developer machines they run on. This contradiction is a security problem. Sandboxes provide an isolated environment, but for local macOS development, the existing local, open-source options for AI coding agents are few in number and cumbersome to use. I conducted a formative online user survey which indicates that fewer than 40% of AI coding agent users run their agents in a sandbox and identifies the top usability barriers hindering AI coding agent sandbox adoption. These findings are used to develop SideKernel: an open-source, local, microVM-based macOS sandbox for AI coding agents designed for usability. To evaluate SideKernel, I compiled a list of sandboxes available on the market and filtered it against five inclusion criteria. Then I performed a comparative analysis between SideKernel and the sandboxes that satisfy these criteria, across 23 capability tests derived from the usability barriers revealed by the user survey. I discovered that only a few sandboxes are similar to SideKernel, and that among those, Docker Sandboxes and SideKernel score highest on capability features related to usability. A secondary contribution of this paper is a survey of the existing solution space for local, open-source, microVM-based macOS sandboxes for AI coding agents.
AI agents combine language models with external data and tools that can modify files, call APIs, or execute code. Security failures can arise when adversarial content changes an agent's tool use or when the surrounding software contains vulnerabilities such as path traversal or command injection. We study authorized white-box pre-deployment auditing, where the auditor has access to the target repository and a controlled runtime, but successful attacks must still act through the task-defined attacker interface and be confirmed by an external verifier. We present AgentXploit, a two-role auditing system that separates repository-level attack-path discovery from runtime exploitation. The Analyzer Agent traces attacker-controlled inputs to sensitive operations and records code-supported candidate attack paths; the Exploiter Agent turns these paths into concrete attacks and revises them using runtime feedback. We also introduce AgentXploit-Bench, containing 72 reproducible vulnerabilities across 12 open-source AI-agent systems and frameworks. Across three runs, AgentXploit reaches 59.3% end-to-end success, compared with 38.4% for Codex. Under a token-budget-matched comparison, Codex reaches 46.3%. On AgentDojo, where injection points are provided, the Exploiter Agent reaches 79.2% attack success versus 52.7% for AgentVigil. These results highlight repository discovery and runtime exploitation as distinct challenges in end-to-end agent security auditing.
Weida Liang, Shi Qiu, Zhun Wang +6
National University of Singapore · University of North Carolina at Chapel Hill · University of California, Berkeley +2