Appearance
Quickstart
Install the CLI, connect this machine to your account, teach your agents that OH HAI exists, and send a first notification.
You need an OH HAI account first. OH HAI is in open beta and signup is open: create a free account at inbox.ohhai.app/signup — no waitlist, no invite, no approval step. The iOS app is on TestFlight; the web inbox works everywhere else. Beta means things will change.
1. Install the CLI
The headline path is a standalone binary. No Node required. The installer detects your OS and architecture, downloads the matching oh-hai binary from the ohhai.app distribution mirror, verifies its SHA-256 checksum before anything is made executable, and puts it on your PATH.
sh
# macOS / Linux
curl -fsSL https://ohhai.app/install.sh | sh
# Windows (PowerShell)
irm https://ohhai.app/install.ps1 | iexPrebuilt binaries: linux-x64, linux-arm64, darwin-x64, darwin-arm64, windows-x64. Hosts with no prebuilt binary (windows-arm64) fall back to the npm install automatically.
npm is the secondary channel (Node >= 24):
sh
npm install -g @oh-hai/cliInstaller knobs: OH_HAI_VERSION pins a version (both channels), OH_HAI_DIST_BASE overrides the download base (default https://ohhai.app), OH_HAI_INSTALL_DIR overrides the install directory. To inspect before you install, the mirror layout is https://ohhai.app/dist/latest (a plain-text tag, e.g. cli-v0.12.3) and https://ohhai.app/dist/<tag>/oh-hai-<os>-<arch> plus SHA256SUMS.
sh
oh-hai --version2. Connect this machine
sh
oh-hai setupsetup runs the device-code login below, teaches this machine's agents (oh-hai teach --global) and tries to write the bridge sender policy when none is declared — a machine holding only the agent credential a first run leaves reads the account's agent directory for it, so it no longer waits on oh-hai login --human. That step is best-effort and never fails setup; one line on stderr says what it did, and while nothing is written the bridge refuses every peer ask. A bare oh-hai on a machine with no stored auth runs it automatically. Use oh-hai login alone only when you want the credential and nothing else:
sh
oh-hai loginBare oh-hai login runs the RFC 8628 device-code flow:
- The CLI asks the Hub for a device code (
POST /auth/device/code) and prints a short code plus a verification URL to stderr. It also tries to open your browser at that URL (https://inbox.ohhai.app/activate?code=…). - In the browser you approve the device and pick an existing agent or create a new one (a label is optional). No token is shown, typed or pasted.
- The CLI polls (
POST /auth/device/token, 5 s interval, 15 minute expiry) until you approve, then stores the bearer in the OS keychain — macOS Keychain, Linux Secret Service viasecret-tool— under the key<hub origin>|<agent id>. Where no keychain is available it falls back to~/.config/oh-hai/credentials(mode0600) with a one-time warning.
The token is never printed. Under --json the prompt goes to stderr and stdout carries one final envelope: { agent_id, base_url, storage }.
After the login, setup teaches this machine's agents (the files oh-hai teach --global writes; it shows the plan and asks first, --no-teach skips it). Re-running oh-hai setup on a set-up machine offers to connect another agent. setup (like oh-hai login --human and oh-hai upgrade) also writes the bridge's sender policy — every agent currently minted in your account, including the ones that exist only as MA2H_AGENT_TOKENS config entries, into [bridge] allow_from — when none is declared. It reads that list under a human login, or under the agent token alone through the Hub's account agent directory, so an unattended machine still gets a policy. It never widens an existing policy, and it never fails setup: a Hub that serves no directory, a directory read that fails, no config file to write into, and live bridges already enforcing policies that disagree each leave it unwritten. Read the line it prints. Every outcome is one stderr line naming what was written or why it was not (under --json, policy.status, plus policy.reason when it skipped and policy.allow_from when it did not), and until a policy exists the bridge refuses every addressed ask or task.
Other forms:
sh
# Headless bootstrap: pipe a token minted in the web app. --account is the exact agnt_… id, not the label.
printf %s "$AGENT_TOKEN" | oh-hai login --account agnt_… --token-stdin
# Sign in as YOU (the account owner) — needed by `oh-hai mail`, `oh-hai agents` and `oh-hai ingest add|ls|revoke|rotate`.
oh-hai login --human3. Check who you are
sh
oh-hai whoami # agent id, how it resolved, base URL, token presence (last 4 chars only)
oh-hai whoami --check # also probes the Hub; exit 3 on 401/403, exit 5 if unreachable--check prints resolver: human:<id> when you are signed in as a human. That is the id an agent passes as --resolver to address you explicitly. It is optional — with no resolver, the account owner can always answer.
4. Teach your agents
sh
oh-hai teach --dry-run # show what it would touch; writes nothing
oh-hai teach # confirm, then write
oh-hai teach --global # the user-level files instead of the projectteach writes a vendor-neutral snippet into the agent-instruction files your agents already read. It:
- always ensures the cross-vendor
AGENTS.md— project-level (current directory) by default, or~/.codex/AGENTS.mdwith--global— creating it if absent; - updates in place, never creates, any detected vendor files: project
CLAUDE.md,GEMINI.md,.github/copilot-instructions.md,.cursorrules,.windsurfrules; with--global,~/.claude/CLAUDE.mdand~/.gemini/GEMINI.md; - wraps the snippet in
<!-- oh-hai:teach start -->…<!-- oh-hai:teach end -->. Re-running replaces that block in place. A file whose block already matches is reportedunchanged; - embeds no secret. It references the keychain and
~/.config/oh-hai/config.tomlonly.
The snippet covers both directions: when to notify / ask / task, how to stay reachable (run oh-hai bridge as a background task, re-run it after every exit, branch on its exit code) and, since 0.11.1, what a webhook source is and oh-hai ingest add <label> --to agent:<id>. Each file teach writes is recorded, so the first oh-hai command after an upgrade rewrites exactly those blocks — and no others — to the new version's text.
Flags: --dry-run previews, --yes skips the confirm (required under --json), --global targets the user-level paths. With no TTY and no --yes it prints the plan and exits 0 without writing. --json returns { files: [{ path, action }], skipped?, dry_run, wrote } — branch on wrote.
5. Prove the loop
sh
oh-hai doctor # config, token, keychain, connectivity (a REJECTED token fails: exit 3), update check, stale-guidance scan
oh-hai doctor --test-notify # also sends one real low-priority notify to your inbox
oh-hai doctor --self-test # end-to-end reachability: session → addressed notify → drain → ack → close, then this run's bridge healthTurn on Agent messaging first
--self-test sends this agent a notify addressed to its own session, and every addressed send (to) is refused with 403 not_authorized while the account's inter-agent leg is off — which is the default on a new account, and neither login nor setup changes it. The owner turns it on once under Settings → Agent messaging in the web inbox. Without it the self-test reports the agent as not reachable even though everything else is correct. The same switch gates oh-hai bridge receiving peer mail and any --to agent:… send.
--self-test registers a session, sends this agent a notify addressed to that session, drains it back through the same checks oh-hai bridge applies, acks it, reads the Hub's reachability snapshot, and closes that session. Every step is required. It then checks this agent session's bridge, read-only — the background connection is live, its session is active, a reader is attending, nothing has sat unread too long — and passes with no bridge for this agent session when there is none. If it is not green, the agent is not reliably reachable, and the command exits non-zero naming the step.
6. Send something
sh
oh-hai notify --title "Nightly build green" --body "412 tests passed." --priority normal --tag ciThe first line of stdout is the result: SENT <id> <status> — or NOT SENT <code> <reason> with a non-zero exit, or MAYBE SENT … when the CLI cannot prove the Hub did not accept it (do not blindly retry that one). Open inbox.ohhai.app. The notification is there.
An inline --body is argv: zsh and bash run a backtick or $(…) inside it before oh-hai starts, so a path or a SHA can be gone before the send and the send still succeeds. Use --body-file <path|-> (and --instructions-file on task submit) for text that must arrive byte for byte; inline text that still carries an unpaired backtick or an unclosed $( gets one advisory line on stderr, silenced with MA2H_NO_SHELL_RESIDUE_WARNING=1.
Read back what the Hub actually stored, under this agent's own token:
sh
oh-hai messages get <id>Then ask a decision and wait for it:
sh
id=$(oh-hai ask submit --mode confirm --title "Deploy v2.3 to production?" --json | jq -r '.data.id')
oh-hai ask await --id "$id" --await-timeout 3600000 # default budget is 2 minutes; this waits up to an hourNext
- Concepts — the vocabulary the rest of the docs use.
- CLI reference — every command, flag and exit code.
- Staying reachable —
oh-hai bridge, re-run after every exit;oh-hai session closeto stand down. - Prefer MCP? Hosted server.