opencode-sandbox
v0.6.0
Published
OpenCode plugin that sandboxes agent commands using @anthropic-ai/sandbox-runtime (seatbelt on macOS, bubblewrap on Linux)
Maintainers
Readme
opencode-sandbox
An OpenCode plugin that sandboxes agent-executed commands using @anthropic-ai/sandbox-runtime.
Every bash tool invocation is wrapped with OS-level filesystem and network restrictions — no containers, no VMs, just native OS sandboxing primitives.
| Platform | Mechanism |
|----------|-----------|
| macOS | sandbox-exec (Seatbelt profiles) |
| Linux | bubblewrap (namespace isolation) |
| Windows | Not currently supported by OpenCode's command-string hook (commands pass through in permissive mode and are blocked in enforce mode) |
Install
// opencode.json
{
"plugin": ["opencode-sandbox"]
}The plugin is automatically installed from npm when OpenCode starts.
Linux prerequisites
1. Install bubblewrap:
# Debian/Ubuntu
sudo apt install bubblewrap
# Fedora
sudo dnf install bubblewrap
# Arch
sudo pacman -S bubblewrap2. Ubuntu 24.04+ (AppArmor fix):
Ubuntu 24.04 and later restrict unprivileged user namespaces via AppArmor, which prevents bubblewrap from working. You need to enable the bwrap-userns-restrict AppArmor profile:
# Install the AppArmor profiles package
sudo apt install apparmor-profiles
# Create the symlink to enable the profile
sudo ln -s /etc/apparmor.d/bwrap-userns-restrict /etc/apparmor.d/force-complain/bwrap-userns-restrict
# Load the profile
sudo apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrictYou can verify bwrap works:
bwrap --ro-bind / / --dev /dev --proc /proc -- echo "sandbox works"Without this fix, bwrap will fail with loopback: Failed RTM_NEWADDR: Operation not permitted or setting up uid map: Permission denied.
What it does
When the agent runs a bash command, the sandbox enforces three layers of protection:
Filesystem write protection
Commands can only write to the project directory and /tmp. Writing anywhere else returns "Read-only file system":
$ touch ~/some-file
touch: cannot touch '/home/user/some-file': Read-only file system
$ echo "data" > /etc/config
/usr/bin/bash: line 1: /etc/config: Read-only file systemSensitive file read protection
Access to credential directories is blocked:
$ cat ~/.ssh/id_rsa
cat: /home/user/.ssh/id_rsa: Permission deniedNetwork allowlist
Only approved domains are reachable. All other traffic is blocked via a local proxy:
$ curl https://evil.com
Connection blocked by network allowlist
$ curl https://registry.npmjs.org
(works — npmjs.org is in the default allowlist)Default restrictions
Filesystem (deny-read):
~/.ssh,~/.gnupg~/.aws/credentials,~/.azure,~/.config/gcloud,~/.config/gh~/.kube,~/.docker/config.json~/.npmrc,~/.netrc,~/.env
Filesystem (allow-read):
- Empty by default
Filesystem (allow-write):
- Project directory
- Git worktree (validated — unsafe paths like
/are rejected) /tmp
Network (allow-only):
registry.npmjs.org,*.npmjs.orgregistry.yarnpkg.compypi.org,*.pypi.org,crates.io,*.crates.iogithub.com,*.github.comgitlab.com,*.gitlab.com,bitbucket.org,*.bitbucket.orgapi.openai.com,api.anthropic.com*.googleapis.com
Everything else is blocked by default.
Configuration
Config files are stored outside the project directory (in ~/.config/opencode-sandbox/) so that sandboxed commands cannot modify them. This prevents indirect prompt injection from weakening the sandbox by overwriting the config.
Config file locations
The plugin searches for configuration in this order (first match wins):
- Environment variable
OPENCODE_SANDBOX_CONFIG(JSON string) - Per-project config
~/.config/opencode-sandbox/projects/<project-name>.json - Global config
~/.config/opencode-sandbox/config.json - Built-in defaults
The <project-name> is the basename of the project directory (e.g., my-app for /home/user/projects/my-app).
If XDG_CONFIG_HOME is set, it is used instead of ~/.config.
Example: Global config
// ~/.config/opencode-sandbox/config.json
{
"mode": "enforce",
"filesystem": {
"denyRead": ["~/.ssh", "~/.aws/credentials"],
"allowRead": ["~/.ssh/id_ed25519.pub"],
"allowWrite": [".", "/tmp", "/var/data"],
"denyWrite": [".env.production"]
},
"network": {
"allowedDomains": [
"registry.npmjs.org",
"github.com",
"*.github.com",
"api.openai.com",
"api.anthropic.com",
"my-internal-api.company.com"
],
"deniedDomains": ["malicious.example.com"]
}
}Path precedence
Path precedence is inherited from @anthropic-ai/sandbox-runtime:
- Read:
allowReadtakes precedence overdenyRead - Write:
denyWritetakes precedence overallowWrite
Example: allow git commit signing with SSH public key
If your Git workflow needs to read a public key (for example ~/.ssh/id_ed25519.pub) while keeping ~/.ssh blocked by default, re-allow only that file:
// ~/.config/opencode-sandbox/config.json
{
"filesystem": {
"denyRead": [
"~/.ssh",
"~/.gnupg",
"~/.aws/credentials",
"~/.azure",
"~/.config/gcloud",
"~/.config/gh",
"~/.kube",
"~/.docker/config.json",
"~/.npmrc",
"~/.netrc",
"~/.env"
],
"allowRead": ["~/.ssh/id_ed25519.pub"]
}
}Example: Per-project config
// ~/.config/opencode-sandbox/projects/my-app.json
{
"network": {
"allowedDomains": ["my-internal-api.company.com"]
}
}Environment variable
OPENCODE_SANDBOX_CONFIG='{"filesystem":{"denyRead":["~/.ssh"]},"network":{"allowedDomains":["github.com"]}}' opencodeExample allowing only the SSH public key to be read:
OPENCODE_SANDBOX_CONFIG='{"filesystem":{"denyRead":["~/.ssh","~/.gnupg","~/.aws/credentials","~/.azure","~/.config/gcloud","~/.config/gh","~/.kube","~/.docker/config.json","~/.npmrc","~/.netrc","~/.env"],"allowRead":["~/.ssh/id_ed25519.pub"]}}' opencodeEnforcement mode
The default mode is permissive: if the sandbox cannot initialize or wrap a command, the command runs without sandboxing.
Set mode to enforce to block bash commands whenever sandboxing cannot be applied, including on unsupported platforms:
{
"mode": "enforce"
}Disable
OPENCODE_DISABLE_SANDBOX=1 opencodeOr in any config file:
{
"disabled": true
}How it works
The plugin uses two OpenCode hooks:
tool.execute.before— Intercepts bash commands and wraps them withSandboxManager.wrapWithSandbox()before executiontool.execute.after— Restores the original command on the tool arguments after execution
It also listens to OpenCode events to restore the original command in persisted tool history and clean up sandbox resources when commands finish or are interrupted.
Agent → bash tool → [plugin wraps command] → sandboxed execution → [plugin restores UI] → AgentThe AI model interprets sandbox errors (like "Read-only file system" or "Connection blocked") directly from command output — no additional annotation layer needed.
Sandbox initialization is deferred until the first bash command, so the plugin does not interfere with OpenCode startup. Plugin diagnostics are sent through OpenCode's structured logger instead of being printed into the TUI. Sandbox violations are correlated with each individual tool call, including concurrent or repeated commands.
Windows status
@anthropic-ai/sandbox-runtime supports Windows through an argv-and-environment API, while OpenCode currently exposes this plugin's bash hook as a command string. Until those interfaces can be connected safely, the plugin leaves Windows commands unsandboxed in permissive mode and blocks bash commands in enforce mode.
Failure behavior
In the default permissive mode, commands run normally if sandbox initialization or wrapping fails. In enforce mode, the affected bash command is blocked instead.
Related
- @anthropic-ai/sandbox-runtime — The underlying sandbox engine
- OpenCode Plugins Docs — How to create and use plugins
- Claude Code Sandboxing — Anthropic's sandboxing documentation
