Runner Setup
Run tasks locally with a standalone worker that connects to buildd.dev
Runner Setup
The Runner is a standalone worker that connects to buildd.dev. It runs on your machine (or a remote workspace like Coder) and executes tasks using the Claude Agent SDK.
Prerequisites
- Bun runtime
- A buildd.dev account with an API key
Quick Start
cd apps/runner
bun install
BUILDD_API_KEY=bld_xxx PROJECTS_ROOT=~/projects bun startThe runner is headless by default — there is no web UI to open. It starts an
HTTP server only when you pass --debug or set PORT. Without one of those,
nothing is listening on port 8766 and http://localhost:8766 will not connect.
Configure it in one of three ways:
BUILDD_API_KEYenv var — the simplest path, and it takes priority over the config file.buildd login— writesapiKey(andbuilddServer) into~/.buildd/config.json.bun start --debug— starts the debug HTTP server on port 8766, which exposes an OAuth login route (/auth/login) and aPOST /api/configendpoint. Only in this mode does openinghttp://localhost:8766do anything.
With no API key and no serverless flag, a headless runner prints
No API key — run with --debug or set BUILDD_API_KEY and does no work.
PROJECTS_ROOT is effectively mandatory: the runner exits at startup if it
cannot resolve at least one project root.
Configuration
All config is stored in ~/.buildd/config.json. The file path can be overridden
with BUILDD_CONFIG, and the containing directory with BUILDD_HOME.
Config Fields
| Field | Type | Default | Description |
|---|---|---|---|
apiKey | string | — | Buildd API key (bld_xxx) |
serverless | boolean | false | Run without an API key; forced to false whenever a key resolves |
model | string | claude-sonnet-4-6 | Model to use (the standard tier default) |
maxConcurrent | number | 3 | Max concurrent workers |
acceptRemoteTasks | boolean | true | Accept task assignments from the dashboard |
bypassPermissions | boolean | false | Skip permission prompts (dangerous commands still blocked) |
openBrowser | boolean | — | Auto-open browser on startup |
builddServer | string | https://buildd.dev | Server URL override |
localUiUrl | string | see URL Resolution | Direct-access URL for this instance, used only in --debug mode |
pusherKey | string | — | Pusher key for realtime events |
pusherCluster | string | — | Pusher cluster (e.g. us2) |
pusherChannelPrefix | string | '' | Channel prefix for environment isolation |
llmProvider | string | anthropic | LLM provider: anthropic or openrouter |
llmApiKey | string | — | Provider API key (for OpenRouter, etc.) |
llmBaseUrl | string | — | Custom LLM base URL |
maxTurns | number | — | Max turns per worker session; unset means no limit |
Unrecognised keys in config.json are ignored at load time.
Environment Variables
| Variable | Config field | Which wins |
|---|---|---|
BUILDD_API_KEY | apiKey | env |
BUILDD_SERVER | builddServer | env |
MODEL | model | env |
LLM_PROVIDER | llmProvider | env |
LLM_API_KEY | llmApiKey | env |
LLM_BASE_URL | llmBaseUrl | env |
LOCAL_UI_URL | localUiUrl | env (but both lose to headless mode) |
PUSHER_KEY / NEXT_PUBLIC_PUSHER_KEY | pusherKey | env |
PUSHER_CLUSTER / NEXT_PUBLIC_PUSHER_CLUSTER | pusherCluster | env |
PUSHER_CHANNEL_PREFIX | pusherChannelPrefix | env |
MAX_CONCURRENT | maxConcurrent | config.json — see below |
MAX_CONCURRENT is the one exception to "env overrides config". The runner
resolves it as config.maxConcurrent || MAX_CONCURRENT || 3, so any
maxConcurrent value in config.json silently wins and MAX_CONCURRENT is
ignored. To raise concurrency on a runner that has a config file, edit
maxConcurrent — or remove it from the file first.
Env vars with no config-file equivalent:
| Variable | Effect |
|---|---|
PROJECTS_ROOT | Comma-separated project directories (e.g. ~/projects,~/work). The runner exits if none resolve |
PORT | Listen port (default 8766) — setting it at all also enables the HTTP server |
BUILDD_CONFIG | Config file path (default ~/.buildd/config.json) |
BUILDD_HOME | Config directory (default ~/.buildd) |
BUILDD_RUNNER_POLL_MIN | Task-poll cadence in minutes (default 60) |
BUILDD_WORKSPACE_ISOLATION_ROOT | Opt-in structural workspace isolation root |
Note that every one of these is resolved with ||, not ??. A falsy value in
config.json — maxConcurrent: 0, builddServer: "" — falls through to the
next source rather than being honoured.
CLI Flags
| Flag | Effect |
|---|---|
--debug | Start the HTTP server and debug UI on PORT (default 8766) |
--doctor | Run diagnostics and exit non-zero on any error |
--fix | With --doctor, apply available auto-fixes |
--env-verify (or env verify) | Verify the environment |
--json | With --env-verify, emit JSON |
There is no argument parser — any other flag is silently ignored.
URL Resolution
This whole ladder applies only in --debug mode. A headless runner reports
the sentinel headless://<hostname> as its identity and ignores both
LOCAL_UI_URL and localUiUrl entirely, so there is nothing for the dashboard
to link to.
localUiUrl determines how the dashboard links back to a debug-mode instance
(the "Open" and "Open Terminal" buttons). It is resolved once at startup:
- Headless (no
--debug, noPORT) →headless://<hostname>, and the steps below are never reached LOCAL_UI_URLenv varlocalUiUrlin~/.buildd/config.json- Tailscale auto-detect — runs
tailscale ip -4and uses the IPv4 address http://localhost:<PORT>(fallback)
Remote Workspaces (Coder, SSH, etc.)
To get working dashboard links from a remote machine, run in debug mode and set the URL explicitly:
// ~/.buildd/config.json on the remote machine
{
"localUiUrl": "http://<tailscale-ip>:8766"
}Or via env var:
LOCAL_UI_URL=http://<tailscale-ip>:8766 bun start --debugIf the Tailscale CLI is installed on the machine, the IP is auto-detected and you don't need to set anything — again, only in debug mode.
How It Works
The runner keeps two timers, not one:
| Timer | Interval | Purpose |
|---|---|---|
| Liveness ping | 60 seconds | Pure "runner is alive" signal; drives the online indicator |
| Task-poll cycle | BUILDD_RUNNER_POLL_MIN minutes, default 60 | Reconcile local workers, claim-fallback, knowledge ingest |
Both are skipped entirely when serverless is set. Extra heartbeats are also
sent at startup, after a claim, and on demand over Pusher.
Server-side, a runner shows as online while its last beat is within 1.5× the task-poll interval, stale between 1.5× and 2.5×, and is dropped from runner queries beyond 2.5×.
- The runner starts and begins both timers
- The dashboard shows connected instances under "Connected Agents"
- When a task is created and assigned, the runner claims it and spawns a Claude Agent SDK session
- Progress is reported back to buildd.dev in real-time via API + Pusher
- In
--debugmode, the dashboard can link directly to the runner for live terminal output