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.--procand--dev— a fresh/procfor 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-netto keep networking.)--die-with-parent— kill the sandbox if the launcher dies; no orphaned processes.--new-session— new session ID, blocking theTIOCSTIterminal-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:
--unshare-netso the process has only loopback.- Bind a Unix socket from the host into the sandbox.
- Inside, a small forwarder (e.g.
socat) exposes the socket as a local HTTP proxy;HTTP(S)_PROXYpoints at it. - On the host side, the proxy checks each
CONNECThostname 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
WebAssembly as a Sandbox: Running Untrusted Code With Wasmtime and WASI
WebAssembly's design makes it a strong in-process sandbox: linear memory, no ambient authority, and capability-based access through WASI. How the isolation works, limiting CPU with fuel and epochs, memory limits, the component model, real uses for plugins and user code, and where it falls short.
Linux Capabilities: Splitting Root Into Pieces
Linux capabilities break root's power into about 40 separate privileges, so a process can have just the one it needs. The capability sets (permitted, effective, inheritable, bounding, ambient), file capabilities with setcap, Docker's default set and --cap-drop ALL, why CAP_SYS_ADMIN is 'the new root', and inspecting capabilities.