Skip to main content
By the end of this page the daemon runs as a user service that starts at boot, survives reboots, and you know exactly which files it reads and writes.

Install the binaries

Each release ships oh (the CLI) and ohd (the daemon) as self-contained executables — the bundled runtime, the daemon, the web app, and the compiled SQLite addon are all inside the binary. No Node.js required.
The script verifies SHA-256 checksums and installs to ~/.local/bin. Binaries currently ship for Apple silicon (mac-arm64).
The same binaries are downloadable from the releases page. On first use the binary unpacks the SQLite addon (and the web app) to <state dir>/sea/<build>/ — a native module can only load from disk — with checksum-verified, crash-safe unpacking; set OH_DAEMON_UNPACK_DIR to move that base.

The service lifecycle

A freshly started daemon has an empty user directory — the first browser to reach it creates the administrator, and ohd status prints where to point that browser. See First run. ohd install writes a user service unit — a launchd LaunchAgent on macOS, a systemd user unit on Linux — and persists the config flags it is given into daemon.json. Re-run it with new flags at any time to reconfigure: an omitted flag keeps its persisted value (--no-trusted-proxy / --no-allow-insecure-lan clear the booleans), and ohd restart applies the result. ohd start is a no-op while the service already runs. On Linux, install also enables the unit for boot and turns on user lingering (systemctl --user enable oh-daemon.service, loginctl enable-linger) so the daemon survives reboots and outlives the SSH session that installed it; if either command needs privileges, the exact manual command is printed instead. On macOS the LaunchAgent starts at login.

Foreground instead

ohd run runs the daemon in the foreground — it is exactly what the service unit execs. Ctrl-C / SIGTERM shuts it down cleanly. Handy under container supervisors and for trying things out before installing a service unit.

Where everything lives

Move the data dir with --data-dir (persisted like every install flag). Everything the daemon persists lives under it.

Configuration precedence

Precedence, highest first: argv → env → daemon.json → defaults. daemon.json is the durable configuration — ohd install writes the flags it is given into it, the service unit carries only --config, and every ohd command (status, show-token, the daemon boot itself) reads the same file.

Runtime settings are separate

Runtime settings live in storage.json, not daemon.json. The CLI exposes the MCP switches and the update switch, all off by default:
Settable keys: mcp.enabled, mcp.allowObserve, mcp.allowWrite, mcp.allowExecute, mcp.allowSecrets, updates.autoUpdate, and backend.allowRemotePeerExecute — whether devices that joined the server may run their requests on it (on by default; the same switch sits under Server Admin › Server). Bind and network options are not settings — they persist through the ohd install flags above. config set refuses while the daemon runs (storage.json is single-writer); a running daemon takes settings changes from a connected admin surface instead. Reads work anytime.

The web app

The daemon serves the Open Headers web app — the same Workbench UI the desktop app and extension run — as static files on its bind. Browsers only run it from a secure origin, so where you open it depends on the machine: The Workbench needs the browser’s cryptography APIs (crypto.subtle) to mint its identity, and browsers withhold them on any plain-HTTP origin that is not loopback. That rule is the browser’s and it binds the web app only: the browser extension and the desktop app connect over ws://<server>:8137 from anywhere on the network without TLS, which is how a headless server is normally used — no browser on the server required. Distributions built with the web bundle serve it out of the box; point --web-root at a different built bundle to serve that instead. An explicitly configured web root must contain an index.html, or the daemon refuses to boot; without any web root the daemon runs headless-only and / answers 400.

Next