How to Sandbox AI Coding Agents with Bubblewrap (bwrap) on Linux

Quick Fix (TL;DR)

Autonomous coding agents executing untrusted terminal commands can read private SSH keys, leak environment tokens, or damage host filesystems. On Linux, run agent tool executions inside an unprivileged Bubblewrap sandbox with read-only root mounts and an isolated workspace:

# 1. Install bubblewrap on Debian / Ubuntu
sudo apt update && sudo apt install -y bubblewrap

# 2. Run agent commands in an isolated user namespace sandbox
bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --ro-bind /sbin /sbin \
  --ro-bind /etc/resolv.conf /etc/resolv.conf \
  --ro-bind /etc/ssl /etc/ssl \
  --proc /proc \
  --dev /dev \
  --tmpfs /tmp \
  --tmpfs /home/sandbox \
  --bind /path/to/project /workspace \
  --chdir /workspace \
  --unshare-all \
  --share-net \
  --die-with-parent \
  bash -c "npm test"

This configuration restricts the process to /workspace, makes your host filesystem read-only, and creates an isolated tmpfs home directory.


1. Why Docker Is Insufficient for Local Agent Tooling

Most developers default to Docker containers for tool execution. However, Docker introduces three operational friction points for local workstation agents:

  1. Daemon Dependency: Docker requires a background system daemon running with root privileges (/var/run/docker.sock). Granting an AI agent access to the Docker socket allows complete host root escalation.
  2. Slow Cold Starts: Spinning up a Docker container takes 500ms to 2 seconds per tool call. Bubblewrap starts in under 5 milliseconds.
  3. UID/GID Ownership Clashes: Files created inside standard Docker containers often end up owned by root:root, corrupting your local git working tree permissions.

2. Agent Sandboxing Technologies Compared

Sandbox Technology Startup Latency Root Privileges Required Filesystem Isolation Network Isolation Host File Sharing
Bubblewrap (bwrap) < 5 ms No (Unprivileged user namespace) Strict (bind mounts) Unshared namespace Zero-copy local directory bind
Docker Engine 800-2000 ms Yes (or rootless daemon) Strict (overlayfs) Bridge network Volume mounts with UID translation
Firecracker MicroVM 120-300 ms Yes (KVM access) Complete VM boundary TAP device routing Virtio-fs overhead
gVisor (runsc) 50-150 ms Yes Intercepted syscalls Virtual network stack 9p / virtiofs sharing

Bubblewrap provides the best balance of speed, security, and unprivileged execution for local CLI agents like Claude Code, Cursor, and custom Python loops.


3. Building a Reusable Sandbox Runner Script

Create an executable wrapper script named agent-sandbox.sh:

#!/usr/bin/env bash
set -euo pipefail

TARGET_DIR="${1:-$(pwd)}"
shift || true
COMMAND="${*:-bash}"

# Check that target directory exists
if [[ ! -d "$TARGET_DIR" ]]; then
  echo "Error: Target directory '$TARGET_DIR' does not exist." >&2
  exit 1
fi

exec bwrap \
  --ro-bind /usr /usr \
  --ro-bind /lib /lib \
  --ro-bind /lib64 /lib64 \
  --ro-bind /bin /bin \
  --ro-bind /sbin /sbin \
  --ro-bind /etc/alternatives /etc/alternatives \
  --ro-bind /etc/resolv.conf /etc/resolv.conf \
  --ro-bind /etc/ssl /etc/ssl \
  --ro-bind /etc/pki /etc/pki 2>/dev/null || true \
  --proc /proc \
  --dev /dev \
  --tmpfs /tmp \
  --tmpfs /home/agent \
  --bind "$TARGET_DIR" /workspace \
  --chdir /workspace \
  --setenv HOME /home/agent \
  --setenv PATH /usr/local/bin:/usr/bin:/bin \
  --unshare-all \
  --share-net \
  --die-with-parent \
  bash -c "$COMMAND"

Make the script executable:

chmod +x agent-sandbox.sh

4. Integrating with Python Agent Tool Calls

When building custom AI agent loops (such as ReAct loops with FastMCP or LangGraph), wrap your bash execution tool inside the sandbox script:

import subprocess
from pathlib import Path

def execute_sandboxed_command(command: str, workspace_path: str, timeout_seconds: int = 30) -> dict:
    workspace = str(Path(workspace_path).resolve())
    
    bwrap_cmd = [
        "bwrap",
        "--ro-bind", "/usr", "/usr",
        "--ro-bind", "/lib", "/lib",
        "--ro-bind", "/lib64", "/lib64",
        "--ro-bind", "/bin", "/bin",
        "--ro-bind", "/sbin", "/sbin",
        "--ro-bind", "/etc/resolv.conf", "/etc/resolv.conf",
        "--ro-bind", "/etc/ssl", "/etc/ssl",
        "--proc", "/proc",
        "--dev", "/dev",
        "--tmpfs", "/tmp",
        "--tmpfs", "/home/sandbox",
        "--bind", workspace, "/workspace",
        "--chdir", "/workspace",
        "--setenv", "HOME", "/home/sandbox",
        "--unshare-all",
        "--share-net",
        "--die-with-parent",
        "bash", "-c", command
    ]
    
    try:
        proc = subprocess.run(
            bwrap_cmd,
            capture_output=True,
            text=True,
            timeout=timeout_seconds,
            check=False
        )
        return {
            "exit_code": proc.returncode,
            "stdout": proc.stdout,
            "stderr": proc.stderr
        }
    except subprocess.TimeoutExpired:
        return {
            "exit_code": 124,
            "stdout": "",
            "stderr": f"Execution timed out after {timeout_seconds} seconds."
        }

5. Security Verification: Testing Boundary Enforcement

Test whether your sandbox prevents data exfiltration and unauthorized writes:

Test 1: Block Reading Host Sensitive Files

./agent-sandbox.sh . "cat ~/.ssh/id_rsa"

Output:

cat: /home/agent/.ssh/id_rsa: No such file or directory

The agent cannot see your user home directory or SSH keys because /home/agent is an empty tmpfs mount.

Test 2: Block Modifying System Binaries

./agent-sandbox.sh . "touch /usr/bin/malicious"

Output:

touch: cannot touch '/usr/bin/malicious': Read-only file system

Test 3: Verify Permitted Workspace Writes

./agent-sandbox.sh . "echo 'passed' > test_output.txt && cat test_output.txt"

Output:

passed

The write succeeds inside /workspace, reflecting directly in your host project repository with standard non-root user permissions.