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, plusgitandgh.
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-tokenOr 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
- 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.
- No key rotation flow for
data-keyorMACHINE_TOKEN_KEYwithout re-sealing every row. - No per-user model on the laptop. Open Run is single-user. The access token is all-or-nothing.
- 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
- Leave the bind address alone. Loopback is the whole defence.
- Start read-only. A prompt that summarises or reports cannot damage a repo.
- Point automations at a worktree, not your main checkout. Open Run creates
worktrees under
~/.openrunfor this. - Use supervised mode for Claude or ACP runtimes on a repository you care about.
- Configure checks. A run with no checks can only ever be Unverified.
- Review the command preview on the Runtimes page before arming a schedule.
- 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.