Skip to content

About

Just OpenCode, but runs in an isolated sandbox. Powered with bubblewrap.

Topics

Resources

Code of conduct

Contributing

Stars

9 stars

Watchers

1 watching

Forks

Latest commit

 

History

38 Commits

Folders and files

Repository files navigation

opencodebox

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.

Features

  • 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 --with and --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)

Prerequisites

Security Note (CVE-2017-5226): Bubblewrap sandbox can be escaped via TIOCSTI ioctl if the kernel allows it. Since Linux 6.2, TIOCSTI is restricted when dev.tty.legacy_tiocsti=0 (default). On older kernels, ensure bubblewrap >= 0.1.5 (uses setsid() fix) or enable seccomp filtering. The install.sh script performs this check automatically.

Installation

curl -fsSL https://raw.githubusercontent.com/fityannugroho/opencodebox/main/install.sh | bash

This installs opencodebox to ~/.local/bin/opencodebox. Make sure ~/.local/bin is in your PATH.

Verify the installation :

opencodebox --version

Usage

opencodebox 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 opencode command stays available when you need it. We didn't replace it.

Options

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.

Examples

# 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

How It Works

  1. Parse arguments (--with, --with-ro, --version, --help)
  2. Check prerequisites (bwrap and opencode)
  3. Load seccomp sandbox filter (see details)
  4. Enforce security restrictions:
    • Rejects running from $HOME, ~/.ssh, ~/.gnupg, or their ancestors
    • Rejects sensitive paths in --with/--with-ro binds
    • Validates ~/.ssh directory permissions (must be 0700)
  5. Build bubblewrap sandbox with namespace isolation and bind mounts
  6. Setup SSH (sanitized .pub keys, known_hosts, agent forwarding)
  7. Add conditional tool mounts (bun, npm, pnpm, uv, pipenv, cargo, git, mise) and extra bind mounts
  8. Execute opencode inside the sandbox

Bind Mounts Structure

Unconditional Mounts (Always Present)

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/ssl symlinks (if present); /etc/pki/tls/private is 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/.ssh is not a symlink
  • $HOME/.ssh/known_hosts - SSH host key, read-only, for Git-over-SSH host verification
  • gpg.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)

OpenCode Launcher Mount

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; expect not 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 upgrade writes to the install tree, and opencode plugin add updates 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 bin directory), 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 .gitignore into ~/.config/opencode, which is mounted read-only).
  • Only node_modules/.bin shims are relocated. Global bin shims from npm|pnpm|bun install -g and symlinks whose target lies outside the mounted roots still fail with not 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.

Conditional Tool Mounts (Requires Tool Installed on Host)

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) —

Security Restrictions

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: --with and --with-ro reject paths that point to or enclose sensitive locations ($HOME, ~/.ssh, ~/.gnupg).
  • State directory: ~/.local/state/opencode is 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: ~/.ssh must have permissions 0700. Fix with: chmod 700 ~/.ssh

SSH Agent and Git Signing

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: .pub files 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.

Seccomp Sandbox Filter

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.

References

Development

Tests

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.

Seccomp BPF filter

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-dev

Compile 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-gen

The .bpf filter files are pre-generated and shipped with the repository, so end users do not need these development dependencies.

License

MIT License

About

Just OpenCode, but runs in an isolated sandbox. Powered with bubblewrap.

Topics

Resources

Code of conduct

Contributing

Stars

9 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages