TL;DR#

A small guide on how to run AI agents in a sandbox… natively!

Context#

I first heard about running AI agents in a sandbox while reading the article Running Claude Code dangerously (safely). I then wanted to see if there were alternatives to using Vagrant, and the most interesting ones (Docker Sandbox and Matchlock) are based on microVMs.

The downside of these tools is that you have to somehow reproduce your development environment inside them. Why does the agent need access to our development environment? Once they start to change files, agents can also validate their work by running unit/integration/end-to-end tests. In a Python project, this means running pytest; in Go, go test; in Node.js, npm test (or whichever tool you use). In a DevOps project, this could mean running Docker, Terraform, or Kustomize. By the way, if you find it difficult to track those tools per project, and you find Nix too intimidating, I recommend you check out Mise, which is really great for this.

Matchlock does support booting custom OCI images, which means we could reproduce our development environment by redoing our setup in a Dockerfile. But after a lot of trial and error, I gave up and realized… I already have my development environment set up on my machine thanks to Nix! Why can’t I reuse it to run agents in a sandbox?

Guide#

Dedicated non-admin user#

The basic idea is to use a dedicated non-admin user on the host machine to run our AI agents like Claude Code or Gemini CLI. I’ve mainly tested this technique on macOS, but this should also work on other OSs (on Windows, we may have to use WSL). This non-admin user doesn’t have access to the admin user’s secure files like SSH keys or AWS credentials, so it can’t compromise our host machine.

Package management#

Nix (or other tools like Homebrew or your favorite package manager) is then responsible for installing tools globally so that they are available for both your admin user and your AI sandbox user. And given that those agents are configured via dotfiles, one can use dedicated tools like chezmoi or home-manager to store those configurations.

File access#

In theory, these agents support the whole development cycle—from creating feature branches and making code changes to committing and pushing to our Git server, and even opening pull requests. However, I prefer to restrict them to making code changes and retain control over the rest (commit, push, opening PR, …). This is a deliberate choice; you may give agents more responsibilities while still using the technique described here.

To support a workflow where the agents make code changes and the admin user commits and pushes those changes, both users must have access to the same code. On macOS, you can move or clone your repositories into a native shared folder named /Users/Shared.

Network access#

I haven’t tested this yet, but you can also restrict network access, usually via a man-in-the-middle proxy tool like mitmproxy, which allows you to allowlist the websites your agent can access. You could even intercept requests to inject secrets so that the agent doesn’t know about your secrets—as done in Matchlock—which sounds crazy but very powerful at the same time! Don’t forget to set up a firewall rule with Packet Filter on macOS or iptables/nftables on Linux so that the agent can’t escape your beautiful proxy.

Alternatives#

Both Claude Code and Gemini CLI have dedicated sandboxing documentation, so you may want to try their built-in solutions if they fit your needs. However, on macOS, they both rely on the deprecated sandbox-exec tool. This initial limitation—and the desire for a more robust, multi-layered approach—is what led me to explore the custom non-admin user + kernel-level sandboxing technique described in this guide.

Future work#

Some ideas for the future:

  • combine the non-admin user with sandbox-exec which offers another level of security
  • run multiple agents in parallel (Claude Code + Gemini CLI) to bypass the Claude Code token limit per day / week (rtk is great but doesn’t always solve the token limit)
  • restrict network access, either via mitmproxy + PF or sandbox-exec
  • run Docker inside the agent, given that on macOS the Docker daemon runs inside a Linux VM (I’m currently using lima for this)