
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:
- 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. - Slow Cold Starts: Spinning up a Docker container takes 500ms to 2 seconds per tool call. Bubblewrap starts in under 5 milliseconds.
- 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.