Boof Runner security model
How Boof Runner protects its API: host checks, API keys, scopes, trust levels, audit log and safe remote access.
Security model
- The server binds
127.0.0.1by default and rejects requests whose Host header is not a loopback name or listed insecurity.allowedHosts. This blocks DNS-rebinding attacks. - Anyone who can reach the API can create a task, and every task runs coding agents with permission prompts disabled inside your repository. Treat API access as equivalent to running code as your user.
- Ids are validated as kebab-case before they touch the filesystem. Request bodies are schema-validated and size-limited (1 MB JSON, 25 MB attachments).
- Agents work in a git worktree on a
boof-runner/<id>branch. Nothing is merged or pushed for you. - Do not expose the port to the public internet without keys and TLS.
Remote access
A request from a loopback peer with no Authorization header is the trusted local principal (while security.auth.mode is "local-trust", the default). Anything else needs an API key.
One command prepares the server for a remote AI harness:
boof-runner remote setup --hostname boof.example.com --allowed-root ~/codesIt sets security.auth.mode = "required", adds the host name to security.allowedHosts and the directories to security.allowedProjectRoots, creates a harness API client (the key is printed once; default scopes are the full harness set, trust review; change with --name, --scopes, --trust, --expires, or skip with --no-key), restricts the data root to your user (chmod 700) and prints the next steps: serve with TLS (boof-runner serve --host 0.0.0.0 --tls-cert cert.pem --tls-key key.pem) or keep --host 127.0.0.1 behind a tunnel or reverse proxy that forwards https://<hostname> to http://127.0.0.1:4319 with the original Host header (for example Caddy reverse_proxy 127.0.0.1:4319 or a Cloudflare named tunnel routed to that host name; a quick cloudflared tunnel --url gets a random *.trycloudflare.com name, which you add with remote setup --no-key --hostname <name>). Then give the agent BOOF_URL=https://<hostname>, the key and https://nodets.com/boof/harness.txt. Running it again does not duplicate entries; a taken client name makes it stop without changes (use auth rotate <id>).
boof-runner remote status shows the current state and whether a non-loopback bind is allowed; boof-runner remote disable sets the mode back to local-trust (hosts, roots and clients stay).
The same by hand:
# 1. require keys everywhere: set in settings.json
# "security": { "auth": { "mode": "required" }, "allowedHosts": ["boof.example.com"],
# "allowedProjectRoots": ["~/codes"] }
# 2. create a client (the key is printed once; scopes limit what it can do)
boof-runner auth add --name ci-bot --scopes tasks:read,tasks:write --expires 30d
# 3. bind beyond loopback; refused unless mode is "required" and a client exists
boof-runner --host 0.0.0.0 --tls-cert cert.pem --tls-key key.pem- Clients may be restricted to some projects (
--projects a,b): other projects answer403 forbidden_project. A client withprojects:writemay register a project only when its real path is undersecurity.allowedProjectRoots, is a git work tree, is not inside the data root and is not/or the home directory. - Call the API with
Authorization: Bearer boof_<client>_<secret>. The browser UI asks for the key once per tab. The desktop app manages its own key. boof-runner auth list | revoke <id> | rotate <id>manage clients. Revoked and expired keys stop working within 5 seconds.- Keys are root-equivalent. Scope them narrowly, set
--expires, and keep the defaultreviewtrust for anything you do not fully control: its tasks and plans land as drafts that a human approves in the UI. Use--trust trustedonly for clients you would give your own shell. - Without TLS, keys travel in clear text; the server warns when you bind a non-loopback address without
--tls-cert/--tls-key. Tailscale or a reverse proxy are fine alternatives. Requests carryingX-Forwarded-Fornever get local trust. - Every
/apiand/mcpcall is appended to<data>/audit/<day>.jsonl(principal, action, status, timing, IP; never bodies, prompts or keys). Requests are rate limited per principal (security.rateLimit, default 600/min).
The full REST reference, error codes and webhooks are in docs/api.md.