A few weeks ago I gave my coding agent permission to run shell commands, watched it run cargo test, and felt good about myself. Then it hit me what I had actually done. "Let the model run shell commands" is just a friendly way of saying "let a program I do not fully control execute arbitrary code on my laptop." That is the textbook definition of remote code execution. I had built myself an RCE machine and handed it the keys.
So I went looking for a way to box it in. This is what I tried, why Docker was the wrong tool, and what I ended up building instead.
The obvious answer, and why it is wrong
"Put it in a container" is everyone's first instinct, and it is not crazy. But Docker is the wrong shape for this specific job:
- Cold start. An agent does not run one command, it runs hundreds of short ones. A 200ms+ spin-up per command turns a snappy session into a slideshow.
-
It needs a daemon and root, and on macOS a whole Linux VM. That is a lot of moving parts to babysit just to run
lssafely. - It is the wrong granularity. A container isolates a whole environment. What I actually wanted was to confine a single process, per command, for almost no cost.
The thing is, every major OS already ships exactly that primitive. We just rarely reach for it.
The kernel already does this
Each platform has a built-in way to confine a single process at the kernel level, no daemon required:
-
macOS: Seatbelt. The same
sandbox_initmechanism Chrome and friends use. You hand it a profile describing what the process may touch, and the kernel enforces it. - Linux: Landlock + seccomp. Landlock (an LSM in mainline since 5.13) restricts filesystem access; seccomp-bpf filters which syscalls the process can even make.
- Windows: AppContainer + a Job Object. Capability-based confinement plus resource limits.
The catch is that these are three completely different APIs with three different mental models, and two of them are barely documented. Hiding that behind one interface ("confine this command to this directory, deny the network") was most of the work. The payoff is that the confinement is enforced by the kernel rather than by asking the model nicely, and cold start stays under 5ms because there is no container to build.
In the tool I built (Skarn), it looks like this:
\bash
skarn run --net deny -- cargo test
\\
That runs the command locked to the project directory with network egress denied. If the model decides to curl your secrets somewhere or rm -rf a path outside the repo, the syscall fails. Not because of a policy prompt, but because the kernel said no.
The harder problem: running code the model wrote
Sandboxing shell commands is the easy half. I also wanted the agent to orchestrate tools by writing a short script, which keeps huge tool schemas out of the context window (that is another post). But running model-generated code is the same RCE problem wearing a nicer hat.
A JavaScript isolate alone is not a security boundary. People escape them. So I did not rely on it being one. The script runs in a QuickJS isolate, and that isolate runs inside a worker process that sandboxes itself (deny network, no workspace writes) before it ever loads the model's code.
That gives two independent walls:
-
The isolate. Static validation rejects
eval,Function,require,import, andprocess, and execution is bounded by memory, stack, wall-clock, and output-size limits. - The kernel sandbox underneath it. Even a full isolate escape lands in a process that still cannot reach the network or write outside the workspace.
You have to get through both, and the outer one is enforced by the OS. The inner layer is for ergonomics, the outer layer is for actually stopping you.
Being honest about the threat model
A security post that only lists wins is marketing. So: this runs untrusted, model-generated code on purpose, and the most useful thing anyone can do is try to break it. The hand-written unsafe FFI into those kernel APIs is where I am least confident, because the surfaces are sparsely documented. There are things it does not defend against, which is why the repo has a SECURITY.md that says so plainly. If you find a hole, I would rather hear about that than hear that it is cool.
The other half, briefly
The same gateway also cuts the agent's token usage by compressing noisy shell output (70-90% fewer tokens, errors and warnings always kept) and by the schema-avoidance trick above. That is the part that saves money rather than saving your filesystem, and it is a separate story.
If you want to read the code, kick the tires, or attack the sandbox, it is one Rust binary here: https://github.com/Rani367/Skarn
It is early, MIT or Apache-2.0, and review of the sandbox crate is the most welcome thing you could send.













