@oh-hai/cli
v0.4.9
Published
OH HAI — the `oh-hai` CLI: reach a human from your agent through an MA2H Hub (notify / ask / task), and reach another agent over the v0.5 inter-agent leg (sessions, addressed sends, bridge, fleet).
Readme
@oh-hai/cli — the oh-hai command-line tool
One universal, runtime-agnostic way for an autonomous agent to reach a human through an
MA2H Hub: oh-hai notify | ask | task. Shell-out is the lowest common denominator (Claude
skills are Claude-only, MCP is partial); a small, well-behaved binary works everywhere.
The design contract is docs/specs/cli.md (the command surface,
auth/token model, config resolution, exit codes, and the --json envelope). This package
implements that contract.
The one invariant: the agent bearer token never passes through the LLM.
oh-hai loginputs the token in the OS keychain; the agent invokesoh-haiby reference and never sees, prints, or reasons over the secret (spec §5).
Status
The command surface is implemented — auth, messaging, the v0.4 principal inbox, and the
v0.5 inter-agent leg (sessions, addressed sends, bridge, fleet). See
CHANGELOG.md for what landed when.
| Works today | |
|---|---|
| --help, --version (and --version --json); global-flag parsing + dispatch | ✅ |
| Exit-code table (§7), --json envelope (§8), config resolution (§6) | ✅ |
| setup · login --token-stdin / logout / whoami / use (OS keychain + 0600 file fallback) | ✅ |
| notify / ask submit\|await / task submit\|await (account-aware; --json; pull-await) | ✅ |
| inbox watch — the v0.4 human→agent principal mailbox | ✅ |
| session start\|close\|status · --to addressed sends · fleet ls · bridge — the v0.5 inter-agent leg | ✅ |
| messages list · agents list\|create\|revoke · doctor (--self-test) · teach · upgrade | ✅ |
| oh-hai login device-code acquisition (server device-authz + web approval) | ✅ |
Any command that is not yet wired exits 1 with a stable not_implemented error and a
pointer to its owning issue — it never silently no-ops or fakes success.
Auth (login / logout / whoami)
The agent bearer token is stored in the OS keychain (macOS Keychain, Linux Secret
Service) or, where no keychain is available, a 0600 file at ~/.config/oh-hai/credentials
(with a loud one-time warning). It is keyed per Hub (<origin>|<agent id>), never printed,
and never accepted as a flag value (spec §5).
# Store a token minted in the web app / via `oh-hai agents create` (never typed, never logged):
printf %s "$AGENT_TOKEN" | oh-hai login --account my-agent --token-stdin
oh-hai whoami # agent id + base URL + token presence (…last4) + source
oh-hai whoami --check # also probe the Hub for token validity
oh-hai logout # remove this identity's token (--all clears every identity)Bare oh-hai login (the browser device-code flow) is the primary flow: it requests a
device code, prints a short verification URL + code, and polls until you approve it in the web
inbox (/activate). oh-hai setup and first-run onboarding share the same flow;
--token-stdin remains the headless path.
Per-project identity (use)
Running multiple agents on one machine? Bind a directory to a specific agent so sessions in
that tree target it automatically — no --account on every command. The simple path stays
simple: one agent, zero config, no use needed.
oh-hai use my-project-agent # bind this directory (and its subtree) to my-project-agent
oh-hai whoami # → agent: my-project-agent · via: binding (<dir>)
oh-hai use --list # show all directory bindings
oh-hai use --unset # remove this directory's bindingBindings are user-side (~/.config/oh-hai/bindings.json), scoped per Hub, and never
settable from a repo — a cloned .oh-hai/config.toml can't re-route your identity
(spec §6). Precedence: --account/MA2H_AGENT_ID > directory
binding > user default_account. Offline — the agent need not be logged in yet.
Messaging (notify / ask / task)
The productized counterparts of the @oh-hai/scripts wrappers (spec §4.4):
the bearer is resolved account-aware (keychain / 0600 file / MA2H_AGENT_TOKEN env — not
raw env), output follows the --json envelope, and Hub statuses map to the §7 exit codes.
oh-hai notify --title "Build finished" --body "All green" --priority normal
oh-hai ask submit --mode select --option ship:Ship --option hold:Hold \
--resolver human:owner --title "Ship v1?"
oh-hai ask await --id msg_02A… # poll until the human answers; prints the outcome
oh-hai task submit --instructions "Rotate the signing key" --title "Rotate key" \
--resolver human:owner # terminal by default (--callback opts into a return leg)
oh-hai <ask|task> submit --dry-run … # compose + print the envelope, POST nothing
oh-hai ask submit --envelope @captured.json # idempotent replay of a captured envelope (§4.7)
--state(sealed resume) requiresMA2H_STATE_SEAL_KEY— a 32-byte base64url key in the environment. With it set,notify/ask/taskseal the state blob (AES-256-GCM via the vendored MA2H core) andask await/task awaitopen it on resume; passing--statewithout the key is a usage error pointing at this requirement.
Install
New to OH HAI? For a top-to-bottom walkthrough — install → onboard → your first
notify/ask/task— see the Connect your agent guide. This README is the implementer-facing package reference.
The headline path is the standalone binary — no Node required. The installer detects your
OS + arch, downloads the matching binary from the ohhai.app distribution mirror, verifies
its SHA-256 checksum, and puts oh-hai on your PATH:
# macOS / Linux
curl -fsSL https://ohhai.app/install.sh | sh
# Windows (PowerShell)
irm https://ohhai.app/install.ps1 | iexThe repo is private, so binaries are served from the public ohhai.app mirror (
dist-worker/+ an R2 bucket CI fills on each release — issue #357), which is live: the installer resolves the latest stable tag fromhttps://ohhai.app/dist/latestand downloads the matchingoh-hai-<os>-<arch>plusSHA256SUMSfromhttps://ohhai.app/dist/<tag>/, verifying the checksum before installing. The same scripts are also served from the Pages domain athttps://ohhai.autonomy.com.ai/install.
npm is the secondary channel for node devs (and the automatic fallback when a host has no prebuilt binary, e.g. windows-arm64):
npm install -g @oh-hai/cliOH_HAI_VERSION pins a version on either channel. (Homebrew was dropped in favor of the
standalone binaries — epic #277.)
Command surface (see spec §4)
oh-hai [global flags] <command> [subcommand] [flags]
Setup setup (bare `oh-hai` on a new machine onboards)
Auth login | logout | whoami | use
Messaging notify | ask | task (add --to to address another agent)
Inbox inbox watch (v0.4 principal mailbox)
Bridge bridge | bridge status (v0.5 session mailbox — supervised, loud failure)
Sessions session start | close | status
Fleet fleet ls (the agent sessions you can address with --to)
Messages messages list
Agents agents list | create | revoke
Health doctor
Teach teach
Update upgrade
Global flags: --base-url --account --json --quiet --verbose --no-color --timeoutThe v0.5 inter-agent leg (sessions, --to, bridge, fleet)
agent.id names a machine principal that many concurrent runs share, so on its own it cannot
say which run you are talking to. A session can: it is Hub-minted, lease-bound, and is
what --to agent:<id>#<sess_…> addresses.
oh-hai session start --label "build-worker" # prints the id + an `export MA2H_SESSION_ID=…` line
oh-hai fleet ls # the sessions you can address, with copyable --to values
oh-hai notify --to agent:agnt_… --title "Handing off" # acks `queued`, never `delivered`
oh-hai ask submit --to agent:agnt_…#sess_… --mode confirm --title "Proceed?"
oh-hai bridge --until-event --allow-from any-in-account # drain your session's mailbox
oh-hai session closebridge requires an explicit --allow-from policy before acting on an addressed
ask/task — there is no permissive default, by spec. Its fatal exits are distinct so a
supervisor can branch rather than blanket-restart: 3 auth (stop) · 11 session terminal
(restart) · 12 verification failure (stop) · 13 retries exhausted (restart) · 14 a
human closed your session with the §16.4 operator kill-switch (STOP — do not restart;
restarting is what silently undoes the human's decision). Put the stop classes in
RestartPreventExitStatus (2 3 12 14); see
docs/guides/bridge-supervision.md.
Usage examples
wire_version is the highest MA2H minor this CLI speaks — a bare 0.x with no v prefix
(0.5 = the inter-agent leg is supported). Sniff it to feature-detect the client. Individual
envelopes declare the lowest minor their features require (0.3 for a plain notify/ask/task;
0.5 when to / agent.session ride), so an envelope's ma2h_version tells you what that
message needs — both are derived from the same vendored constants and cannot drift.
$ oh-hai --version
0.4.0
$ oh-hai --version --json
{"ok":true,"command":"version","data":{"version":"0.4.0","wire_version":"0.5"},"error":null}
$ oh-hai notify --title "Build finished" --body "All green" --priority normal
$ oh-hai ask submit --mode select --option ship:Ship --option hold:Hold \
--resolver human:owner --title "Ship v1?" --json
$ oh-hai ask await --id msg_02A… --jsonMachine contract
- Exit codes (§7):
0success ·1generic ·2usage ·3auth ·4not-found (and Hub410 Gone— a destination session that died) ·5network ·6server ·7timeout ·8conflict ·9invalid-request ·10already-running (a second localinbox watch) · and thebridgefatal classes11session-terminal ·12verification-failure ·13retries-exhausted ·14operator kill-switch. (not_implementedmaps to exit1.) --jsonenvelope (§8): every command emits one uniform shape:
Absent optional fields are omitted from{ "ok": true, "command": "ask.submit", "data": { "id": "…" }, "error": null }data, not set tonull. On failureokisfalse,dataisnull, anderroris{ code, message }. The token is never in the envelope (spec §5.4).- Config precedence (§6):
flag > env > config file > default, resolved per value. The token is special-cased (keychain /MA2H_AGENT_TOKENenv — never a config value), and per-project config is credential-safe: it may set onlyoutput/timeout/color, neverbase_url/account.
Develop
Node ≥ 24, run from the repo root:
pnpm --filter @oh-hai/cli start --help # run the CLI from source via tsx (or: pnpm oh-hai --help)
pnpm --filter @oh-hai/cli typecheck # tsc --noEmit
pnpm --filter @oh-hai/cli test # node:test unit suite
pnpm --filter @oh-hai/cli build # publish-time compile → dist/ (gitignored)Dev + test run the .ts sources directly via tsx (repo convention — no build step). The
published artifact is compiled to dist/ because end users have node, not tsx.
Release
Tag-driven: push cli-vX.Y.Z (matching package.json) and the
cli-release workflow builds + publishes to npm.
See the runbook: docs/runbooks/cli-release.md.
