Appearance
Minting tokens
An agent token is the bearer every surface authenticates with. This page lists every way to get one and how the CLI decides which identity to use.
The rules
- A token is shown once, at mint time. The Hub stores only its hash.
- The token identifies one agent (
agnt_…). Sending as a differentagent.idis a 403. - Never commit a token. In client configs use your client's environment expansion (
${MA2H_AGENT_TOKEN},${env:MA2H_AGENT_TOKEN}), and keep the value in the environment or the OS keychain. - The CLI never prints a token and never accepts one as a flag value. An ingest key is a different credential with a different job, and it is printed — once — for the reason given under Ingest keys.
1. oh-hai login — the keychain path
sh
oh-hai loginThe device-code flow. You approve the device in the browser, pick or create an agent, and the Hub hands the CLI a token for it. The CLI stores it in the OS keychain (service oh-hai, account <hub origin>|<agent id>) — macOS Keychain, Linux Secret Service — or, with no keychain, in ~/.config/oh-hai/credentials (0600). You never see the token. This is the recommended path for any machine where an agent runs the CLI.
oh-hai setup and a bare first oh-hai do the same. oh-hai login --human stores the account owner's session token instead (for oh-hai mail, oh-hai agents and oh-hai ingest). A first human login also writes the bridge's sender policy when none exists — see Staying reachable.
2. The web app — Agents screen
In inbox.ohhai.app, open Agents:
- Create agent — one field, New agent label (1–128 characters). Creating an agent does not mint a token.
- Open the agent's Tokens panel and click Mint token (optional token label).
- The reveal block shows the token once — Copy this token now — you won't be able to see it again. — together with the agent id: Agent id:
agnt_…— use it as--accountwhen logging the agent in. Copy, then Dismiss.
The same screen lists each agent's id and tokens, lets you Revoke a single token or the whole agent, and shows operator stops in force.
Hand the token to a machine without a browser:
sh
printf %s "$AGENT_TOKEN" | oh-hai login --account agnt_… --token-stdin--account must be the exact agnt_… id shown with the token, not the label.
3. oh-hai agents create
sh
oh-hai login --human # once: the owner's session token
oh-hai agents create --label ci-bot # mints the agent + a token, stores the token in the keychainThe /v1/agents routes require a human session, so this needs login --human first. The minted token is written straight to the keychain under the new agent id and is not printed. oh-hai agents list and oh-hai agents revoke --target-agent agnt_… [--yes] complete the set; revoke also removes the local credential.
list is the one exception. On a machine with no human login it falls back to the Hub's account agent directory (GET /v1/account/agents), which an agent bearer may read for its own account — ids, labels, revocation state and source, no timestamps and no token counts. That is what lets oh-hai setup write the bridge's default sender policy unattended on an agent-only machine; where the Hub has no directory, nothing is written and the machine stays fail-closed. create and revoke have no such fallback and never will: GET /v1/agents is human-only, and the 403 it gives an agent bearer is what the web and iOS sign-in gates use to prove a pasted token is a human session. On a Hub that advertises no directory, list says exactly what it said before the directory existed. Details: Concepts → Listing the agents in an account.
4. MA2H_AGENT_TOKEN — the CI path
sh
export MA2H_AGENT_ID=agnt_…
export MA2H_AGENT_TOKEN=…
oh-hai notify --title "deploy finished"An explicit MA2H_AGENT_TOKEN wins over every stored credential and is never persisted. This is the path for CI and for the MCP servers (the hosted server takes the same value in an Authorization: Bearer header; the stdio server reads the two variables directly).
Ingest keys are not agent tokens
A webhook source — Sentry, GitHub, PagerDuty, anything that POSTs — holds a different credential, an ingest key (ohik_…), minted by oh-hai ingest add <label> and shown once like every other secret here. It is not an agent token and cannot be used as one:
- It authenticates on one endpoint only,
POST /v1/ingest/<source>(header form) or…/<source>/<key>(URL form); the<source>segment is advisory metadata — identity comes from the key. Presenting it anywhere else on the Hub is an ordinary 401 — not because a scope check says so, but because no other route's authentication ever looks the key up. - It has one destination and one identity: the agent it was bound to at mint time, and the source label it posts as. Neither is chosen by the sender.
- It gives no account access, no inbox reads, and no ability to speak as an agent. Someone holding it can put entries in one mailbox, at 120 a minute. That is all.
That narrowness is the point, and it is what makes the rest of the design acceptable. There is no signature verification on the ingest leg and there is not going to be — a per-vendor HMAC layer is exactly what the universal adapter refuses to have — so the bearer key is the permanent authentication story, and the credential is kept small enough that a leak is noise rather than a compromise. The mitigations that fit: the header form is the default, a key in the URL is rate-limited harder, and oh-hai ingest rotate <label> replaces the secret without changing the URL or the destination.
For the same reason, an ingest key is printed by the CLI, including under --json — the one credential here that is. An agent token never leaves the keychain because nothing outside your machine needs it; an ingest key exists to be pasted into somebody else's settings page, so withholding it would only move the paste into a screen-scrape.
Full detail: Webhook sources.
How the CLI picks an identity
Each value resolves as flag > env > config file > default.
Account (which agent): --account / --agent → MA2H_AGENT_ID → a directory binding from oh-hai use → default_account in ~/.config/oh-hai/config.toml → the default identity for this Hub. The default is recorded at your first login for that Hub and changed with oh-hai use --default <agent>, so connecting a second agent never unsettles the first. Failing that, the CLI discovers the sole stored identity of the kind the command needs (an agent for notify/ask/task/whoami; a human for mail/agents). Two stored identities of the same kind with no default are never guessed between: pass --account, or record a default. oh-hai whoami prints which layer won, and which identity is the default.
Token: MA2H_AGENT_TOKEN → the keychain entry for <hub origin>|<account> → the 0600 file. Never a config-file value.
Base URL: --base-url → MA2H_BASE_URL → base_url in the user config → https://inbox.ohhai.app. Credentials are keyed by Hub origin, so a token for a development Hub can never be sent to production by accident.
oh-hai use — which agent to act as
sh
oh-hai use agnt_… # this directory and its subtree act as that agent
oh-hai use --list # bindings, plus each Hub's default identity
oh-hai use --unset # drop this directory's binding
oh-hai use --default agnt_… # who this machine acts as for this Hub, by default
oh-hai use --default --unset # clear itTwo scopes of the same answer: a binding says who you are in this directory tree; the default identity says who you are on this machine for this Hub when nothing else said. A binding always wins over the default.
Both live in ~/.config/oh-hai/bindings.json — bindings keyed by absolute path and Hub origin, the default keyed by Hub origin. They are read only from the user's config directory, never from the repository, so a cloned project cannot redirect your identity.
~/.config/oh-hai/config.toml
toml
base_url = "https://inbox.ohhai.app"
default_account = "agnt_…"
output = "human" # or "json"
timeout_ms = 10000
color = trueA [bridge] section holds the sender policy and bridge bounds — see Staying reachable. --config <path> / MA2H_CONFIG point at a different file.
A project-level .oh-hai/config.toml (found by walking up from the current directory) may set only output, timeout_ms and color; base_url and account in it are ignored with a warning.
Checking what you have
sh
oh-hai whoami # id, how it resolved, base URL, token present (…last4) [keychain|file|env]
oh-hai whoami --check # 0 valid · 3 rejected · 5 unreachable
oh-hai doctor # adds the keychain and connectivity checks