Run OpenCode inside a bubblewrap sandbox for security isolation.
opencodebox is a bash script that runs OpenCode (AI coding assistant) inside a sandbox using bubblewrap. The sandbox provides process isolation with Linux namespaces (PID, IPC, UTS) and restricted filesystem access.
- Process Isolation: Uses unshare PID, IPC, and UTS namespaces
- Controlled Filesystem: Most system filesystem mounted read-only
- Custom Bind Mounts: Add read-write or read-only access with
--withand--with-ro - Mise Support: Integrated with mise for tool management
- SSH Agent Forwarding: Supports SSH commit signing through the host
ssh-agent - Seccomp Sandbox Filter: Mitigates kernel privilege escalation vulnerabilities (see details)
- bubblewrap (
bwrap) - for sandboxing - opencode - AI coding assistant
Security Note (CVE-2017-5226): Bubblewrap sandbox can be escaped via
TIOCSTIioctl if the kernel allows it. Since Linux 6.2,TIOCSTIis restricted whendev.tty.legacy_tiocsti=0(default). On older kernels, ensure bubblewrap >= 0.1.5 (usessetsid()fix) or enable seccomp filtering. Theinstall.shscript performs this check automatically.
curl -fsSL https://raw.githubusercontent.com/fityannugroho/opencodebox/main/install.sh | bashThis installs opencodebox to ~/.local/bin/opencodebox. Make sure ~/.local/bin is in your PATH.
Verify the installation :
opencodebox --versionopencodebox is a wrapper for the opencode command. All arguments are passed through to opencode inside the sandbox, except the commands OpenCode 2 refuses in isolation (see ### OpenCode Launcher Mount).
opencodebox [OPTIONS] [OPENCODE_ARGS...]Note: The
opencodecommand stays available when you need it. We didn't replace it.
opencodebox adds the following options :
--with /host[:/sandbox]- Bind host path read-write to sandbox--with-ro /host[:/sandbox]- Bind host path read-only to sandbox
These options allow you to specify additional directories to mount inside the sandbox for read-write or read-only access.
# Run sandboxed opencode in current directory
opencodebox
# Run sandboxed opencode with read-write access to /data
opencodebox --with /data
# Run sandboxed opencode with read-write access to /mnt/data mapped to /workspace/data
opencodebox --with /mnt/data:/workspace/data
# Run sandboxed opencode with read-only access to config
opencodebox --with-ro /etc/hosts
# Run sandboxed opencode server with specified bind mounts
opencodebox --with /data --with-ro /config serve- Parse arguments (
--with,--with-ro,--version,--help) - Check prerequisites (bwrap and opencode)
- Load seccomp sandbox filter (see details)
- Enforce security restrictions:
- Rejects running from
$HOME,~/.ssh,~/.gnupg, or their ancestors - Rejects sensitive paths in
--with/--with-robinds - Validates
~/.sshdirectory permissions (must be0700)
- Rejects running from
- Build bubblewrap sandbox with namespace isolation and bind mounts
- Setup SSH (sanitized
.pubkeys,known_hosts, agent forwarding) - Add conditional tool mounts (bun, npm, pnpm, uv, pipenv, cargo, git, mise) and extra bind mounts
- Execute opencode inside the sandbox
Read-Only:
/usr- System basics/etc/resolv.conf,/etc/hosts- DNS configuration and host mappings/etc/nsswitch.conf- Name service lookup configuration (if present)/etc/ssl- SSL/TLS certificates (if present)/etc/ca-certificates- CA certificate store (if present)/etc/pki/ca-trust,/etc/pki/tls/certs- Fedora/RHEL trust store and certificate targets for/etc/sslsymlinks (if present);/etc/pki/tls/privateis not mounted/etc/alternatives- System alternatives (managed by update-alternatives)$HOME/.local- User local data (except keyrings/tool data)$HOME/.cache/opencode- OpenCode cache$HOME/.ssh/*.pub- Sanitized OpenSSH public key material, when$HOME/.sshis not a symlink$HOME/.ssh/known_hosts- SSH host key, read-only, for Git-over-SSH host verificationgpg.ssh.allowedSignersFile- Configured SSH allowed signers file (if configured and under$HOME)- OpenCode: config dir (
$OPENCODE_CONFIG_DIR, else$XDG_CONFIG_HOME/opencode, else~/.config/opencode),.agents
Read-Write:
- Current project directory (
$PWD) $HOME/.local/share/opencode- OpenCode application data$HOME/.local/state/opencode- OpenCode runtime state (locks, etc.)
Tmpfs (Private, writable per-session):
/tmp- Temporary files$HOME/.cache- Universal cache$HOME/.local/share/keyrings- Exclude private keyring (if exists on host)
The directory holding the opencode binary is mounted at /opt/opencode and executed from there (read-write, as before). When opencode is an npm-style shim inside a <...>/node_modules/.bin directory, the sandbox instead mounts that node_modules directory read-only and executes the shim at /opt/opencode/.bin/opencode, because such shims resolve their target relative to their own location.
- The shim's target must resolve with exactly one
..level (as with mise's npm backend/aube); a deeper path escapes the mounted root and fails. - A shim outside
node_modules(for example$HOME/.bin) is not relocated: it keeps the old mount, which binds only the shim's own directory, and prints a warning; expectnot found(exit 127) for relative-target shims. - Self-upgrading or installing plugins from inside the sandbox is not supported for the read-only layout:
opencode upgradewrites to the install tree, andopencode plugin addupdates the global configuration under~/.config/opencode(mounted read-only) — do both on the host instead (e.g.mise upgrade opencode). - On first run the launcher creates opencode's config, agents, cache (including the helper
bindirectory), data and state directories when they are missing, so a fresh host does not fail at the bind step. OpenCode 2 starts from there; OpenCode 1 may still need its config seeded on the host (it writes.gitignoreinto~/.config/opencode, which is mounted read-only). - Only
node_modules/.binshims are relocated. Globalbinshims fromnpm|pnpm|bun install -gand symlinks whose target lies outside the mounted roots still fail withnot found(exit 127). - OpenCode 2 runs fully isolated: every command gets a private in-sandbox server — the launcher overlays a generated
{"disabled": true}service config so the client can never attach to a host service — and masks the host service registration in the state directory, so nothing in the sandbox can read the host server URL/password — while sessions, credentials, and TUI state stay shared (the config directory is shared read-only). Commands needing the host service (pair,service,serve --service/--stdio,--server,upgrade,update,uninstall) are refused; each error names the alternative.
Each tool is mounted only when command -v <tool> succeeds on the host. If the tool is not installed, none of its directories are bound into the sandbox.
| Tool | Read-Only Bind | Tmpfs |
|---|---|---|
| Bun | ~/.bun |
~/.bun/install/cache |
| npm | ~/.npmrc |
~/.npm |
| pnpm | ~/.config/pnpm |
~/.local/share/pnpm/store |
| uv | ~/.config/uv |
— |
| pipenv | — | ~/.local/share/virtualenvs |
| Rust/Cargo | ~/.rustup, ~/.cargo/bin, ~/.cargo/config.toml |
~/.cargo/registry |
| Git | ~/.gitconfig |
— |
| Mise | ~/.config/mise, ~/.local/share/mise, ~/.cache/mise (or MISE_CONFIG_DIR/MISE_DATA_DIR/MISE_CACHE_DIR when set; a sensitive directory is skipped with a warning) |
— |
opencodebox enforces several security restrictions to prevent sandbox escape:
- Project directory: Cannot run from
$HOME,~/.ssh,~/.gnupg, or their ancestors. Use a dedicated project directory. - Bind mounts:
--withand--with-roreject paths that point to or enclose sensitive locations ($HOME,~/.ssh,~/.gnupg). - State directory:
~/.local/state/opencodeis mounted read-write so that locks and TUI state stay shared between the sandbox and the host. The launcher masks the host background-service registration inside the sandbox, but a sandboxed process can still create files in that directory, and the host client reads some of them (including the pty runtime registration). Treat that directory as shared state, not as a trust boundary. - SSH directory:
~/.sshmust have permissions0700. Fix with:chmod 700 ~/.ssh
If SSH_AUTH_SOCK points to a valid socket, opencodebox forwards that socket into the sandbox. This allows SSH commit signing with keys already loaded by ssh-add on the host. This feature does not mount private SSH keys into the sandbox.
For Git SSH signing, use a public key path such as ~/.ssh/id_ed25519.pub, or an inline key::ssh-ed25519 ... value. Validates and sanitizes .pub files (rejects hardlinks, multi-line files; validates key type, base64 format, and OpenSSH key structure with ssh-keygen). The sandbox receives sanitized key material only (<key-type> <key-data>), so comments or extra file content are not exposed.
For Git-over-SSH network operations, known_hosts is mounted read-only when available. This allows host verification without exposing private keys. ~/.ssh/config is not mounted, and user-supplied binds under ~/.ssh are rejected by the sensitive-path guard (the launcher still mounts known_hosts and sanitized *.pub itself).
For local SSH signature verification, the configured gpg.ssh.allowedSignersFile is mounted read-only when it is an absolute regular file under $HOME; otherwise it is skipped with a warning.
Note:
.pubfiles are validated on the already-open file descriptor (via/proc/self/fd), so the validated content cannot be swapped afterwards.
Git-over-SSH network operations may still need explicit read-only binds for other files in custom setups, but every user-supplied path under ~/.ssh is rejected by the sensitive-path guard. User-provided binds and the current project bind can expose private keys if they include such files, so avoid binding ~/.ssh wholesale.
Forwarding an agent still lets sandboxed processes ask the agent to authenticate or sign while the socket is available. Use a dedicated signing key and consider ssh-add -c -t 1h ~/.ssh/signing_key for confirmation and expiry.
opencodebox includes a seccomp BPF filter that blocks socket creation for several protocol families to mitigate kernel privilege escalation vulnerabilities from inside the sandbox:
| Vulnerability | CVEs | Blocked Sockets |
|---|---|---|
| Copy Fail | CVE-2026-31431 | socket(AF_ALG, *, *) |
| Dirty Frag (ESP) | CVE-2026-43284 | socket(AF_INET/AF_INET6, *, IPPROTO_ESP) |
| Dirty Frag (ESP Bypass) | CVE-2026-43284 | socket(AF_NETLINK, *, NETLINK_XFRM), setsockopt(*, IPPROTO_UDP, UDP_ENCAP, *) |
| Dirty Frag (RxRPC) | CVE-2026-43500 | socket(AF_RXRPC, *, *) |
| Dirty Frag (IPCOMP) | CVE-2026-43284 | socket(AF_INET/AF_INET6, *, IPPROTO_IPCOMP) |
These are defense-in-depth mitigations and do not replace kernel patches. Supported architectures: x86_64 and aarch64.
The filter is automatically applied if the corresponding .bpf file is available; otherwise a warning is displayed and the sandbox runs without it. The seccomp filter is stored at ~/.local/share/opencodebox/seccomp-security.bpf after installation.
- Copy Fail — CVE-2026-31431
- Dirty Frag — CVE-2026-43284 / CVE-2026-43500
- Ubuntu Security Advisory — Dirty Frag
- AWS Security Bulletin — 2026-027
Dependencies:
- uv - Python package manager (install)
- bubblewrap - Required only for
tests/seccomp.sh(skips cleanly when absent)
Run:
uv run --group dev pytest -v # launcher tests (mocked bwrap, no root needed)
bash tests/seccomp.sh # seccomp integration (real bwrap + prebuilt .bpf)Launcher tests are black-box: bwrap is mocked, the argv it would receive is
asserted. GPU tests (future) plug into tests/conftest.py the same way.
To generate the seccomp BPF filter files (.bpf):
Dependencies:
- gcc - C compiler
- libseccomp-dev - libseccomp development headers and library
Install on Ubuntu/Debian:
sudo apt install gcc libseccomp-devCompile and generate:
# Compile the BPF generator
gcc -o seccomp/seccomp-security-gen seccomp/seccomp-security-gen.c -lseccomp
# Generate BPF filters for each architecture
./seccomp/seccomp-security-gen x86_64 > seccomp/seccomp-security-x86_64.bpf
./seccomp/seccomp-security-gen aarch64 > seccomp/seccomp-security-aarch64.bpf
# Clean up compiled generator
rm seccomp/seccomp-security-genThe .bpf filter files are pre-generated and shipped with the repository, so end users do not need these development dependencies.