opencode can read and write files and run shell commands in your projects. Anyone who can use this UI can do the same. Treat access to it like SSH access to the box.
| layer | protects against | how |
|---|---|---|
| Reverse-proxy auth (tinyauth, Authelia…) | strangers on the internet | one Traefik router for pages, API, proxy and SSE stream; the forward-auth cookie covers all of it |
| opencode basic auth | other containers / hosts reaching opencode | the opencode service is only on the internal network; the web app injects the password server-side, the browser never sees it |
| Origin check (CSRF) | a malicious website you visit driving your instance, especially LAN-only ones without any login | every POST/PUT/PATCH/DELETE whose Origin header is not this app's own host gets 403. Requests without Origin (curl, MCP clients, opencode) are unaffected |
| API token (optional) | scripts and MCP clients when the API is exempt from forward auth | NUXT_API_TOKEN: Authorization: Bearer <token> on /api/v1/*, /mcp and /api/opencode/* |
| Secret redaction | API keys and MCP credentials leaking to browsers | /config, /global/config, /config/providers and the list_models MCP tool replace every key/token/secret/password/authorization value with ***, at any depth |
| iframe sandbox | MCP UI apps reaching the opencode API | see MCP UI security |
With NUXT_API_TOKEN set:
- external clients send
Authorization: Bearer <token> - every page load hands the browser an
HttpOnly,SameSite=Strictcookie derived from the token (ocw_ui), so the UI's own requests keep working without the token ever reaching JavaScript
Typical Traefik setup: keep pages behind tinyauth and give /api/v1 and /mcp a second router without the forward-auth middleware. Pages can only be loaded after logging in, so only logged-in browsers get the cookie, while scripts use the token.
labels:
- traefik.http.routers.opencode-web.middlewares=tinyauth@docker
# token-protected API for scripts / MCP clients, no forward auth
- traefik.http.routers.opencode-web-api.rule=Host(`${WEB_DOMAIN}`) && (PathPrefix(`/api/v1`) || Path(`/mcp`))
- traefik.http.routers.opencode-web-api.entrypoints=websecure
- traefik.http.routers.opencode-web-api.tls=true
- traefik.http.routers.opencode-web-api.service=opencode-webBrowser tools (for example the MCP Inspector) send an Origin header and are refused by default. Allow them explicitly:
NUXT_ALLOWED_ORIGINS=http://localhost:6274,https://inspector.example.com- Agent control gives the agent in every chat this app's MCP tools. Tools that change state (reloads, adding or switching MCP servers, prompting or aborting other sessions, schedules, testing an ad-hoc server) are registered with the
askpermission, so a prompt-injected agent cannot use them without your click. Relaxing them toautoon the MCP page is your call. - Testing a local (stdio) MCP server spawns its command on the web app's host, like tool discovery always has, with a minimal environment that excludes the app's own secrets.
NUXT_MCP_LOCAL_DISCOVERY=neverturns both off. - The server editor never receives stored header or environment values: they arrive as
***, and the server substitutes the stored value when an untouched placeholder is saved or tested.
Running on a LAN without a proxy (ports: ["3000:3000"]) means everyone on that network has full agent access. The Origin check stops other websites from using it through your browser. It does not stop DNS rebinding: if that matters to you, put the app behind a proxy that checks the Host header, or use the token.
Please open a private security advisory instead of a public issue.