bwai is a CLI tool that runs AI coding agents (Claude, Gemini, Goose) inside a bubblewrap sandbox. The sandbox enforces a read-only host filesystem, whitelisted dotfiles and environment variables, and restricts writes to the current working directory only — preventing agents from accessing sensitive data like SSH keys, AWS credentials, or GPG keys.
cmd/bwai/
main.go # Entry point, flag parsing, bwrap command construction
config.go # Config struct and JSON loading (~/.bwai.json)
defaults.json # Built-in default configuration (embedded into the binary at build time)
mounts.go # Filesystem mount logic (home, DNS, GPU, shm)
bwrap.go # Low-level bwrap argument helpers (roBind, rwBind, devBind, tmpfs)
update.go # Self-update from GitHub releases with SHA-256 verification
version.go # Version constant (set at build time via ldflags)
*_test.go # Unit tests for each module
scripts/
check.sh # Runs fmt, lint, and tests (used in CI and pre-commit)
Makefile # build, install, test, fmt, lint, clean targets
- Pure Go 1.21, zero external dependencies (stdlib only)
- Requires
bwrapinstalled on the host system (Linux only)
make build # Compiles to bin/bwai
make test # Runs all unit tests
make lint # Runs golangci-lint
make fmt # Formats codeCI runs scripts/check.sh which chains fmt-check, lint, and test.
The config file lives at ~/.bwai.json. Key fields:
| Field | Purpose |
|---|---|
bwrap_path |
Path to the bwrap binary |
bwrap_extra_args |
Extra flags passed directly to bwrap. Each element is split on whitespace, so "--ro-bind /var /var" and "--ro-bind", "/var", "/var" are equivalent |
command |
Default command to run inside the sandbox |
home_allow |
Dotfiles/paths from $HOME to expose (read-only) |
home_block |
Dotfiles/paths to explicitly block |
env_allow |
Environment variables to pass through |
home_block takes precedence over home_allow at the same nesting level. However, the two can be combined at different levels: a home_block entry hides a sub-path inside an otherwise allowed directory (e.g. home_allow: [".cache"] + home_block: [".cache/sccache"] hides .cache/sccache), and a home_allow entry re-exposes a sub-path inside an otherwise blocked directory (e.g. home_block: [".cache"] + home_allow: [".cache/sccache"] exposes only .cache/sccache). Patterns support glob suffixes (e.g., .bash_history*) and nested paths (e.g., .config/goose).
- Read-only by default: The entire host OS tree is bind-mounted read-only. Only the current working directory is writable.
- No parent-process exit issue: Non-bash commands are wrapped in
bash -i -cto avoid agent processes becoming orphaned when the shell exits. - GPU support:
gpuMounts()inmounts.goauto-detects and mounts NVIDIA and DRI devices. - Safe self-update: Downloads binary, verifies SHA-256 digest, replaces atomically with rollback on failure.
- Zero dependencies: Keeps the supply chain minimal; everything uses Go stdlib.
bwai # Open sandboxed bash shell
bwai -c claude # Launch claude directly inside sandbox
bwai --ro-dir /some/path # Expose extra read-only directory
bwai -- --some-agent-flag # Pass args to the configured command
bwai update # Self-update to latest GitHub release
bwai --dump-config # Print default config as JSON
- New mount type: Add a helper function in
mounts.gofollowing the existing pattern ([]stringreturn, appended inmain.go). - New CLI flag: Add to
main.gousing theflagpackage; wire into the bwrap args slice. - New config field: Add to the
Configstruct inconfig.goand add the default value todefaults.json. - New agent support: Add its dotfiles to
home_allowand its API key toenv_allowindefaults.json.
Tests live alongside source files (*_test.go). They use only testing from stdlib. Run with make test. There is no integration test harness — unit tests cover mount logic, bwrap arg construction, and the update mechanism.