Skip to content
Merged
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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.21
0.0.22
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ with ddev — see the [README](../README.md) whether the kit fits your setup.

- [CLI — scripts and flags](reference/cli.md)
- [Files and paths](reference/files.md)
- [Ecosystem compatibility](reference/compatibility.md) — which tools that
spawn opencode work with the kit, and how
- [Audit log](reference/audit-log.md)
- [Glossary](reference/glossary.md)

Expand Down
31 changes: 31 additions & 0 deletions docs/concepts/security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ Concretely:

- the agent's processes run as `opencode`, never as the developer — no
credentials, SSH keys, or dotfiles in `/home/<developer>` are reachable,
- the agent-resources migration (`--migrate-agents`) moves `~/.agents`
whole but only `~/.claude/skills` — credentials such as
`~/.claude/.credentials.json` never cross into the agent's home,
- containers run under the `opencode` host UID via a rootless backend — the
agent can never reach a root-equivalent docker socket,
- no code path executes as the developer (no RunAs-developer sudoers rule).
Expand Down Expand Up @@ -92,3 +95,31 @@ the same: the agent user must end up as "other", with no bits.

Not sure what your values are? Check them with `id -u` / `id -g` in a fresh
WSL terminal (logged in as the default user).

## Other root-equivalent surfaces (audit)

Docker is not the only tool whose daemon socket means root. `status.sh`
ships a **report-only audit** ("Root-equivalent access" section) that
checks the surfaces below for agent reachability — the kit never changes
them, because removing access is an admin decision:

| Surface | Why it matters | Default on WSL2 |
|---|---|---|
| Rootful docker socket (`/var/run/docker.sock`) / `docker` group | full root, classic escape | installed by many devs — fine while the agent user stays out of the group |
| Docker Desktop / Rancher Desktop integration sockets under `/mnt/wsl` | root-equivalent daemon in the Windows VM, exposed **world-usable** to every WSL distro user | present whenever Docker Desktop's WSL integration is enabled — remove the agent's distro from it or restrict the socket |
| `containerd` socket | full root | root-only by default |
| LXD/LXC, libvirt (`lxd`, `libvirt` groups + sockets) | full root (host VMs/mounts) | not installed by default |
| `sudo`/`admin`/`wheel`/`disk`/`snap` groups for the agent user | full root | never granted by the kit — the audit catches later manual grants |
| `wireshark`/`adm`/`systemd-journal` groups | not root, but packet/log access can leak credentials | not granted by the kit |
| Windows interop (executing `.exe` via `/mnt/c`) | runs code as the Windows session user | blocked by the `/mnt/c` restriction above; the audit probes the exec bit explicitly |

Deliberately **not** flagged:

- `snapd`'s socket is mode 666 by design, but snapd gates write requests
by peer credentials (root only) — not root-equivalent for the agent.
- `newuidmap`/`newgidmap` are setuid, but required by the rootless
backends and bounded to the ranges in `/etc/subuid`.

The audit is stat math only — no privileged probes, no prompts. Override
the socket list with `ROOT_EQUIV_SOCKS="…"` (same pattern as
`LEAK_SCAN_DIRS`).
61 changes: 43 additions & 18 deletions docs/concepts/wrapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ Every `opencode` invocation goes through the wrapper at
`/usr/local/bin/opencode`:

1. **Validate working directory** — the current directory must be inside a
path listed in `projects.conf`. Otherwise opencode does not start.
path listed in `projects.conf`. Otherwise the interactive start is
refused (headless invocations skip this — see
[below](#headless-invocations-serve-run-queries)).
2. **Detect container tools** — if the project's `opencode.jsonc` broadly
allows docker/ddev, the wrapper attaches the configured rootless
backend (no confirmation dialog — the state is visible in the TUI,
Expand All @@ -29,23 +31,46 @@ The wrapper prints its banner and starts opencode **immediately** — no
0.0.21: the question was effectively always answered with yes, and the
kit's state is now visible inside the TUI at all times).

## The serve exception (headless start)

`opencode serve` does not go through these steps. Third-party UIs like
OpenChamber spawn the server non-interactively — stdin is `/dev/null` and
stdout is parsed for the `opencode server listening on <url>` line — so
any interactive prompt would break the startup (and `read` on a closed
stdin kills the wrapper before it execs). Since 0.0.21 the wrapper is
prompt-free everywhere; serve mode additionally:

- skips the project-directory check — the server accepts sessions per
client request, and the soft permission layer (global + per-project
`opencode.jsonc`) still applies to every session,
- prints nothing on stdout; diagnostics (shadow binary, backend
warnings) go to stderr,
- resolves container tools silently — a server serves many projects, so
there is no single opt-in to confirm; whether a session may actually
use docker/ddev stays decided by the `opencode.jsonc` rules.
## Headless invocations (serve, run, queries)

`opencode serve` and the other non-interactive subcommands do not go
through the interactive path. Third-party tools spawn opencode
non-interactively and parse its stdout:

| Invocation | Who uses it | What is parsed |
|---|---|---|
| `opencode serve` | OpenChamber, cezar, CodeWalk, the VS Code extension | `opencode server listening on <url>` |
| `opencode run` | CI runners, kanban orchestrators, eval harnesses | `--format json` / stream-json events |
| `opencode acp` | IDE agents (Agent Client Protocol) | JSON-RPC over stdio |
| `opencode models`, `agent`, `providers`, `export`, … | cezar (model discovery), scripts | JSON/plain listings |

A banner or prompt on stdout would break those parsers (and `read` on a
closed stdin would kill the wrapper under `set -e`), and the
project-directory check would refuse tools running from git worktrees or
temporary checkouts. Headless invocations therefore:

- skip the project-directory check — the soft permission layer (global +
per-project `opencode.jsonc`) still applies to every session,
- print nothing on stdout; diagnostics (shadow binary, backend warnings)
go to stderr,
- resolve container tools silently — `serve` always attaches them (a
server serves many projects); `run`/queries attach them when the CWD's
project config opts in. Whether a session may actually use docker/ddev
stays decided by the `opencode.jsonc` rules.

What counts as headless: `serve`, `acp`, the query subcommands (`models`,
`agent`, `providers`, `session`, `export`, `import`, `stats`, `account`,
`github`, `pr`, `mcp`, `plug`, `db`, `generate`, `web`, `debug`,
`uninstall`, `upgrade`), and `run` when a message argument is given or
stdin is piped. Interactive TUI starts (no subcommand, flags-only
starts, `tui`, `attach`, or `opencode run` on a terminal without a
message) keep the banner and the project-directory check.

Which ecosystem tools use which invocation — and the verified status of
each — is tracked in the [compatibility
matrix](../reference/compatibility.md) (issue #42 research). Tools that
are opencode *plugins* (awesome-opencode) load inside the agent process
and never touch the wrapper.

`OPENCODE_SERVER_PASSWORD` and `OPENCODE_SERVER_USERNAME` are preserved
across the `sudo -u opencode` exec, so the Basic-auth credentials a UI
Expand Down
14 changes: 8 additions & 6 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ It then asks only the essential questions:
read your repositories: the group baseline covers `.git/`, and the kit
sets `safe.directory` for the `opencode` user (no "dubious ownership"
errors).
3. **Agent resources** — when you already have `~/.agents` or `~/.claude`
(skills; opencode auto-loads both from `<dir>/skills/**/SKILL.md`),
bring them into `/home/opencode` so the agent can use them:
**move** (recommended — one canonical copy; you keep read/write via the
sharing group), **copy** (both sides keep their own, may drift) or
**skip**. Non-interactive installs move;
3. **Agent resources** — bring the resources opencode auto-loads into
`/home/opencode` so the agent can use them: `~/.agents`
(**whole directory** — it is opencode's own namespace) and
`~/.claude/skills` (**skills/ only** — the rest of `~/.claude` is
Claude Code's home, so credentials like `.credentials.json` stay in
your home). Choose **move** (recommended — one canonical copy; you
keep read/write via the sharing group), **copy** (both sides keep
their own, may drift) or **skip**. Non-interactive installs move;
`--migrate-agents move|copy|skip` forces a choice.
4. **ddev settings** — dev-owned projects (default): the kit writes
`disable_settings_management: true` into each project's
Expand Down
9 changes: 6 additions & 3 deletions docs/how-to/openchamber.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ OpenChamber can manage the opencode server itself: it spawns
connects to it. That spawn is non-interactive — stdin is closed and
OpenChamber waits for the `opencode server listening on …` line on stdout.

The kit's wrapper has a **headless serve mode** for exactly this spawn
The kit's wrapper has a **headless mode** for exactly this spawn
style: when the first argument is `serve`, it prints nothing on stdout,
asks nothing, and starts the server as the `opencode` user directly.
(Since 0.0.21 the wrapper is prompt-free in general — no `Press Enter`,
no `[Y/n]` — but serve mode additionally keeps stdout clean for
no `[Y/n]` — but headless mode additionally keeps stdout clean for
parsers.) Project-directory checks do not apply to
`serve` — sessions still get the global and per-project `opencode.jsonc`
permission rules (see [the wrapper](../concepts/wrapper.md)).
permission rules (see [the wrapper](../concepts/wrapper.md)). The same
headless contract covers other ecosystem tools — `opencode run`
orchestrators (cezar, CI runners) and `opencode acp` IDE agents — see
[headless invocations](../concepts/wrapper.md#headless-invocations-serve-run-queries).

## Just run it

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ curl -fsSL https://raw.githubusercontent.com/steffenmaechtel/opencode-permission
| `--projects <path...>` | Pre-define project roots, skip interactive selection (consumes every following non-flag argument) |
| `--container-backend <docker-rootless\|podman-rootless>` | Non-interactive backend choice |
| `--secure-git-config` | Enable `.git/config` hardening up front |
| `--migrate-agents <move\|copy\|skip>` | Bring the developer's `~/.agents` + `~/.claude` (skills) into `/home/opencode` — move (recommended), copy, or skip; default: ask (`--yes` = move) |
| `--migrate-agents <move\|copy\|skip>` | Bring the developer's agent resources into `/home/opencode`: `~/.agents` **whole** (opencode's own namespace) + `~/.claude/skills` **skills/ only** (credentials like `~/.claude/.credentials.json` stay in your home) — move (recommended), copy, or skip; default: ask (`--yes` = move) |

Flags may appear in any order; unknown options abort the install.

Expand Down
57 changes: 57 additions & 0 deletions docs/reference/compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Ecosystem compatibility

This page lists third-party tools that spawn or front opencode, how they
invoke it, and whether they work on a machine where the kit owns the
`opencode` command. Last verified: Aug 2026 (opencode v1.18.15).

## How the kit intercepts tools

Everything that spawns `opencode` through `PATH` gets the kit's wrapper
at `/usr/local/bin/opencode` automatically — no tool configuration
needed. Tools that take an explicit binary path (`OPENCODE_BINARY`,
`CEZ_OPENCODE_BIN`, …) must point at the wrapper, not at
`~/.opencode/bin/opencode`.

Non-interactive invocations (`serve`, `run`, `acp`, query subcommands)
run through the wrapper's [headless
contract](../concepts/wrapper.md#headless-invocations-serve-run-queries):
stdout stays machine-clean, no prompts, no project-directory refusal.
The soft permission layer (global + per-project `opencode.jsonc`) applies
to every session regardless of which tool started it.

## Compatibility matrix

| Tool | Kind | Invocation | Status |
|---|---|---|---|
| [OpenChamber](https://openchamber.dev) (web/desktop/VS Code) | UI | `opencode serve` | works (headless serve since 0.0.16) |
| CodeWalk | remote UI | user-run `opencode serve` | works |
| OpenCode Mobile, P4OC | mobile clients | user-run `opencode serve` | works |
| [cezar](https://github.com/lukaszuznanski/cezar) | orchestrator | `opencode serve` + `opencode models` | works (headless queries since 0.0.22) |
| Vibe Kanban | kanban orchestrator | `opencode run` | works (headless run since 0.0.22) |
| eval-harness | skill testing | `opencode run` | works (headless run since 0.0.22) |
| opencode-actions | CI (GitHub Actions) | `opencode run` | works (headless run since 0.0.22) |
| Telegram/harness bots (kimaki, GolemBot, …) | chat bots | `opencode run` / `serve` | works |
| opencode.nvim, opencode-vim | editor frontends | `opencode run` / SDK | works |
| ACP-based IDE agents | IDE | `opencode acp` (JSON-RPC stdio) | works (headless acp since 0.0.22) |
| awesome-opencode plugins | plugins | load inside the agent process | unaffected — the soft layer applies to their tool calls |
| [OpenHarness](https://github.com/HKUDS/OpenHarness) | own harness | does not invoke opencode (own auth: `~/.claude/.credentials.json`, `~/.codex/auth.json`) | no interaction with the kit |

"Works" means: the tool's spawn pattern passes the wrapper and opencode
runs under the kit's UID separation with the soft permission layer
enforced. It does not mean the kit audits or endorses the tool itself.

## Caveats

- **Absolute-path spawns.** A tool hardcoding `~/.opencode/bin/opencode`
bypasses the wrapper — the kit's [bypass
guards](../concepts/wrapper.md) detect and warn about that binary.
Point the tool's binary setting at `/usr/local/bin/opencode`.
- **sudo-spawning tools.** Tools that spawn opencode under `sudo` run it
as root — outside the kit's model. Report such a tool and we will take
a look; the kit deliberately grants no root path.

## Keeping this page current

The ecosystem moves fast — if a tool breaks or a notable one is missing,
please open an issue at
[steffenmaechtel/opencode-permissions-kit](https://github.com/steffenmaechtel/opencode-permissions-kit/issues).
30 changes: 24 additions & 6 deletions files/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1314,9 +1314,16 @@ sudo chmod 2775 /home/opencode/.config /home/opencode/.config/opencode /home/ope
# DEVELOPER's home they are invisible to the agent, which reads
# /home/opencode. Offer move (recommended — one canonical copy, the
# developer keeps rw via the sharing group), copy (both sides keep their
# own, may drift) or skip. Applies to both folders; the choice is made
# once and applies to whichever exist.
MIGRATE_AGENT_DIRS=".agents .claude"
# own, may drift) or skip. The choice is made once and applies to
# whichever exist.
#
# Scope (review 0.0.22): ~/.agents is opencode's OWN directory and
# migrates WHOLE — everything in it belongs to the agent namespace
# anyway. ~/.claude is Claude Code's home and carries OAuth tokens
# (.credentials.json) and account state at its top level — only
# skills/ crosses (the only part opencode loads), so credentials
# never reach the agent's group-readable home.
MIGRATE_AGENT_DIRS=".agents .claude/skills"
_opk_migrate_one() {
_opk_src="/home/$DEFAULT_USER/$1"
_opk_dst="/home/$OPENCODE_USER/$1"
Expand All @@ -1330,9 +1337,19 @@ _opk_migrate_one() {
# Sharing baseline: opencode owns, the developer keeps rw
# through the group (dirs setgid so new files inherit it). The
# top dir gets the explicit 2775 — mkdir's mode depends on the
# process umask.
# process umask. Sub-path migrations (.claude/skills) also
# normalize the parent the kit just created (mkdir -p leaves it
# root-owned otherwise) — never /home/opencode itself, whose
# mode 2750 (no world bit) is by design.
sudo chown -R "$OPENCODE_USER:$OPENCODE_GROUP" "$_opk_dst"
sudo chmod 2775 "$_opk_dst"
case "$1" in
*/*)
_opk_parent="$(dirname "$_opk_dst")"
sudo chown "$OPENCODE_USER:$OPENCODE_GROUP" "$_opk_parent" 2>/dev/null || true
sudo chmod 2775 "$_opk_parent" 2>/dev/null || true
;;
esac
sudo find "$_opk_dst" -type d -exec chmod g+rwxs {} + 2>/dev/null || true
sudo find "$_opk_dst" -type f -exec chmod g+rw {} + 2>/dev/null || true
if [ "$_opk_ag" = m ]; then
Expand All @@ -1357,7 +1374,8 @@ if [ "$DEFAULT_USER" != "$OPENCODE_USER" ] && [ "$_opk_have_agent_dirs" = true ]
if [ "$INTERACTIVE" = true ]; then
while true; do
echo "" >&2
printf "[?] Existing agent resources (~/.agents, ~/.claude — skills etc.) — bring them into /home/%s?\n" "$OPENCODE_USER" >&2
printf "[?] Existing agent resources (~/.agents, ~/.claude/skills) — bring them into /home/%s?\n" "$OPENCODE_USER" >&2
echo " (~/.agents moves whole; from ~/.claude only skills/ — credentials like .credentials.json stay in your home)" >&2
echo " (m) Move — recommended: one canonical copy; you keep read/write via the $OPENCODE_GROUP group" >&2
echo " (c) Copy — duplicate; both sides keep their own copy (may drift)" >&2
echo " (s) Skip — leave them in your home (the agent cannot use them)" >&2
Expand All @@ -1376,7 +1394,7 @@ if [ "$DEFAULT_USER" != "$OPENCODE_USER" ] && [ "$_opk_have_agent_dirs" = true ]
case "$_opk_ag" in
m|c) for _opk_d in $MIGRATE_AGENT_DIRS; do _opk_migrate_one "$_opk_d"; done ;;
s)
ui_detail "skipped: ~/.agents and ~/.claude stay in your home — the agent cannot use these skills"
ui_detail "skipped: ~/.agents and ~/.claude/skills stay in your home — the agent cannot use them"
log "agents migration: skipped by choice"
;;
esac
Expand Down
Loading