Run AI Agents in Lightweight Sandboxes

Currently, “AI agents” like Claude Code are all the rage for software development. These programs facilitate feeding context into LLMs directly from your codebase and environment by empowering the LLM to read/write files and run arbitrary code on your system.

If you’re at all security conscious, the above should make you very uncomfortable. An intentionally non-deterministic model running arbitrary code and reading arbitrary files? No thanks. What’s worse is programs like Claude Code are not even open source. As a rule I go to lengths to avoid proprietary software, but I decided to cave on Claude Code just to see what all the fuss is about.

But there’s no way I’m going to let it loose in my $HOME.

I’ve seen some talk online of running things inside Docker, but I feel like Docker is a pretty heavy solution when all we need is a sandbox. Fortunately there’s a much more lightweight solution called bubblewrap.

Installing Claude Code

First of all we need to install Claude Code. The official installation method is to use npm, but I don’t want to install this somewhere on my $PATH, so instead you can simply install it at a different prefix:

1npm install --prefix ~/claude/ -g @anthropic-ai/claude-code

Now it’s safely away in it’s own root and can’t be accidentally executed outside of a sandbox.

Using Bubblewrap

By default running bwrap --new-session prog will run prog isolated from the rest of your system, so much so it probably won’t be able to do anything useful. What you can then do is selectively enable the things it needs access to. For running Claude Code I have come up with the following:

 1#!/usr/bin/env bash
 2
 3set -euo pipefail
 4
 5bwrap --ro-bind /usr /usr \
 6      --dir /tmp \
 7      --dir /var \
 8      --symlink ../tmp var/tmp \
 9      --proc /proc \
10      --dev /dev \
11      --ro-bind /etc /etc \
12      --symlink usr/lib /lib \
13      --symlink usr/lib64 /lib64 \
14      --symlink usr/bin /bin \
15      --symlink usr/sbin /sbin \
16      --unshare-all \
17      --share-net \
18      --die-with-parent \
19      --new-session \
20      --dir /run/user/$(id -u) \
21      --setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \
22      --setenv PS1 "bwrap-demo$ " \
23      --bind $HOME/claude /claude \
24      --ro-bind $HOME/.local /claude/.local \
25      --setenv HOME /claude \
26      --setenv PATH /claude/bin:/claude/.local/bin:$PATH \
27      --bind $(pwd) /work \
28      --chdir /work \
29      claude $@

This gives the process read-only access to bits of my system like /usr/bin etc. (so it can run commands), network access, read/write access to its own home directory at $HOME/claude (which if you recall is also where I installed claude using npm), sets some env vars like $PATH, and finally gives read/write access to the current directory. The intention is you would run this from the root of a project directory, like you would normally.

Conclusion

I find it a lot more pleasing to be able to write a simple script like this to sandbox programs without having to write pointless Dockerfiles everywhere. The way Docker works isn’t magic and quite often you don’t really need everything it has to offer. Bubblewrap is a great tool to have in the toolbox. In this case the $HOME/claude directory is analogous to a persistent Docker volume and our use of bwrap equivalent to a docker run --rm.