A detailed writeup of Claude Code constrained by Bubblewrap.

An AI agent that can edit files can also delete them. Here’s a detailed explanation of how I set boundaries while still keeping Claude powerful.

When you let an AI assistant run commands on your computer, you face a problem: the assistant needs enough access to help you, but you don’t want it wandering through your entire system, reading your .env files, scanning your photos. Last week I wrote about how you can use Bubblewrap to prevent agents from accessing your files. There were some interesting comments on HackerNews that inspired me to do some further experimentation and explanation of my config.

I wanted to write a more detailed summary about this config for anyone who is going to try and incorporate bubblewrap into your workflow. I also want to make it insanely easy for you to get started with your bubble wrapping. To that end, I have a couple of Git Repositories you can clone to get started.

If you want to get started with bubblewrap+claude, you can use one of my sample scripts. Btw- I also created versions for fire jail & Apple’s “Containers”

https://github.com/CaptainMcCrank/SandboxedClaudeCode

The bubblewrap script passes all arguments through to Claude via “$@”. Just append your arguments after the script:

./bubblewrap_claude.sh --dangerously-skip-permissions "ruminate on the nature of life"

Don’t trust strangers on the Internet. Here is a git repository of tests you can use to prove if the containers work. read them and understand them. I have exposition below that explains each test in detail. They will help you understand how to execute the tests and validate if the controls work.

https://github.com/CaptainMcCrank/BlogCode/tree/main/BubblewrapTests

The approach above will give your bubblewrap container access to a file system structure like the following:

I welcome collaboration! Please file git issues against my code if you think you have a better approach!


The Complete Command

Here’s the full Bubblewrap command. Save this as a script (e.g., sandboxed-claude.sh), make it executable with chmod +x sandboxed-claude.sh, then run it from any project directory.

#!/usr/bin/env bash

# Optional paths - only bind if they exist
OPTIONAL_BINDS=""
[ -d "$HOME/.nvm" ] && OPTIONAL_BINDS="$OPTIONAL_BINDS --ro-bind $HOME/.nvm $HOME/.nvm"
[ -d "$HOME/.config/git" ] && OPTIONAL_BINDS="$OPTIONAL_BINDS --ro-bind $HOME/.config/git $HOME/.config/git"

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --ro-bind /etc/resolv.conf /etc/resolv.conf \
  --ro-bind /etc/hosts /etc/hosts \
  --ro-bind /etc/ssl /etc/ssl \
  --ro-bind /etc/passwd /etc/passwd \
  --ro-bind /etc/group /etc/group \
  --ro-bind "$HOME/.ssh/known_hosts" "$HOME/.ssh/known_hosts" \
  --bind "$(dirname $SSH_AUTH_SOCK)" "$(dirname $SSH_AUTH_SOCK)" \
  --ro-bind "$HOME/.gitconfig" "$HOME/.gitconfig" \
  $OPTIONAL_BINDS \
  --ro-bind "$HOME/.local" "$HOME/.local" \
  --bind "$HOME/.npm" "$HOME/.npm" \
  --bind "$HOME/.claude" "$HOME/.claude" \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --setenv HOME "$HOME" \
  --setenv USER "$USER" \
  --setenv SSH_AUTH_SOCK "$SSH_AUTH_SOCK" \
  --share-net \
  --unshare-pid \
  --die-with-parent \
  --chdir "$PWD" \
  "$(which claude)" "$@"

This looks complex. I promise it’s not. The only weirdness is at the beginning, where the script checks for optional paths like .nvm and .config/git before binding them. Not everyone uses nvm for Node.js management, and git’s config directory location varies. If you use other version managers (like fnmasdf, or volta), add similar conditional binds for their directories.

The rest of this post explains what each piece does and why I included it.


The System Tools

Your computer stores its programs in folders like /usr/lib, and /bin. These folders contain thousands of tools: file editors, network utilities, programming languages, and more.

I give the AI read-only access to these folders. “Read-only” means the AI can use these tools but cannot change them. The AI can run git to manage code. The AI can run node to execute JavaScript. But the AI cannot replace these programs with different versions or delete them.

Without these folders, every command fails with “command not found.” The sandbox contains no programs to run.

I also share a few files from /etc, your computer’s configuration folder:

  • /etc/resolv.conf: Without this, DNS lookups fail. The AI cannot translate “github.com” into an IP address, so git clone and npm install break.
  • /etc/ssl: Without this, HTTPS connections fail. The AI cannot verify that a server is who it claims to be.
  • /etc/passwd and /etc/group: Without these, programs display raw numeric IDs instead of usernames. Git commits show “1000” instead of “patrick.”

Your Personal Files

You keep important files in your home folder. Git needs your .gitconfig file to know your name and email. Node.js lives in your .nvm folder.

I share these files as read-only. The AI can use your git identity to make commits. But the AI cannot change your git settings or modify your configuration files.

SSH Access Without Exposing Your Keys

SSH keys prove your identity to remote servers. Exposing them directly to the sandbox creates risk—the AI could read your private key files. I use a safer approach: SSH agent forwarding.

The SSH agent runs outside the sandbox on your host machine. It holds your decrypted keys in memory. Programs inside the sandbox can ask the agent to sign requests, but they never see the actual key material.

Here’s how to set it up:

Step 1: Start the SSH agent (if not already running)

Most Linux desktop environments start the agent automatically. Check if yours is running:

echo $SSH_AUTH_SOCK

If this prints a path like /run/user/1000/keyring/ssh, your agent is running and you’re set.

Important: If SSH_AUTH_SOCK is empty, you can start an agent manually with eval "$(ssh-agent -s)". However, manually started agents create sockets under /tmp (e.g., /tmp/ssh-XXXXX/agent.1234). This conflicts with our sandbox’s --tmpfs /tmp mount, which creates an isolated /tmp that hides the host’s socket.

If you must use a manually started agent, either:

  1. Start the agent with a custom socket location: ssh-agent -a /run/user/$(id -u)/ssh-agent.sock and export SSH_AUTH_SOCK accordingly
  2. Or move the --tmpfs /tmp line in the script to appear before the --bind "$(dirname $SSH_AUTH_SOCK)" line (bind mounts take precedence over earlier tmpfs mounts for their specific paths)

For simplicity, I’d recommend using your desktop environment’s built-in agent when possible.

Step 2: Add your key to the agent

ssh-add ~/.ssh/id_ed25519

Replace id_ed25519 with your key’s filename. The agent prompts for your passphrase once, then holds the decrypted key in memory.

Step 3 (Optional but recommended): Require confirmation for each use

ssh-add -c ~/.ssh/id_ed25519

The -c flag tells the agent to ask for confirmation every time something tries to use the key. A dialog box appears on your screen: “Allow use of key?” You must click confirm. The AI cannot bypass this—the prompt happens outside the sandbox.

What this buys you:

ApproachAI can read private key?AI can use key silently?
Direct ~/.ssh bindingYesYes
SSH agentNoYes
SSH agent with -c flagNoNo

The sandbox script binds only ~/.ssh/known_hosts (so SSH can verify server identities) and the agent socket (so SSH can request signatures). Your private key files stay outside the sandbox entirely.


The Working Directory

Your goal is to develop software within a specific project folder. The AI needs write access to that folder to create files, modify code, and delete outdated artifacts.

I bind the current working directory ($PWD) with read-write access. When you run the sandbox script from /home/youruser/projects/my-app, the AI can modify anything inside my-app. When you run it from a different folder, the AI works there instead. The sandbox adapts to wherever you invoke it.

This scoping provides two benefits. First, the AI can do useful work—writing code, running builds, managing files. Second, the AI cannot touch anything outside that folder. Your other projects, your documents, your system files all remain invisible and unreachable.

I also give write access to two other locations outside your project folder.

The .npm folder stores downloaded packages. When the AI runs npm install, npm caches packages here so future installs run faster. Without write access, the AI could still install packages into your project’s node_modules, but every install would re-download everything from scratch. With write access to .npm, the AI can install dependencies at normal speed and benefit from cached packages across all your projects.

The .claude folder stores authentication credentials. This binding deserves special attention. When you first run Claude, you authenticate through your browser. Claude stores a session token in ~/.claude so you don’t repeat this process every time. Without write access to this folder, the sandbox cannot persist your login. You would need to re-authenticate every time you start the sandboxed Claude—a significant usability problem. With write access, you log in once and the session persists across sandbox invocations.


The Fake Temporary Folder

Every program needs a place to store temporary files. Normally, programs use /tmp, a shared folder visible to everything on your computer.

I create a fake /tmp that only the AI can see. When the AI writes temporary files, those files exist only inside the sandbox. When the sandbox closes, those files vanish.

This prevents the AI from leaving debris scattered across your system. It also prevents the AI from reading temporary files that other programs created.


Process Isolation

Your computer runs hundreds of processes at once: your web browser, your music player, system services, and more. Normally, any program can see the full list.

The --unshare-pid flag creates a separate process namespace for the sandbox. When the AI looks at running processes, it sees only itself and the programs it started. Your browser, your email client, your other terminals—all invisible. This prevents the AI from sending signals to other programs or inspecting what they do.

The --die-with-parent flag sets a kill switch: if the parent process dies, the sandbox dies with it. No orphaned AI processes linger after you close your terminal.


The Network Question

Networks present the hardest choice.

An AI with network access can clone repositories, install packages, and fetch documentation. An AI without network access cannot do any of those things.

An AI with network access can also send files or other information to external servers. This represents a real risk when working with private codebases.

I chose to allow network access. Most programming tasks require it. But you should understand: anything the AI can read, the AI can theoretically transmit elsewhere.

A paranoid setup would disable networking entirely. You would pre-download all dependencies, clone all repositories ahead of time, and work offline. This approach works for high-security situations but breaks the normal development workflow.


What This Buys You

The sandbox prevents accidents and limits damage.

The AI cannot read your documents, photos, or browser history—I never shared those folders. The AI cannot install system-wide packages or modify your shell configuration. The AI cannot see your password manager or read your email.

The AI operates in a controlled space: your project folder, plus the tools needed to work on it.


What This Does Not Buy You

The sandbox does not prevent a determined attack through the network. If the AI decided to exfiltrate your code, network access makes that possible.

The sandbox does not prevent damage to your project folder. The AI has full write access there—it can delete everything.

Security involves tradeoffs. I have tried to balance usability and protection. A tighter sandbox would be safer but less easy to use during experimentation & rapid development.

This configuration is useful for everyday development work, protected against casual mistakes but could be vulnerable to sophisticated attacks. For most scrappy programming tasks, this balance should be sufficient.


Testing Your Sandbox

Before trusting your sandbox, verify it works. These commands let you poke at the walls and confirm they hold.

Test 1: Confirm your home directory contents are hidden

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --chdir "$PWD" \
  /bin/sh -c "ls $HOME/.bashrc 2>&1; ls $HOME/Documents 2>&1"

Both commands should fail with “No such file or directory”. Note that ls $HOME itself may show a partial directory structure (like Development) because Bubblewrap creates the path hierarchy needed to reach your bound $PWD. But the actual contents of your home folder—config files, documents, other projects—remain invisible.

Test 2: Confirm you cannot write to read-only paths

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --ro-bind "$HOME/.gitconfig" "$HOME/.gitconfig" \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --chdir "$PWD" \
  /bin/sh -c "echo 'test' >> $HOME/.gitconfig"

This should fail with “Read-only file system”. The sandbox prevents writes to paths mounted with --ro-bind.

Test 3: Confirm you CAN write to the working directory

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --chdir "$PWD" \
  /bin/sh -c "touch sandbox-test-file && rm sandbox-test-file && echo 'Write access confirmed'"

This should print “Write access confirmed”. The sandbox allows writes to paths mounted with --bind.

Test 4: Confirm process isolation

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --unshare-pid \
  --chdir "$PWD" \
  /bin/ps aux

This should show only a few processes (ps itself and its parent). Your browser, terminal, and other applications stay hidden.

Test 5: Confirm /tmp isolation

Run this in one terminal:

echo "secret from host" > /tmp/host-secret.txt

Then run this in the sandbox:

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --chdir "$PWD" \
  /bin/cat /tmp/host-secret.txt

This should fail with “No such file or directory”. The sandbox has its own empty /tmp and cannot see files in the host’s /tmp.

Test 6: Confirm SSH agent works but keys are hidden

First, verify you have a key loaded in your agent:

ssh-add -l

This should list your key. Now test that the sandbox can use the agent but cannot read the key file:

bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --ro-bind "$HOME/.ssh/known_hosts" "$HOME/.ssh/known_hosts" \
  --bind "$(dirname $SSH_AUTH_SOCK)" "$(dirname $SSH_AUTH_SOCK)" \
  --bind "$PWD" "$PWD" \
  --tmpfs /tmp \
  --proc /proc \
  --dev /dev \
  --setenv SSH_AUTH_SOCK "$SSH_AUTH_SOCK" \
  --chdir "$PWD" \
  /bin/sh -c "ssh-add -l && cat ~/.ssh/id_ed25519"

The first command (ssh-add -l) should succeed and list your keys. The second command (cat ~/.ssh/id_ed25519) should fail with “No such file or directory”. The sandbox can use your SSH identity through the agent, but cannot read the private key file itself.


If all six tests pass, your sandbox walls are solid. The AI operates in the space you defined—no more, no less. Again- you can just git clone these tests from https://github.com/CaptainMcCrank/BlogCode/tree/main/BubblewrapTests.

Happy hacking!