Security model

Open Run spawns coding-agent CLIs as child processes, in directories you choose, with your own credentials. The trust boundary is the machine.

To report a vulnerability, see SECURITY.md.


What Open Run is

A local scheduler and control panel for agent CLIs you already have installed and logged in. It holds no model API keys and talks to no model provider. It has:

  • a local HTTP server (the UI and its RPC surface),
  • a SQLite database in data/openrun.db,
  • managed clones and worktrees under ~/.openrun (OPENRUN_HOME),
  • child processes: claude, codex, grok, gemini, agy, plus git and gh.

Optional Sign in talks to the control plane at openrun.sh. Every local feature works without it.

Threat model

We defend against We do not defend against
A remote, unauthenticated request reaching the server Another process running as you on the same machine
A forged event on the relay starting a run A prompt you wrote that tells an agent to do damage
An agent writing outside its run's working directory through the file API A malicious agent CLI binary on your PATH
Stored secrets leaking into the client bundle, logs or notifications An agent running rm -rf in a directory you pointed it at
A bypassed supervised-mode approval letting a tool call run undecided Someone with disk access reading ~/.openrun on an unencrypted disk

The right-hand column is the product working as designed, or outside what a local tool can control.


Control 1 — the bind address

This is the primary control. Anyone who can reach the HTTP server can start a run, and a run is arbitrary command execution as your user. The server binds 127.0.0.1 by default. A configuration that would publish it refuses to start.

OPENRUN_HOST OPENRUN_ACCESS_TOKEN Result
unset (default) unset Binds 127.0.0.1. No token needed.
unset set Binds 127.0.0.1. Token required on every request.
0.0.0.0, a LAN IP, … unset Refuses to start. Exit code 1.
0.0.0.0, a LAN IP, … set Starts, warns, requires the token.
0.0.0.0 + OPENRUN_ALLOW_INSECURE_HOST=1 unset Starts wide open. Warns loudly on every boot.

Rules live in src/lib/serverAccess.ts. scripts/start.ts settles the bind address before the socket opens. src/start.ts applies the same rules as request middleware in front of every server function and API route, plus a CSRF check on server functions.

pnpm dev is loopback-only (Vite's default).

If you need remote access

Put Open Run behind an authenticating reverse proxy or a VPN. If you reach it by a tunnel hostname, add that name to OPENRUN_ALLOWED_HOSTS — on a loopback bind, any other Host header is refused (DNS-rebinding guard). Binding the HTTP port to a public interface is not the supported answer.

Control 2 — the access token

Required on every request whenever a token is configured, loopback or not.

pnpm token:print          # prints it; stores it 0600 in ~/.openrun/access-token

Or pin your own with OPENRUN_ACCESS_TOKEN. Present it as:

Channel Use
x-openrun-token header Preferred. Scripts, curl, reverse proxies.
openrun_token cookie The browser, after the first authenticated load.
?openrun_token=… query parameter Establishing that cookie, and SSE — EventSource cannot set headers.

Signing a browser in. Load http://127.0.0.1:3000/?openrun_token=<token> once — the URL pnpm token:print prints. That redirects to the same page without the parameter, setting an HttpOnly, SameSite=Lax cookie. Later server functions and SSE streams carry the cookie themselves.

Comparison is constant-time over the token's bytes. Length is observable; the length is not the secret.

Nothing bypasses the token

Provider webhooks land on the control plane and reach this machine over its own outbound relay, so there is no inbound endpoint a vendor addresses directly and no exemption from the token check.

Control 3 — the workspace path boundary

Every path crossing src/server/files.ts is resolved against the run's working directory and rejected if it escapes — ../ traversal, absolute paths, and symlinks pointing outside are refused before any read or write. Files above 2 MB are not editable through the browser.

This constrains the file browser/editor API, not the agent process. A CLI running in your repository can write wherever your user account can.

Control 4 — supervised mode and approvals

On Claude Code and ACP runtimes, supervised mode surfaces tool calls as Allow/Deny before they run (lib/approvals.ts, lib/supervisedPolicy.ts). An unanswered approval auto-denies after five minutes. Codex and Grok CLI transports cannot ask; the UI does not offer Supervised for them.

When a runtime is in supervised mode, Open Run removes --dangerously-skip-permissions from the args template. The Runtimes command preview shows the exact argv that will run.

Control 5 — verification before you trust a run

Project checks run in the worktree after every turn and produce a verdict: Verified, Checks failed, No changes, Unverified, Timed out, Crashed. A clean exit that touched no file is No changes.

Runs have a wall-clock budget. Failed checks can be handed back to the same session as repair turns.


Secrets at rest

Secret Where Protection
Paired device tokens data/openrun.db SHA-256 only — a copy of the database is not a working phone
APNs push tokens data/openrun.db AES-GCM sealed with ~/.openrun/data-key
MCP OAuth access / refresh / client secret data/openrun.db AES-GCM sealed with ~/.openrun/data-key
Notification webhook URLs data/openrun.db AES-GCM sealed with ~/.openrun/data-key
Cloud session + refresh tokens ~/.openrun/cloud-session File mode 0600
Access token ~/.openrun/access-token File mode 0600
Local wrapping key ~/.openrun/data-key File mode 0600. Not stored in SQLite.
Hosted Jira / GitHub / … tokens Control plane D1, never this machine AES-GCM under a Worker secret
Agent CLI credentials Not stored by Open Run Owned by each CLI's own login

A stolen openrun.db without ~/.openrun/data-key is ciphertext. File permissions still matter for the key file itself. Full-disk encryption is the operating system's job.

The database and its WAL sidecars are chmodded on every open, best-effort: Windows and some network filesystems ignore POSIX modes. If you have copied ~/.openrun or data/ between machines, check the permissions that survived.

CLI config files under your home directory still hold MCP Authorization headers in the clear — those CLIs read them at startup.

Known gaps

  1. Better Auth session tokens on the control plane are a lookup key and stay stored as issued. They hijack the Open Run dashboard, not Jira.
  2. No key rotation flow for data-key or MACHINE_TOKEN_KEY without re-sealing every row.
  3. No per-user model on the laptop. Open Run is single-user. The access token is all-or-nothing.
  4. No audit log. Runs are recorded, but there is no tamper-evident record of who caused what. (A commercial plane — see COMMERCIAL-LICENSE.md.)

Running agents safely

  1. Leave the bind address alone. Loopback is the whole defence.
  2. Start read-only. A prompt that summarises or reports cannot damage a repo.
  3. Point automations at a worktree, not your main checkout. Open Run creates worktrees under ~/.openrun for this.
  4. Use supervised mode for Claude or ACP runtimes on a repository you care about.
  5. Configure checks. A run with no checks can only ever be Unverified.
  6. Review the command preview on the Runtimes page before arming a schedule.
  7. Do not run Open Run as root, and do not point it at a directory holding credentials, ~/.ssh, or another project's secrets.

Reporting

Private disclosure via GitHub Security Advisories. Scope, targets and response times are in SECURITY.md.