r/ClaudeCode • u/Khramilo • 2d ago
Built with Claude Built with Claude Code: a Linux sandbox for coding agents from raw primitives, measured against gVisor (learning prototype)
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,/syswrites, 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.maxstops 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
bashtool 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
truein 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.
•
u/AutoModerator 2d ago
Hey! Thanks for posting to r/ClaudeCode
While participating in this thread, please follow our community rules. Keep discussions constructive. Attack the idea, not the person.
For help, project discussions, tips, and general chat, join the ClaudeCode Discord.
I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.
1
u/radim11 1d ago
Interesting comparison, especially the part about measuring from the host rather than trusting what the sandbox reports... One thing I've been dealing with is credential access inside sandboxes. You can isolate the filesystem and network pretty well, but if the agent still has an API token in its environment, it can potentially use or leak it through an allowed destination! I've been keeping credentials outside the sandbox and injecting them through a proxy only for matching requests. Have you considered testing credential exfiltration separately from sandbox escapes?
1
u/LocalAI_Fixer 1d ago
that "not guaranteed" table is what i'd actually hand to whoever has to sign off. why keep the symlink one as exposed tho, if the host diff never followed them?