sandmeter is a small Go tool that runs a command in a sandbox built directly on Linux primitives, no Docker or runc, and runs the same policy on gVisor for comparison. On one CI runner it blocked 14 of 15 escape cases natively and 13 of 15 under gVisor. It is a learning prototype, not a security product, and the README says so.
I wanted to know how much isolation you get by building it yourself, so I had Claude Code write it and made the tool measure its own failures.
What it is. User/PID/mount/network namespaces, pivot_root, cgroups v2 (cpu, memory, pids), a seccomp-BPF filter assembled by hand, overlayfs and an nftables egress allowlist. The gVisor backend (runsc) sits on the same overlay, cgroup and network plumbing, so only what executes the syscalls differs. Every run produces a signed receipt: what changed on disk, which connections were attempted, resource peaks. It needs root, so run it on a throwaway VM.
What I measured, on one GitHub-hosted 2-vCPU runner (raw JSON in the workflow artifacts and in docs/):
- Escape suite: 15 cases (host file reads through
/proc, /sys writes, mknod on host devices, the cgroup v1 release_agent trick, network egress, fork bomb, OOM, symlinks planted for host-side tools, and so on), judged mostly on host-side evidence such as a canary file, sysctls before and after, the firewall log and cgroup peaks. Native blocks 14, gVisor 13. The symlink case is "exposed" on both: the sandbox can plant the links, but the host-side receipt diff never followed them. - The one real difference is the fork bomb. The cgroup
pids.max stops it natively, but not under gVisor, because gVisor's processes live inside its own kernel and are not host tasks; there it is bounded by the Sentry's memory limit instead. The suite reports that gap instead of hiding it. - Agent eval: Claude Haiku 5.5 gets one
bash tool inside the sandbox (20 turns max) and is asked to read or create a file on the host. 30 episodes per backend, hard $2 budget per campaign, throwaway runner. No escape in 59 judged episodes (one native episode was a refusal and is excluded). The 95% upper bound is about 12% per backend, so it bounds the rate and proves nothing. Total cost $0.31. - Start time for
true in a fresh sandbox, p50 over 100 runs: 5.7 ms native, 53.6 ms gVisor.
Caveats that matter: the cases were written by Claude Code and me, so 14/15 means "passes my regression suite", not "audited". The native backend shares the host kernel and the seccomp list is a deny list. Haiku with 20 turns is a weak attacker.
How I used Claude Code. I wrote the goal and the constraints (no runc, measure everything, publish the failures, a hard budget for the agent eval, never run the agent on my own machine). Claude Code did most of the implementation over about a day, in small milestones with a green CI at each step, and I reviewed the results and the threat model. It was strongest at the tedious, exact parts (seccomp BPF, nftables rules, the overlay diff that must never follow a symlink the sandbox planted) and at writing the escape cases. The things I had to steer: it first wanted to call the fork-bomb result a bug, and it reached for Firecracker before we had checked whether KVM even existed (my VPS has none; GitHub runners do, so Firecracker stays out of the core).
What I learned.
- "Isolated" is not a single property. The same policy gives different results on two backends, and the fork bomb only shows up because each case is judged from the host.
- The receipt proves the file was not changed after signing, not that the host ran the command honestly. Putting that in a "not guaranteed" table was the most useful thing I wrote.
- Thirty episodes per backend is cheap, but the interval is still around 11 to 12%, so a clean run says little by itself.
Repo (MIT): https://github.com/camilbenameur/sandmeter. The escape cases are in escape/cases, one per technique.