Blog
4 min read

Bubblewrap (bwrap): Building Unprivileged Sandboxes From Namespaces

Bubblewrap is the small, unprivileged sandboxing tool behind Flatpak and several AI coding agent sandboxes. How it assembles a sandbox from user, mount, PID and network namespaces, practical bwrap command lines, adding seccomp, network allow-listing via a proxy, the Ubuntu AppArmor user-namespace restriction, and its limits.

Bubblewrap (bwrap) is a tiny program that runs a command inside a sandbox built from Linux namespaces — without a daemon, without root (given unprivileged user namespaces), and without images. It's the engine under Flatpak, and it's what Claude Code's built-in sandbox uses on Linux and WSL2. (Linux namespaces explained, Claude Code sandbox)

Where Docker gives you a whole container runtime, bubblewrap gives you exactly the namespaces and mounts you ask for, assembled per invocation.

How it builds a sandbox

By default a bwrap sandbox starts with an empty mount namespace — nothing is visible until you bind it in. You then compose:

  • User namespace — map your UID inside; enables the other namespaces unprivileged.
  • Mount namespace — choose exactly which host paths appear, read-only or read-write.
  • PID namespace — the sandboxed process can't see or signal host processes.
  • Network namespace — optional; an empty one has no interfaces except loopback.
  • IPC / UTS / cgroup namespaces — further separation.

Then it execves your command inside.

A practical command line

Run a build tool with read-only system files, a writable project directory, no access to $HOME, and no network:

bwrap \
  --ro-bind /usr /usr \
  --symlink usr/lib /lib --symlink usr/lib64 /lib64 --symlink usr/bin /bin \
  --ro-bind /etc/resolv.conf /etc/resolv.conf \
  --ro-bind /etc/ssl /etc/ssl \
  --proc /proc \
  --dev /dev \
  --tmpfs /tmp \
  --bind "$PWD" /work \
  --chdir /work \
  --unshare-all \
  --die-with-parent \
  --new-session \
  --clearenv --setenv PATH /usr/bin --setenv HOME /work \
  npm test

What each piece does:

  • --ro-bind / --bind — expose host paths read-only / read-write.
  • --proc and --dev — a fresh /proc for the new PID namespace and a minimal /dev.
  • --tmpfs /tmp — private, throwaway temp.
  • --unshare-all — new user, IPC, PID, network, UTS and cgroup namespaces. (Add --share-net to keep networking.)
  • --die-with-parent — kill the sandbox if the launcher dies; no orphaned processes.
  • --new-session — new session ID, blocking the TIOCSTI terminal-injection trick.
  • --clearenv — don't leak the parent's environment variables (tokens!) into the sandbox.

Credentials stay invisible because ~/.ssh, ~/.aws and friends were never bound in.

Adding seccomp

Namespaces control what's visible; seccomp controls which syscalls are allowed. bwrap accepts a compiled BPF filter on a file descriptor:

bwrap ... --seccomp 3 3< filter.bpf -- cmd

Generate the filter with libseccomp (block ptrace, mount, keyctl, bpf, unusual socket families, and so on). Claude Code's sandbox, for example, optionally adds a seccomp filter to block Unix domain socket creation. (Container hardening with seccomp)

Network allow-lists

bwrap's network choice is binary: share the host network or get none. To allow specific domains, give the sandbox no network of its own and route traffic through a proxy on the host:

  1. --unshare-net so the process has only loopback.
  2. Bind a Unix socket from the host into the sandbox.
  3. Inside, a small forwarder (e.g. socat) exposes the socket as a local HTTP proxy; HTTP(S)_PROXY points at it.
  4. On the host side, the proxy checks each CONNECT hostname against an allow-list.

This is the design Anthropic's sandbox-runtime uses on Linux, and the general pattern for agent egress control. (Egress control for AI agents)

The Ubuntu user-namespace restriction

Ubuntu 24.04+ restricts unprivileged user namespaces through AppArmor by default (kernel.apparmor_restrict_unprivileged_userns=1), because they expand kernel attack surface. Unconfined processes can't create fully privileged user namespaces, so bwrap fails with permission errors. The fix is an AppArmor profile granting userns to /usr/bin/bwrap — not disabling the restriction system-wide. Inside containers, bwrap often fails for similar reasons unless the container permits nested user namespaces.

What bubblewrap is not

  • Not a policy tool — it's a low-level primitive; the security depends entirely on the command line you build. Flatpak, agent runtimes and others are the policy layer on top.
  • Not a kernel boundary — everything inside shares the host kernel. A kernel exploit reachable from inside escapes. User namespaces in particular have a history of exposing kernel bugs. (Rootless containers and user namespaces)
  • Not resource control — add cgroups (e.g. systemd-run --user --scope -p MemoryMax=2G bwrap …). (Container CPU and memory limits)

bwrap vs alternatives

bubblewrap Docker/Podman Landlock gVisor / microVM
Setup One binary Daemon/runtime, images Library calls in-process Runtime / VMM
Per-command overhead Milliseconds Higher ~None Higher
Filesystem view Bind exactly what you choose Image + mounts Host FS with access rules Image
Kernel isolation Shared Shared Shared Strong

(Landlock, Firecracker vs gVisor vs containers)

For sandboxing individual commands an agent runs, bubblewrap's speed and precision are hard to beat. For isolating tenants from each other, use a VM boundary underneath.


EasySpawn provides that VM boundary: every server is its own virtual machine, so per-command sandboxes like bubblewrap protect your workspace while the hypervisor protects everyone else. See how it works or join the waitlist.

Related: Landlock · Linux Namespaces Explained · The Claude Code Sandbox · Egress Control for AI Agents

Keep reading