Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ opencodebox [OPTIONS] [OPENCODE_ARGS...]

- `--with /host[:/sandbox]` - Bind host path read-write to sandbox
- `--with-ro /host[:/sandbox]` - Bind host path read-only to sandbox
- `--exec COMMAND` - Run `COMMAND` inside the sandbox instead of `opencode`

These options allow you to specify additional directories to mount inside the sandbox for read-write or read-only access.

Expand All @@ -70,11 +71,17 @@ opencodebox --with-ro /etc/hosts

# Run sandboxed opencode server with specified bind mounts
opencodebox --with /data --with-ro /config serve

# Run a custom command inside the sandbox
opencodebox --exec /bin/bash

# Run a custom command with arguments
opencodebox --exec /bin/bash -l
```

## How It Works

1. Parse arguments (`--with`, `--with-ro`, `--version`, `--help`)
1. Parse arguments (`--with`, `--with-ro`, `--exec`, `--version`, `--help`)
2. Check prerequisites (bwrap and opencode)
3. Load seccomp sandbox filter (see [details](#seccomp-sandbox-filter))
4. **Enforce security restrictions**:
Expand All @@ -84,7 +91,7 @@ opencodebox --with /data --with-ro /config serve
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
8. Execute the target command (`opencode` by default, or `--exec` override) inside the sandbox

## Bind Mounts Structure

Expand Down
21 changes: 20 additions & 1 deletion opencodebox
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
# ============================================================

VERSION="0.4.4"
# Command to run inside the sandbox; empty means the opencode launcher
# detected later (OPCODE_SANDBOX_PATH) is used.
EXEC_CMD=""

print_version() {
local opencode_version
Expand All @@ -27,6 +30,7 @@ Usage:
Options:
--with /host[:/sandbox] Bind host path read-write (sandbox path defaults to host path)
--with-ro /host[:/sandbox] Bind host path read-only (sandbox path defaults to host path)
--exec COMMAND Run COMMAND inside the sandbox instead of opencode
--version, -v Show version information
--help, -h Show this help message

Expand All @@ -43,6 +47,7 @@ Examples:
opencodebox --with /mnt/data:/workspace/data
opencodebox --with-ro /etc/hosts
opencodebox --with ~/another-project1 --with-ro ~/another-project2 serve
opencodebox --exec /bin/bash
EOF
exit 0
}
Expand Down Expand Up @@ -253,6 +258,17 @@ while [[ $# -gt 0 ]]; do
parse_bind_arg "--ro-bind" "$1"
shift
;;
# --exec: override the command to run inside the sandbox
--exec)
shift
if [[ $# -eq 0 ]]; then
echo "Error: --exec requires a value" >&2
echo "Usage: opencodebox --exec /path/to/command [ARGS...]" >&2
exit 1
fi
EXEC_CMD="$1"
shift
;;
# Pass through all other args to opencode
*)
REMAINING_ARGS+=("$1")
Expand Down Expand Up @@ -411,6 +427,9 @@ if [[ "${OPCODE_DIR##*/}" == ".bin" ]]; then
fi
OPCODE_SANDBOX_PATH="/opt/opencode/$OPCODE_LAUNCHER"

# --exec overrides the command run inside the sandbox; default is opencode.
[[ -z "$EXEC_CMD" ]] && EXEC_CMD="$OPCODE_SANDBOX_PATH"

# bwrap --bind hard-fails if the source is missing; opencode v2 keeps state under
# ~/.local/state/opencode, which v1 users and fresh hosts may not have created yet.
STATE_DIR="$HOME/.local/state/opencode"
Expand Down Expand Up @@ -690,5 +709,5 @@ exec bwrap "${BWRAP_ARGS[@]}" -- \
if [ "'"$mise_installed"'" = true ]; then
eval "$(mise activate bash)"
fi
exec "'"$OPCODE_SANDBOX_PATH"'" "$@"
exec "'"$EXEC_CMD"'" "$@"
' -- "$@"
31 changes: 31 additions & 0 deletions tests/test_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,37 @@ def test_passthrough_args_reach_opencode(launcher):
assert command[3:] == ["--", "-s", "session", "run", "prompt"]


def test_exec_overrides_default_command(launcher):
_, command = launcher.captured(launcher.run_launcher("--exec", "/bin/bash"))
assert command[3:] == ["--"]
assert 'exec "/bin/bash"' in command[2]


def test_exec_with_passthrough_args(launcher):
_, command = launcher.captured(launcher.run_launcher("--exec", "/bin/bash", "-l"))
assert command[3:] == ["--", "-l"]
assert 'exec "/bin/bash"' in command[2]


def test_exec_requires_value(launcher):
launcher.assert_rejected("--exec")


def test_default_command_is_opencode(launcher):
_, command = launcher.captured(launcher.run_launcher())
assert command[3:] == ["--"]
assert 'exec "/opt/opencode/opencode"' in command[2]


def test_exec_with_binds(launcher):
extra = launcher.root / "extra"
extra.mkdir()
options, command = launcher.captured(launcher.run_launcher("--with", str(extra), "--exec", "/bin/sh"))
assert has_seq(options, ["--bind", str(extra), str(extra)])
assert command[3:] == ["--"]
assert 'exec "/bin/sh"' in command[2]


def test_reject_sensitive_and_bad_binds(launcher):
launcher.assert_rejected("--with", str(launcher.home))
launcher.assert_rejected("--with", "/")
Expand Down
Loading