The server, CI, and GitHub
The last lesson had you running OpenCode headless from your own terminal — one opencode run per task, output on your screen. That’s the right shape for a script you babysit. It’s the wrong shape for the thing feedmill actually needs: an OpenCode review on every pull request, and an agent that can pick up an issue and open a PR without you in the loop at all. For that you stop thinking of OpenCode as a command you run and start thinking of it as a service something else talks to.
That reframe is already baked into the tool. The TUI you’ve used all course isn’t OpenCode — it’s one client of an OpenCode server. Drop the TUI and you can keep the server.
The server is the product; the TUI is just a client
Section titled “The server is the product; the TUI is just a client”OpenCode is a client/server program. When you launch the TUI it quietly starts a server, picks a port, and connects to it — the interface is a view onto a backend that does the real work. You can start that backend on its own with no interface attached:
$ opencode serve --port 4096opencode server listening on http://127.0.0.1:4096That’s a headless HTTP server exposing OpenCode’s full API — sessions, models, tools, the loop, all of it — over the network. It publishes its own OpenAPI spec at /doc, so anything that can speak HTTP can drive it. The TUI is now just the most convenient of many possible clients, not a requirement.
By default serve binds to 127.0.0.1 and stays local. The moment you bind it anywhere reachable, lock it down. OpenCode reads OPENCODE_SERVER_PASSWORD and turns on HTTP basic auth when it’s set; the username defaults to opencode (override with OPENCODE_SERVER_USERNAME). The same basic-auth pair guards opencode web too, not just serve:
$ OPENCODE_SERVER_PASSWORD=$(openssl rand -hex 16) \ opencode serve --hostname 0.0.0.0 --port 4096This is the same instinct you carried through the whole share-and-headless chapter: when feedmill’s work leaves your machine, decide on purpose what’s allowed to reach it. A server with no password on a routable host is an open agent anyone can run commands through.
Attach a client when you need to look inside
Section titled “Attach a client when you need to look inside”A headless server is great until something goes wrong inside it and you want eyes on the running session rather than a log scrape. You don’t restart anything — you attach a client to the server that’s already up:
$ opencode attach http://localhost:4096That gives you the familiar TUI, but pointed at the existing backend instead of spinning up its own. The session the server is running is right there in front of you. The same --attach flag works on run when you’d rather drive the live server with a one-shot prompt:
$ opencode run --attach http://localhost:4096 \ "summarise what the last review session flagged"If the server is password-protected, both attach and run take the creds inline with --username/-u and --password/-p — or, if you skip the flags, they fall back to OPENCODE_SERVER_USERNAME (default opencode) and OPENCODE_SERVER_PASSWORD from the environment, the same pair serve reads:
$ opencode attach http://feedmill-ci:4096 -u opencode -p "$OPENCODE_SERVER_PASSWORD"So the server can run unattended for a feedmill cron or a CI job, and you can still walk up to it interactively the instant you need to. Server and client are decoupled on purpose.
Put it in a container for CI
Section titled “Put it in a container for CI”CI wants one thing from a tool: that it runs the same way every time, on a machine that didn’t exist a minute ago. A container gives you exactly that — a fixed OpenCode version, a clean filesystem, and no dependence on whatever happens to be installed on the runner. That’s the difference between “the review passed on my laptop” and “the review runs identically on every PR for the next year.” There’s now an official image at ghcr.io/anomalyco/opencode (Debian and Alpine variants) you can base on, but in practice you’ll still own a small Dockerfile on top of it: pin OpenCode plus feedmill’s toolchain, then make opencode run the entrypoint.
The pattern is the one you already know from the headless lesson, moved inside an image: a container that has OpenCode and feedmill’s Go toolchain, your provider key passed in as a secret, and a single opencode run as the entrypoint. Because feedmill is a typed Go codebase wired up with the LSP from earlier in the course, the containerised agent gets the same diagnostics you do locally — a parser change that breaks a type is caught in CI, not in production at 3am when a feed’s JSON shifts shape.
# inside the CI container$ opencode run \ "review the diff on this PR. flag dedup or feed-parsing regressions and any LSP diagnostics the change introduces. be terse."One provider key, one container, one command — and you swap the model per the job exactly like you’ve done all course: a cheap fast model for routine diff review, a stronger one reserved for the runs that touch the dedup core.
Let OpenCode act on the repo directly
Section titled “Let OpenCode act on the repo directly”Wrapping OpenCode in your own CI script is the general escape hatch. On GitHub you usually don’t need it — OpenCode ships a first-class GitHub connector, so the agent participates in the repo platform natively instead of being a step you wire by hand. GitLab has a path too, but a less polished one (more on that below).
On GitHub the fastest path is the guided installer:
$ opencode github installIt walks you through adding the GitHub App and drops a workflow at .github/workflows/opencode.yml. After that the agent lives in your issues and PRs. Mention /opencode (or the short /oc) in a comment and it runs inside your GitHub Actions runner — your code never leaves your infrastructure:
# as a comment on a feedmill issue/oc the JSON Feed parser drops items with no published date. figure out why and open a PR with a fix and a test.OpenCode reads the issue, works the problem in the runner, and opens a pull request with the change — the full loop you’ve watched all course, now triggered from a comment instead of your terminal. And on a pull_request event with no prompt, it defaults to reviewing the PR, which is the every-PR review you came to this lesson for, without a custom container at all.
The workflow needs the right permissions to do this: id-token: write always, plus contents: write and pull-requests: write if you want it creating branches and PRs, and issues: write if it should comment back on issues.
GitLab is supported but not symmetric — there’s no opencode gitlab install to mirror the GitHub installer. Instead you wire it into your pipeline by including a CI/CD component in .gitlab-ci.yml (or by going through GitLab Duo):
include: - component: $CI_SERVER_FQDN/nagyv/gitlab-opencode/opencode@2And the trigger differs: on GitLab you at-mention @opencode in a comment (configurable to a different phrase) rather than using the /opencode or /oc slash command GitHub uses.
When you’re embedding, reach for ACP
Section titled “When you’re embedding, reach for ACP”A CI script shells out to OpenCode. The GitHub connector lives inside Actions. But if you’re building a tool that hosts OpenCode — an editor, an internal dashboard, your own automation surface — don’t drive the HTTP API by hand. OpenCode speaks ACP, the Agent Client Protocol, and exposes it through opencode acp. ACP is the standard host-integration layer that editors use to embed agents, and it’s the cleanest path for embedding OpenCode in another tool too: you talk a defined protocol instead of reverse-engineering an API surface that drifts.
You won’t need ACP to get feedmill’s PR reviews running — the connector and the container cover that. It’s the door you walk through the day feedmill stops being the thing OpenCode reviews and becomes a thing that has OpenCode inside it.
That’s the headless half of OpenCode end to end: a server you can run with no interface, a client you attach when you want to see inside, a container that makes CI reproducible, and a native GitHub connector (plus a GitLab path) that lets the agent act on the repo on its own. With sharing and headless operation both in hand, the next move is to make the daily, interactive loop fast — the editor, LSP, and the habits that compound.