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
.