This plugin runs code and starts model sessions on your behalf. This page states what is trusted, what is not, and what each control does — including what it does not do.
| Code | Trust | Gate |
|---|---|---|
Durable Workflows (.opencode/workflows/**/*.ts, global config dir) |
Trusted like any plugin or script in your project. They run when a model or a person names them. | Review them like code you commit. |
Inline Workflows (source written by a model, workflow_inline) |
Untrusted. They run with the full privileges of the OpenCode service process — the same user, files, network and environment. | A person approves each one, or the project is marked "always allow", or the plugin option inline says so. |
| Units (the sessions a Workflow starts) | Ordinary OpenCode sessions. | OpenCode's permission rules, plus the Workflow's rules and the engine's policy (below). |
Inline Workflows are not sandboxed. The script can import "node:fs" or spawn processes whatever the
capability settings are. The approval is the control. Read the whole source before you approve, and prefer
durable Workflows for anything you run often (workflow({ save_run }) or workflow_inline({ save })).
inline: "ask"(default): an inline Run startsqueuedand publishes anapprovalinteraction with the whole source, its byte count and SHA-256. The TUI (/workflows, thena) shows the source with line numbers in a scrolling view (PgUp/PgDn, Home/End, mouse wheel); the web app shows it in a scrolling code box. Choices: Run once, Always for this project (stored in the plugin's storage for this project directory), Reject. The request has no time limit: it waits until someone answers or the Run is stopped, and the model'sworkflow_inlinecall waits with it.- The script is loaded (so its top-level code runs) only after the approval, on every path: a new Run, a resumed one (a refused Run keeps its script, so resume asks again) and a save.
- Saving inline source as a durable Workflow (
workflow_inline({ save }),workflow({ save_run })) asks the same way, with Save or Reject: a saved Workflow later runs by name without approval.save_rundoes not ask again for a Run whose exact source a person approved to run. - Nobody attached (headless
opencode run, CI): the Run or save is refused with a message, unless the project was approved before. inline: "allow": no approval (use only where you would run any model-written script).inline: "deny": inline Workflows never run.inlineCapabilities: false: inline Runs get noctx.$,ctx.file,ctx.fetch(defence in depth only; see above).
Every Unit session gets, after the subagent's own rules and your Workflow's meta.permissions / per-Unit
permissions (later rules win):
workflow_resultallowed (typed results),questionallowed (the engine answers it),workflowandworkflow_inlinedenied (no recursion).
When a Unit's tool call hits an ask rule, the engine's policy decides (meta.interaction.permissions):
ask(default): a person attached (TUI, web app) gets the request as a Run interaction; headless, it is denied with a message the Unit can act on. A headless Run never hangs on a permission.auto: allowed once, silently. Prefer explicitallowrules for exactly what the Units need.deny: always denied with a message.
- Paths (and the shell's working directory) must resolve inside the Run's project, through symlinks.
- Shell commands and fetches stop when the Run stops; shell commands also time out (120 s default).
- Every call is recorded in the Run's activity (
kind: "capability"), visible in the TUI and web app. - Tagged-template interpolations in
ctx.$are shell-quoted; a plain string is passed tosh -cas is.
Defaults (plugin option limits, or per Workflow meta.limits): 1 000 Units per Run, 4 096 items per
parallel / pipeline call, 250 model requests per Unit. A Run that hits a limit ends failed with a
message that names the limit. meta.budget: { tokens, hard: true } stops a Run when its output-token budget
is spent.
The Gateway serves the protocol and the web app over HTTP. It is one listener per OpenCode service process.
Setting (gateway.*) |
Default | Effect |
|---|---|---|
enabled |
true |
false turns it off (the TUI keeps working over RPC). |
bind |
loopback |
loopback (127.0.0.1), lan (0.0.0.0), tailscale (your 100.64.0.0/10 address), or an IP. |
port |
4320 |
The next free port is used if it is taken; the TUI and tool results show the real URL. |
auth |
token |
none removes auth for loopback clients on a loopback bind only. |
allowedOrigins |
[] |
Extra browser origins (CORS and CSRF allow-list). |
web |
true |
Serve the web app. |
Rules the Gateway enforces:
- Host check. Requests must name a host the Gateway serves (defeats DNS rebinding).
- Origin check. A browser write must come from the Gateway's own origin or an allowed origin (defeats CSRF). CORS headers are sent to allowed origins only.
- Reads need no token from a loopback client. Remote reads need a token.
- Every control action (start, stop, restart, resume, answer, save, cleanup) needs
Authorization: Bearer <token>withcontrolscope. Tokens never travel in URLs. - Pairing. A browser on the same machine, on the Gateway's own origin, pairs itself
(
POST /v1/pair/local). A remote browser needs a one-use code from the TUI (/workflows pair), valid for five minutes, exchanged atPOST /v1/pair. - Tokens are stored as SHA-256 hashes in
$XDG_STATE_HOME/opencode-dynamic-workflows/gateway-tokens.json(mode 0600). Delete an entry to revoke it: the running Gateway re-reads the file when it changes. A missing or unreadable file grants nothing. - Writes are rate-limited per client. Control actions are recorded in the Run's activity with the device name.
- Responses carry a strict Content-Security-Policy,
nosniff,no-referrerandframe-ancestors 'none'.
Exposing the Gateway beyond loopback (lan) makes the web app reachable by anyone on that network; reads
still need a token, but prefer tailscale or an SSH tunnel.
| Path | Content |
|---|---|
<project>/.opencode/workflows/runs/<runId>/ |
the Run journal: run.json, units.jsonl, script.ts (the source, inline included), result.json. Add it to .gitignore if you do not want it committed. |
$XDG_CACHE_HOME/opencode-dynamic-workflows/workflows/ |
loaded Workflow modules, one directory per content hash. Safe to delete. |
$XDG_STATE_HOME/opencode-dynamic-workflows/ |
Gateway device tokens. |
Unit sessions stay in OpenCode's database like any session. The server plugin cannot delete sessions,
so deletion happens in the TUI and only on your action: d on a finished Run, or /workflows cleanup for the
Runs that retention: "delete-on-success" marked. The web app can only mark a Run for cleanup.