Boot & configuration
| Symptom | Cause | Fix |
|---|---|---|
bind address 0.0.0.0 without TLS would expose auth tokens and pairing secrets as cleartext on the network — front the daemon with a TLS-terminating reverse proxy and set --trusted-proxy, or accept cleartext on a trusted network with --allow-insecure-lan | A 0.0.0.0 bind with no declared protection posture | Make the LAN vs TLS decision: add --trusted-proxy (proxy in front) or --allow-insecure-lan (trusted network) to ohd install, then ohd restart |
bind address must be '127.0.0.1' (loopback) or '0.0.0.0' (LAN), got '…' | A specific interface IP was passed | The daemon binds loopback or all interfaces; scope exposure with a firewall or the port mapping instead |
web root … does not contain an index.html | --web-root points at a directory that is not a built web bundle | Point it at a built bundle, or drop the flag to serve the bundled web app (headless-only if the distribution has none) |
port … is not bindable (privileged or out of range) | --bind-port below 1024 or above 65535 | Pick an unprivileged port; the default is 8137 |
Reachability
| Symptom | Cause | Fix |
|---|---|---|
| Clients on other machines cannot connect; the daemon runs fine locally | The daemon still binds 127.0.0.1 (the default) | ohd install --bind-address 0.0.0.0 with a protection posture, then ohd restart — see LAN vs TLS |
Bind is 0.0.0.0 but LAN clients still cannot connect | Host firewall | Admit port 8137: sudo ufw allow 8137 or sudo firewall-cmd --add-port=8137/tcp --permanent && sudo firewall-cmd --reload |
A client shows Not reachable / Could not open WebSocket: WebSocket error before handshake completed. and the daemon log has no matching line | The connection never reached the daemon — it refuses nothing silently, so an absent log line means the packets stopped earlier (firewall, wrong address, wrong port, no route) | From the client machine: curl http://<server>:8137/healthz — {"ok":true} proves the path, anything else is network. Then check the firewall row above |
A client shows Connection closed before the handshake — back-end likely not running on that port. | Something answered on the port but is not this daemon (or it is starting) | Confirm the port with ohd status on the server; check nothing else holds 8137 |
| Config was changed but behavior did not | A running daemon keeps its old bind until restarted | ohd restart — ohd install persists flags; only a restart applies them |
Browser requests via a domain name are refused; log shows rejected … request: host-forbidden (origin=… host=… peer=…) | The hostname is not one the daemon answers as | Declare it: ohd install --allowed-host oh.example.com, then ohd restart; IPs, localhost, and *.local always work |
Log shows rejected … request: origin-forbidden (…) | A browser origin the route does not accept (e.g. a cross-origin page calling /mcp) | Expected posture — /mcp refuses browser-originated requests outright; call it from an agent or CLI instead |
The served web app
| Symptom | Cause | Fix |
|---|---|---|
The page loads and shows This page needs a secure connection instead of the Workbench | The tab is on a plain-HTTP origin that is not loopback — browsers withhold crypto.subtle there, and the Workbench cannot mint its identity without it | Open it on the server itself at http://127.0.0.1:8137/, or put the daemon behind a TLS reverse proxy and open https://<your-host>/. This binds the web app only — the extension, desktop app, and CLI connect over ws://<server>:8137 from anywhere on the network |
/ answers 400 and no web app is served | The distribution shipped no web bundle, or --web-root points elsewhere | See The web app; a headless-only build is a valid deployment |
Claiming the server
| Symptom | Cause | Fix |
|---|---|---|
The setup card answers The server refused the setup. It may already be set up, or the setup code may be wrong or left over from an earlier start — the server prints a new one every time it restarts. | One refusal covers every state-dependent reason on purpose, so it cannot say which | Read the current code from ohd status (a restart replaces it), confirm the server is still unclaimed there, and retry. Case and dashes do not matter |
| The setup card asks for a code even though the browser IS on the server | The daemon sees the request arriving from a reverse proxy, not from loopback | Set --trusted-proxy so the peer is taken from X-Forwarded-For — see LAN vs TLS — or just enter the code from ohd status |
| A browser reaching a container gets asked for a code | Docker’s bridge address is not loopback to the daemon, whatever the host URL says | Expected — use the code from docker logs <container> |
The card says Sign in to this server but nobody ever created an account | The server is already claimed — the setup screen never comes back | ohd user list on the server (daemon stopped) shows who holds it; set a password for yourself with ohd user set-password |
The card says This server has no way for a browser to sign in: single sign-on is not configured, and no account on it has a password. | Claimed, no identity provider, and no account left holding a password | ohd user set-password <id-or-email> (daemon stopped), then sign in |
| Paired devices stopped working immediately after the claim | The claim revokes every unbound token — they acted as the server operator, which would bypass the new admin | Expected; pair them again from the admin console. Bind future bootstrap tokens with ohd show-token --user <id-or-email> |
ohd status prints no claim block | The server is already claimed (or was claimed mid-run — the block clears without a restart) | Nothing to do; sign in instead |
| Nobody can administer the server any more | The last admin was deactivated — deactivation is deliberately not guarded, only demotion is | ohd stop, ohd user set-admin <id-or-email>, ohd start |
Tokens & auth
| Symptom | Cause | Fix |
|---|---|---|
the daemon is running on port 8137 — stop it first (ohd stop). | show-token, config set, user …, backup, or restore while the daemon runs — storage.json is single-writer | ohd stop, run the command, ohd start; or do it from a connected admin surface while it runs |
HTTP 429 with Retry-After; body {"error":"too many failed attempts"}; log shows peer throttled: … (peer=…) | The per-peer failure budget tripped — repeated bad tokens or pairing-code guesses | Wait out the cool-down and fix the credential; behind a proxy, set --trusted-proxy so the budget hits the real client, not the proxy address |
--verbose reads the token-gated /metrics route — pass --token or set OH_DAEMON_TOKEN | ohd status --verbose without a token | Pass --token oh_… or export OH_DAEMON_TOKEN — /metrics requires a paired token even on loopback |
| A minted token was never saved | show-token and the console both show the secret once | Mint another from the admin console under Paired devices, or run ohd show-token again (daemon stopped) |
Settings
| Symptom | Cause | Fix |
|---|---|---|
bind and network options (--bind-address, --allow-insecure-lan, …) are not settings | ohd config set used for a network option | Network options persist through ohd install flags; config set is for settings keys (mcp.*, updates.autoUpdate) |
<key> is a boolean — expected true/false, got '…' | Non-boolean value for a settings key | All settable keys are booleans: true or false |
Service lifecycle
| Symptom | Cause | Fix |
|---|---|---|
service install is not supported on … yet | ohd install on a platform without service support (e.g. Windows) | Run ohd run in the foreground under your own supervisor, or use Docker |
systemctl --user … failed: … / launchctl failed: … | The user service manager refused | Read the detail in the message; on Linux over SSH, lingering may need sudo loginctl enable-linger <user> — ohd install prints the exact command when it cannot run it |
| Daemon dies when the SSH session ends (Linux) | User lingering is off, so the user manager stops at logout | sudo loginctl enable-linger <user>, then ohd start |
cannot write … — re-run the install with sufficient permissions | Unit or log directory not writable | Fix ownership of the printed path, or set --data-dir somewhere the user owns |
Upgrade
| Symptom | Cause | Fix |
|---|---|---|
checksum mismatch for … — expected …, got …; not installing | Corrupted or tampered download | Re-run ohd upgrade; persistent mismatches mean something on the path rewrites downloads — check proxies |
could not reach the update feed at … — check your network and try again | No route to the update feed | Restore network access and retry; the running daemon is unaffected |
no published ohd binary for this platform | The feed has no binary for this OS/arch | Use Docker or build from source; native binaries currently ship for mac-arm64 and linux-x64 |
this is a development build — ohd upgrade only works on a released binary | Upgrading a from-source build | Pull and rebuild from source instead |
Backup & restore
| Symptom | Cause | Fix |
|---|---|---|
destination … already exists and is not empty — back up into a fresh directory. | Backing up into a used directory | Point ohd backup at a fresh path |
… is not a backup — no manifest.json (torn backup or wrong directory). | Restore target is not a snapshot | Point ohd restore at the directory a backup created |
snapshot is corrupt — … does not match its manifest checksum. | A snapshot file changed since it was written | Use another snapshot — restore refuses rather than loading torn state |
snapshot is incomplete — … is listed in the manifest but missing. | A file was lost from the snapshot | Same — use a complete snapshot |
nothing to back up — … holds no storage.json or oracle.db (never booted?). | The data dir has never been booted | Start the daemon once first, or check --data-dir points where you think |
Users & licensing
| Symptom | Cause | Fix |
|---|---|---|
ohd user add refuses at the seat limit | The free tier includes 6 seats | Deactivate an unused user, or add seats — see Users, seats & SSO |
an individual seat needs the user email to match — add the user with --email. | --individual-license without a matching --email | Pass the email the seat key was issued for |
user '…' is deactivated — reactivation is not supported; add anew. | Operating on a deactivated user | Create a fresh user; deactivation is permanent for that record |
'…' is the only server admin — grant the role to someone else first, or the server would be left with no one who can administer it. | Revoking the last server-admin role | Grant it to another user first; the check counts holders, not who is asking |
the individual-seat key is not usable (invalid, expired, or not an individual seat). | Bad or wrong-type key | Check the key; team licenses install via ohd license install <file> instead |
Still stuck?
ohd status --verbose (with a token) shows the daemon’s own view of
its subsystems, and <data dir>/logs/daemon.log has one line per
event — auth rejections include the peer address and reason. If the
answer is not here, open an issue
with the log lines around the failure.