Security-focused rootless Podman setup for running OpenCode with restricted filesystem access and Podman-in-Podman support.
# From your project directory:
./path/to/opencode.sh
# Update to latest version
./path/to/opencode.sh update-
Rootless Podman on the host (tested with 3.4.4 on Ubuntu 22.04)
-
Podman user API socket enabled:
systemctl --user enable --now podman.socket -
For
--kvm: membership in the hostkvmgroup
On first run, the container builds with the latest OpenCode version. The version is stored in .opencode-version next to opencode.sh. Subsequent runs use the stored version (no automatic update checks). Run ./path/to/opencode.sh update to update to the latest version.
# Mount current working directory
./path/to/opencode.sh
# Mount specific path(s) - first path is the working directory
./path/to/opencode.sh /path/to/project
./path/to/opencode.sh /path/to/project /path/to/otherdir
# Update to latest version
./path/to/opencode.sh updateWhen paths are provided, they are resolved relative to the current working directory. Relative paths (e.g., ./upmon) are supported. All paths are validated before starting the container.
- Project directory (first path argument, or current working directory if no args)
- Additional directories (subsequent path arguments, each mounted at the same path inside the container)
- This tooling repo (mounted at the same path) - provides the
shared-browserskill andbrowser-cli/browser.shto every project ~/.config/opencode/- OpenCode settings and API key~/.local/share/opencode/- OpenCode data~/.ssh/configand~/.ssh/sockets- SSH access (OpenCode can SSH without password or key through existing connections shared via ControlPath)$XDG_RUNTIME_DIR/podman/podman.sock- rootless Podman API socket (for running tests)/tmp/.X11-unix- X11 forwarding (clipboard).opencode/tmp/<project-key>- per-project scratch space mounted at/tmp/opencode
The per-project scratch space keeps OpenCode's temp directory writable even when
the container image contains a stale root-owned /tmp/opencode. Wipe all
projects' temp directories with:
./cleanup-tmps.shThe container ships a statically linked podman-remote client whose version
matches the host's Podman (passed as the PODMAN_VERSION build arg; the libpod
remote API is not cross-version compatible). A docker shim forwards to it, so
both podman ... and docker ... work inside the container.
Both talk to the host's rootless, user-owned Podman API socket — no
privileged daemon and no root-equivalent /var/run/docker.sock is mounted.
Containers started from inside OpenCode run with the host user's privileges,
and the launcher uses --userns=keep-id so file ownership in mounted
directories stays consistent.
browser.sh runs one detached Chromium per project, headless by default. The
agent drives it over an auto-allocated localhost CDP endpoint; no window is
mapped on your desktop, so browser work cannot steal focus. Different projects
get separate containers, profiles and endpoints, so their sessions are
independent. Open tabs and session cookies are restored across stops and mode
switches (use browser.sh rather than raw podman stop so Chromium flushes
state).
For human-in-the-loop logins, expose the same profile in a headful window and hide it again afterwards:
./browser.sh start # build image on first run, then start headless
./browser.sh login # show the same profile headful for a login
./browser.sh headless # hide it again (same profile/tabs)
./browser.sh status
./browser.sh list
./browser.sh stop
./browser.sh stop-allLogins persist in browser-profile/<project-key>/. Agents drive the browser
through browser-cli/ (a small playwright-core CLI speaking CDP); the
global shared-browser skill documents the workflow, so OpenCode loads browser
instructions on demand instead of carrying MCP tool schemas in every request.
Install once: cd browser-cli && npm install. The skill source lives in
skills/shared-browser/ and is registered via skills.paths in the host's
~/.config/opencode/opencode.json.