A project is a directory, not a process

How I stopped babysitting terminals and let one small server drive every coding project on my home box.

The problem

I run a lot of coding-agent sessions across many projects on a home server (<home-server>) and a few other machines. Every one of them assumes you’re sitting in front of a terminal. I wanted to reach them from a phone — while commuting, without a laptop, without SSH-ing into five boxes.

opencode has a headless mode: opencode serve. Before building anything, I wanted to know one thing: can a server control a project it never started?

The probe

I started a server on a throwaway port and hit the API.

curl http://127.0.0.1:4096/global/health
# {"healthy":true,"version":"1.18.34"}

Then I listed “projects” — and this is where the mental model broke, in a good way. The fresh server reported 10 projects it had never launched. It knew about them because they were in the shared database, but it hadn’t started any.

Next test: create a session in a directory the server had never seen.

curl -X POST \
  -H "x-opencode-directory: /tmp/opencode-probe" \
  -H 'content-type: application/json' \
  -d '{"title":"probe"}' \
  http://127.0.0.1:4096/session

It worked. That header is the whole trick: it scopes a session to a directory.

The thing that clicked

A “project” in opencode is a directory, not a running process.

That single sentence changed the design. It means:

  • One long-lived server can drive every project on the machine. No per-project daemon.
  • The server never needs to “start” a project — it just needs its filesystem to see the path.
  • “Managing many projects” collapses into “routing a request to a directory.”

I sent a real prompt through the same path and got a reply back. End to end:

curl -X POST -H "x-opencode-directory: /tmp/opencode-probe" \
  -H 'content-type: application/json' \
  -d '{"parts":[{"type":"text","text":"Reply with exactly: REMOTE-OK"}]}' \
  http://127.0.0.1:4096/session/<id>/message
# ... "REMOTE-OK"

Things worth knowing early

  • Auth is off by default. Set OPENCODE_SERVER_PASSWORD the moment the server is reachable beyond loopback. It’s HTTP basic auth, user opencode.
  • The header is a trust boundary. x-opencode-directory lets any caller act as that project, with your user’s rights. There is no sandbox between projects. Any bridge you build must pass only paths from a fixed allowlist.
  • Server and TUI coexist. A TUI, opencode run, and opencode serve are all client+server, and they share one config and one database. I ran a standalone server while three TUIs were open and it listed all their sessions.
  • The API you want is v1. There’s also a newer surface (more on that in another post) — but the complete, global one is /project, /session, /event.

Takeaway

The expensive-sounding feature — “remotely manage dozens of projects” — turned out to be a thin routing problem over a very capable server. The mental model was the hard part; the code is trivial. Once you internalize directory, not process, the rest is plumbing.