2026-09-05
You want to run something suspicious. Or a build script from a fresh clone. Or just a shell that can't read ~/.ssh. The mainstream answers all suck: Docker requires a daemon and root at install time, chroot needs root and can't hide the network, unshare(1) gives you namespaces but no bind mounts or seccomp. Firejail wants a profile file per program.
bubblewrap (bwrap) is the tool Alexander Larsson wrote for Flatpak. It's a setuid-safe launcher that assembles a fresh mount namespace, user namespace, PID namespace, seccomp filter, and process tree — in a single argv. No daemon. No config files. No root required (on any modern kernel with unprivileged user namespaces).
The simplest useful invocation — a shell that sees the host read-only but has a private /tmp and no network:
bwrap \
--ro-bind / / \
--dev /dev --proc /proc --tmpfs /tmp \
--unshare-all --share-net=false \
--die-with-parent \
bash
That's it. It starts in ~5 ms. rm -rf / inside it does nothing to the host because / is a read-only bind.
Where it earns its keep is composing narrower jails. Run an npm install that can see the project but nothing else in your home:
bwrap \
--ro-bind /usr /usr --ro-bind /etc /etc \
--symlink usr/bin /bin --symlink usr/lib /lib --symlink usr/lib64 /lib64 \
--proc /proc --dev /dev --tmpfs /tmp \
--bind "$PWD" /work --chdir /work \
--setenv HOME /work --setenv PATH /usr/bin \
--unshare-all --share-net \
--die-with-parent \
npm install
The install script cannot read ~/.ssh/id_ed25519, cannot see your gpg-agent socket, cannot write to ~/.bashrc. It can hit the network (because --share-net), and can only write to /work and /tmp.
Some flags that are hard to appreciate until you need them:
--dev-bind /dev/dri /dev/dri — pass through GPU nodes for a sandboxed browser without exposing the whole /dev.--overlay-src / --overlay /some/path — layer a tmpfs overlay so writes go to an ephemeral upper. Perfect for a "try this make install and throw it away" workflow.--seccomp 10 — read a seccomp BPF program from fd 10. Combine with libseccomp to actually forbid syscalls.--new-session — new controlling TTY, so a compromised child can't TIOCSTI-inject keystrokes into your outer shell (a real 2017-era escape).--info-fd 3 — bubblewrap writes JSON about the sandbox (child pid, namespace paths) to fd 3, so a supervisor can join later with nsenter.Compare with unshare: unshare gives you namespaces, but the moment you want a read-only /usr and a fresh /tmp you're writing a mount script that runs inside unshare --mount. Bubblewrap collapses that into declarative flags and — critically — does the pivot_root dance correctly, dropping capabilities in the right order so the child never inherits CAP_SYS_ADMIN in the initial namespace.
Compare with Docker: no image, no layer cache to prune, no daemon socket that's effectively root-equivalent, no iptables chains being rewritten behind your back. It's just a process.
Ships in Debian, Fedora, Arch, Alpine, Nix. Zero runtime dependencies beyond libc.
bwrap gives you namespaces, bind mounts, and seccomp declaratively in one argv — no daemon, no root, no config file.
