bui - Bubblewrap TUI
A terminal user interface for configuring and launching bubblewrap sandboxes.
Instead of memorizing dozens of bwrap flags, visually configure your sandbox and see the generated command before execution.
Contents
- Status
- Requirements
- Installation
- Quick Start
- Profiles
- Managed Sandboxes
- Network Filtering
- Development
- License
Status
- Beta quality, moderately tested. That said it does not modify your system (with the exception of ~/.local install and state management of its own files, and creation of files in /tmp when needed to support various combinations of filtering). Do your own diligence before trusting the security of critical data to it, and also review bubblewrap CVEs / known issues.
- PRs and bug reports welcome. Feature requests will be considered.
Requirements
- uv - see installation docs
- bubblewrap
Installation
curl -LO https://github.com/reubenfirmin/bubblewrap-tui/releases/latest/download/bui chmod +x bui ./bui --install
Update to latest: bui --update
Compatibility
- Intended to work on all modern Linux distros. Please file tickets with any issues
- Will not work on OSX, which doesn't have Bubblewrap or equivalents (your best options would be something like Sandbox.app or Docker Desktop)
Quick Start
Launch the TUI to configure a sandbox interactively:
The TUI lets you:
- Toggle filesystem access (read-only system paths, /tmp, /etc, etc.)
- Add bound directories (read-only or read-write)
- Configure overlays for persistent changes
- Set environment variables
- Enable/disable network access
- Save configurations as reusable profiles
Press Enter to execute or Esc to quit.
Profiles
Profiles are saved sandbox configurations. Once you have a profile, you can skip the TUI and run commands directly:
bui --profile <name> -- <command>
This is useful for:
- Scripting and automation
- Running the same sandbox configuration repeatedly
- Sharing configurations with others
The untrusted Profile
Running bui --install creates a built-in untrusted profile designed for running untrusted code safely:
- Isolated home directory (your real home is not accessible)
- Read-only system paths (
/usr,/bin,/lib, etc.) - Network access enabled (for downloads)
- Strong isolation (new session, PID namespace, dropped capabilities)
Custom Profiles
Create your own profiles:
- Run
bui -- /bin/bash - Configure settings in the TUI
- Click "Save" to save with a name (e.g.,
my-profile)
Profiles are stored in ~/.config/bui/profiles/.
Managed Sandboxes
For applications you want to install and run repeatedly in isolation, bui provides managed sandboxes. This is useful for:
- Running
curl | bashinstall scripts safely - Isolating development tools from your system
- Running AI coding assistants with restricted access
Safe curl | bash - Installing Deno
Install Deno in an isolated sandbox:
# Run the install script in a sandbox named "deno" bui --profile untrusted --sandbox deno -- 'curl -fsSL https://deno.land/install.sh | sh'
This runs the install script with:
- Isolated home directory (
~/.local/state/bui/overlays/deno/) - your real home is not accessible - Read-only access to system paths
- Network access for downloads
After installation, create a wrapper script so you can use deno normally:
bui --sandbox deno --install
Executables in sandbox 'deno':
1. .deno/bin/deno
Select binary (number): 1
Installed: /home/user/.local/bin/deno
Now use Deno from any directory - --bind-cwd is automatic:
cd ~/projects/myapp deno run server.ts deno compile main.ts
Constraining Agents - Sandboxing Claude Code
AI coding assistants like Claude Code can execute arbitrary shell commands and modify files. Running them in a sandbox provides defense in depth - even if the AI makes a mistake or is manipulated, it can only affect files you explicitly allow.
What the sandbox provides:
- Claude cannot read
~/.ssh,~/.aws,~/.gnupg, browser data, or other sensitive dotfiles - Claude cannot modify system files or install packages globally on your system
- Each project directory is explicitly granted access via
--bind-cwd - All of Claude's installed files (npm packages, config) live in an isolated overlay
Why the complex command? We could create a custom profile in the TUI and use --profile my-claude-profile, but here we're reusing the generic untrusted profile and layering on a few flags. This is one-time setup - once we run --install, we get a permanent wrapper script that handles all of this.
Installation
The untrusted profile only exposes system paths (/usr, /bin, /lib). If npm/node are installed in your home directory (e.g., via nvm), bind them explicitly:
# Install Claude Code in a sandbox # --bind: expose the directory containing npm (needed for installation) # --bind-env: set NPM_CONFIG_PREFIX so npm installs to the sandbox home, not /usr bui --profile untrusted --sandbox claude \ --bind $(dirname $(which npm)) \ --bind-env 'NPM_CONFIG_PREFIX=/home/sandbox/.npm-global' \ -- npm install -g @anthropic-ai/claude-code
Create a wrapper script so you can run claude from anywhere:
bui --sandbox claude --install
# Select: claudeUsage
cd ~/projects/myapp claude
The wrapper script automatically:
- Runs Claude in the sandbox with your saved profile
- Binds your current directory read-write (
--bind-cwd) - Passes through the bind paths and environment from installation
Because the wrapper uses --bind-cwd, Claude can read and write files in your current directory. It cannot access other directories, your home directory, or sensitive dotfiles.
Managing Sandboxes
List installed sandboxes:
Sandboxes:
deno
profile: untrusted
scripts: deno
claude
profile: untrusted
scripts: claude
bind: /home/user/.nvm/versions/node/v20.0.0/bin
bind-env: NPM_CONFIG_PREFIX=/home/sandbox/.npm-global
List overlay directories (including orphaned ones):
Overlays:
/home/user/.local/state/bui/overlays/deno/
files: 127
To remove: bui --sandbox deno --uninstall
/home/user/.local/state/bui/overlays/abc123/
files: 8
No sandbox installed (safe to delete)
Uninstall a sandbox:
bui --sandbox deno --uninstall
Removed: /home/user/.local/bin/deno
Removed: /home/user/.local/state/bui/overlays/deno/
Tips
Terminal colors: If the terminal looks basic (no colors), pass through TERM:
bui --sandbox myapp --bind-env "TERM=$TERM" -- myappNetwork Filtering
Network filtering uses pasta (part of passt) to create an isolated network namespace, then applies iptables rules inside that namespace.
Why pasta?
Creating a network namespace normally requires root privileges. Pasta provides user-space networking without requiring CAP_SYS_ADMIN or root access. In spawn mode, pasta creates the namespace and runs your command inside it with full network connectivity.
How filtering works
- Pasta creates an isolated user+network namespace
- A wrapper script applies iptables rules with
CAP_NET_ADMIN - The wrapper execs bwrap with
--unshare-user --disable-usernsfor full namespace isolation - Your command runs unprivileged and cannot modify the firewall rules or create nested sandboxes
This ensures filtering decisions made at launch cannot be bypassed by the sandboxed application.
Hostname filtering
Hostname filtering uses a lightweight DNS proxy inside the sandbox to intercept DNS queries:
- Blacklisted hostnames return NXDOMAIN (name not found)
- Whitelisted hostnames are forwarded to the host's configured DNS servers
- Wildcards are supported:
*.example.commatches subdomains only;example.commatches the domain and all subdomains
How it works:
- A ~180 line Python DNS proxy is generated and written to the sandbox temp directory
- The proxy starts inside the sandbox on
127.0.0.1:53 - The sandbox's
/etc/resolv.confpoints to the local proxy - DNS queries are intercepted and filtered before forwarding to the host's upstream DNS
- The proxy runs with no external dependencies (pure Python stdlib)
Inspecting the proxy:
When hostname filtering is active, the execution output shows the temp directory path (e.g., /tmp/bui-net-abc123/). You can inspect the generated proxy:
# View the generated DNS proxy script cat /tmp/bui-net-*/dns_proxy.py # The script contains your configured hosts list and mode (whitelist/blacklist) # It's plain Python - read it to understand exactly what filtering is applied
Security:
The proxy is protected against tampering by sandboxed processes:
/etc/resolv.confis ro-bind mounted from outside the sandbox, making it completely immutable- The DNS proxy script is ro-bind mounted, preventing modification or replacement
- The proxy binds to port 53 before
CAP_NET_ADMINis dropped, so sandboxed processes cannot bind their own DNS server
A malicious process could kill the proxy, but this would break its own DNS resolution (self-defeating).
Limitations:
- DNS-over-HTTPS (DoH) bypasses the proxy - block known DoH IPs via IP filtering if needed
- Only UDP DNS is intercepted (covers 99% of queries; TCP DNS fallback is rare)
- Applications that hardcode DNS servers bypass the proxy (rare, but possible)
Audit mode
Audit mode captures all network traffic to a pcap file without filtering. After the sandbox exits, a summary shows:
- Unique destinations contacted
- Bytes sent/received per destination
- Hostname mappings (from observed DNS responses)
The pcap file can be opened in Wireshark for detailed analysis.
Requirements
Network filtering requires:
pasta(install thepasstpackage)iptables(for rule application)ip6tables(only if filtering IPv6 traffic)setprivorcapsh(for dropping capabilities)
The TUI shows installation commands if dependencies are missing.
Development
# Run directly from source uv run python src/cli.py -- bash # Build single-file executable ./build.py # Run built version ./bui -- bash # Run tests uv run --with pytest --with pytest-cov --with pytest-asyncio --with textual pytest tests/ -v # With coverage uv run --with pytest --with pytest-cov --with pytest-asyncio --with textual pytest tests/ --cov=src --cov-report=term-missing
Code Layout
src/
├── cli.py # Entry point
├── app.py # Main TUI app (composes UI, orchestrates mixins)
├── bwrap.py # bwrap command generation
├── profiles.py # Profile save/load
├── installer.py # Self-install, managed sandboxes
├── sandbox.py # Sandbox/overlay management
│
├── net/ # Network filtering (pasta, iptables, audit)
├── model/ # Data models
│ ├── sandbox_config.py
│ ├── fields/ # Field definitions
│ └── ... # Config primitives, serializers
│
├── controller/ # Event handlers & UI sync
│ ├── sync.py # Bidirectional UI ↔ config sync
│ └── ... # Per-tab event mixins
│
└── ui/ # UI components
├── tabs/ # Tab layouts
├── widgets/ # Reusable widgets
└── ... # Modals, IDs, CSS
Key patterns:
model/defines data structures,controller/handles events,ui/renders- Each tab has a composition function in
ui/tabs/and event handlers incontroller/ app.pyinherits from controller mixins and composes the full UI
License
MIT