Run opencode inside a bubblewrap sandbox, for use with JetBrains IDEs via their Agent Client Protocol (ACP) integration.
The sandbox isolates opencode so that, apart from its own config/data/cache/state directories, it can only read and write the project directory it was launched in. The rest of $HOME is invisible.
- Prerequisites
- Install
- Usage
- Testing
- Sandbox layout
- Wrapper configuration
- Use with JetBrains ACP
- Troubleshoot
- Linux
bwrapinstalled and setuid / user-namespaces enabled:- Debian/Ubuntu:
sudo apt install bubblewrap - Fedora:
sudo dnf install bubblewrap - Arch:
sudo pacman -S bubblewrap
- Debian/Ubuntu:
- opencode installed and on
$PATH(or setOPENCODE_BIN)
On Ubuntu 24.04, unprivileged user namespaces are restricted by AppArmor by default and
bwrapfails withbwrap: setting up uid map: Permission denied. See Ubuntu 24.04: AppArmor under Troubleshoot.
cp opencode-bwrap ~/.local/bin/opencode-bwrap
chmod +x ~/.local/bin/opencode-bwrapTest it:
cd /path/to/your/project
opencode-bwrap --versionAll flags are passed through unchanged, so opencode-bwrap acp, opencode-bwrap --model ..., etc. work as expected.
To run the full verification suite (syntax, DNS, /etc whitelist, env isolation, /dev/shm, real --version) from the repo checkout:
./test-opencode-bwrap| Path | Access |
|---|---|
$PWD (the project opened in the IDE / current dir) |
read-write |
~/.config/opencode, ~/.local/share/opencode, ~/.cache/opencode, ~/.local/state/opencode |
read-write |
/usr (+ symlinked /bin, /sbin, /lib, /lib64) |
read-only |
/etc — whitelist only: hosts, resolv.conf, nsswitch.conf, passwd, group, ssl, localtime, timezone, ld.so.cache |
read-only |
directory containing the opencode binary |
read-only |
/proc, /dev, /dev/shm, /tmp, /run |
sandbox-internal (proc/devfs, writable tmpfs) |
Everything else in $HOME (~/.ssh, ~/.gitconfig, ~/.config/gh, dotfiles, ...) |
not mounted / invisible |
- The environment is not passed through wholesale. The sandbox clears it (
--clearenv) and only forwards an allowlist:HOME,PATH(minimal, includes theopencodebinary dir),TMPDIR,XDG_CONFIG_HOME/XDG_DATA_HOME/XDG_CACHE_HOME/XDG_STATE_HOME(aligned with the bound directories),TERM,LANG,NO_COLOR, and the proxy variables (HTTP_PROXY,HTTPS_PROXY,ALL_PROXY,NO_PROXYplus lowercase variants, only when set). API keys and any other secrets in the parent environment are not forwarded. - Credentials come from
opencode auth login, which stores them in~/.local/share/opencode/auth.json(mounted read-write inside the sandbox) — no environment variables needed. - To forward extra variables (e.g. a provider API key exported in the shell), set
OPENCODE_BWRAP_PASSTHROUGHto a space-separated list of names:For ACP, set it in the entry'sOPENCODE_BWRAP_PASSTHROUGH="OPENAI_API_KEY OPENROUTER_API_KEY" opencode-bwrap --model ...env:{ "agent_servers": { "OpenCode (sandboxed)": { "command": "/home/you/.local/bin/opencode-bwrap", "args": ["acp"], "env": { "OPENCODE_BWRAP_PASSTHROUGH": "OPENAI_API_KEY" } } } } - To inject static env vars inside the sandbox, use
~/.config/opencode/opencode-bwrap.conf— see Wrapper configuration.
To give the sandboxed opencode its own config, create ~/.config/opencode/opencode.bwrap.json. When present, it is mounted read-only as opencode.json inside the sandbox, replacing the default global config (~/.config/opencode/opencode.json stays untouched on the host).
Network access is kept open (--share-net) as it is required for LLM API calls.
- The project directory is passed through read-write. Anything explicitly mounted is reachable by the sandboxed process — that includes the opencode config/state dirs, which hold credentials; this is inherent to letting opencode work.
- The sandbox uses a PID/IPC/UTS/user namespace,
--die-with-parent, and--new-session(safe here because ACP mode is headless, and it hardens against TIOCSTI escapes). The host kernel is still shared, and a kernel-level exploit could escape. --tmpfs /tmpgives opencode and the tools it shells out to a writable, ephemeral, private/tmp.- If you launch the sandbox inside
$HOMEitself (i.e. the project directory is your home dir), the whole home directory is mounted read-write and nothing is isolated — launch from a real project directory instead.
The sandbox only exposes /usr (plus symlinked /bin, /sbin, /lib*), the project directory, and opencode's own directories. Toolchains installed elsewhere — an SDKMAN JDK, a custom JDK under /opt, Gradle/Kotlin, ... — are neither mounted nor on PATH, and --clearenv drops their environment variables. Extend the sandbox per user with ~/.config/opencode/opencode-bwrap.conf (line-based; # comments and blank lines are ignored; directives may repeat):
| Directive | Meaning |
|---|---|
bind-ro SRC [DEST] |
Mount SRC read-only into the sandbox at DEST (defaults to SRC). Fails hard if SRC is not a directory. |
bind-rw SRC [DEST] |
Mount SRC read-write into the sandbox at DEST (defaults to SRC); writes persist on the host. Fails hard if SRC is not a directory. |
path ENTRY |
Append ENTRY to the sandboxed PATH (after /usr/bin:/bin:<opencode dir>). |
env NAME=VALUE |
Set environment variable NAME to VALUE inside the sandbox. |
For an SDKMAN JDK 21 + Gradle toolchain (all under /opt/sdkman) with a persistent Gradle cache:
# ~/.config/opencode/opencode-bwrap.conf
bind-ro /opt/sdkman
bind-rw /home/you/.gradle
path /opt/sdkman/candidates/java/current/bin
path /opt/sdkman/candidates/gradle/current/bin
env JAVA_HOME=/opt/sdkman/candidates/java/currentBecause bind-ro binds the whole candidates tree, the current symlinks resolve inside the sandbox and both the java/javac and gradle binaries are reachable with JAVA_HOME set.
bind-rw /home/you/.gradle persists the Gradle dependency cache at its default location (Gradle stores its dependency cache, wrapper distributions and daemon under $HOME/.gradle, the GRADLE_USER_HOME default — no env needed). Without bind-rw, $HOME/.gradle resolves to the sandbox's ephemeral tmpfs and every build re-downloads everything.
Any path listed in bind-ro/bind-rw becomes reachable by the sandbox (read-only or read-write respectively), and env values are visible to all processes inside it; everything not explicitly mounted stays invisible.
- Open the AI Chat tool window, click the button in the upper-right and select Add Custom Agent. This creates
~/.jetbrains/acp.json. - Point the
commandat the absolute path of this wrapper script (ACP requires an absolute path), withargs: ["acp"]:
{
"agent_servers": {
"OpenCode (sandboxed)": {
"command": "/home/you/.local/bin/opencode-bwrap",
"args": ["acp"]
}
}
}- Select OpenCode (sandboxed) in the AI Chat agent selector and start chatting.
JetBrains can launch ACP subprocesses with a trimmed environment. If the script reports that opencode is not found, pin the binary in the entry's env:
{
"agent_servers": {
"OpenCode (sandboxed)": {
"command": "/home/you/.local/bin/opencode-bwrap",
"args": ["acp"],
"env": { "OPENCODE_BIN": "/home/you/.opencode/bin/opencode" }
}
}
}Ubuntu 24.04 enables AppArmor-based restriction of unprivileged user namespaces by default (kernel.apparmor_restrict_unprivileged_userns=1), but the bubblewrap package ships no AppArmor profile. Running the script as an unprivileged user then fails with:
bwrap: setting up uid map: Permission denied
Check whether you are affected:
sysctl kernel.apparmor_restrict_unprivileged_userns # 1 = restrictedCreate /etc/apparmor.d/bwrap (needs sudo):
abi <abi/4.0>,
include <tunables/global>
profile bwrap /usr/bin/bwrap flags=(unconfined) {
userns,
# Site-specific additions and overrides. See local/README for details.
include if exists <local/bwrap>
}
Load it (no reboot needed):
sudo apparmor_parser -r /etc/apparmor.d/bwrapThis grants user-namespace creation only to the bwrap binary itself and leaves the system-wide restriction intact.
Ubuntu ships a stricter profile (bwrap-userns-restrict) that also blocks programs inside the sandbox from creating further namespaces. If it is available on your system (it ships in apparmor-profiles on newer releases, e.g. Ubuntu 25.04), install and load it instead of the profile above:
sudo apt install apparmor-profiles apparmor-utils
sudo install -m 0644 \
/usr/share/apparmor/extra-profiles/bwrap-userns-restrict \
/etc/apparmor.d/bwrap-userns-restrict
sudo apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrictOnly if the profiles above do not help — this disables a security feature for the whole system:
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
echo 'kernel.apparmor_restrict_unprivileged_userns=0' | sudo tee /etc/sysctl.d/90-unpriv-userns.confThen retry:
opencode-bwrap --version