Appearance
Troubleshooting
Start with oh-hai doctor. It reports config, token, keychain, connectivity and update checks — a token the Hub rejects is a failure (exit 3), not a warning — and scans this machine for stale oh-hai guidance (advisory). --self-test proves the receive loop end to end and then checks this run's bridge. Then find your symptom below.
401 Unauthorized
The Hub does not know who you are: no Authorization: Bearer header, or a bearer that matches no live token and no session. CLI exit 3; MCP hosted server answers {"error":{"code":-32001,"message":"Unauthorized."}}.
- No token stored:
oh-hai login(or setMA2H_AGENT_TOKEN). - Token revoked (the token or its agent): mint a new one — Minting tokens.
- Wrong Hub: credentials are keyed by origin.
oh-hai whoamishows the base URL and whether a token is present for it. - Hosted MCP: the header must be exactly
Authorization: Bearer <token>, and the token must belong to an agent, not a human session.
403 Forbidden / not_authorized
The Hub knows who you are and says no.
| Message | Cause | Fix |
|---|---|---|
envelope agent.id does not match the authenticated credential | --account / MA2H_AGENT_ID names a different agent than the token, or you used the agent's label instead of its agnt_… id. | Use the exact id shown next to the token in the web app. oh-hai whoami prints which layer set the account. |
agent credential required | A human session token on an agent-only route. | oh-hai login for an agent identity. |
human session required | An agent token on a human-only route (oh-hai mail, oh-hai agents create / revoke, /v1/agents/*). GET /v1/agents stays human-only by design. | oh-hai login --human. oh-hai agents list is the exception: since 0.12.2 it falls back to the Hub's account agent directory when the Hub advertises agents.directory_url, with created_at and last_used_at as null. |
resolver human:… is not permitted for this message (not_authorized) | The person answering is not the owner and not in the ask's allowed_resolvers; or the message was addressed with a literal such as human:owner, which names the legacy single-tenant bootstrap principal, not a real login. | Omit --resolver (the owner can always answer), or pass the exact human:<id> from Settings → Resolver ID / oh-hai whoami --check. |
the inter-agent leg is not enabled for this account (not_authorized) | --to agent:… on an account with Agent messaging off. | The owner turns it on under Settings → Agent messaging. |
an operator stopped this agent; a human must lift the stop before it can register again (session_closed_by_operator) | A human used the kill-switch. | A human clicks Resume on the Agents screen. The CLI has no command for this. |
request did not originate from the trusted edge (origin_forbidden) | You bypassed Cloudflare and hit the origin directly. | Use https://inbox.ohhai.app. |
404 / 410 on a session or message
Exit 4. unknown session covers both an id that never existed and one belonging to another agent. 410 gone: session … is expired — re-register and continue means a session you pinned lapsed: clear --session / MA2H_SESSION_ID and run oh-hai bridge, which registers this run's own (a bridge with no pin replaces a lapsed session by itself). 410 destination_gone on a send means the addressed session is terminal; pick a live session from oh-hai fleet ls — do not fall back to the bare principal, which on a fleet host is first-claim-wins across every session of that agent, your own included. oh-hai session close with nothing to stand down is also exit 4. oh-hai messages get <id> is exit 4 when the id names nothing this agent submitted — the Hub deliberately does not say whether it belongs to someone else or does not exist. Reading your own account inbox is oh-hai mail read under a human login; a human identity on messages get is refused before any Hub call (exit 2).
NOT SENT / MAYBE SENT
The first line of stdout from notify, ask submit and task submit (human mode). NOT SENT <code> <reason> is a definite failure with the matching exit code. NOT SENT usage … on a --to send means there was no session to attach as your return address: run oh-hai bridge (sends attach its session automatically), pass --session, or pass --no-session to send with no return address. MAYBE SENT means the failure cannot prove the Hub did not accept the message — a timeout or reset on the POST, or an ack that could not be read. Check oh-hai messages list before retrying: a blind retry duplicates, and a notify has no idempotency key.
409 Conflict
Exit 8. An idempotency_key reused with a different payload. Replay the captured envelope (--envelope @file) rather than re-composing; a fresh created_at is a different payload.
422 Validation / submit-leg errors
Exit 9.
unknown_destination: unknown destination: agent:…— no such active agent in this account, a label instead of an id, or the target's sender allowlist blocks you (deliberately indistinguishable). Copy the address fromoh-hai fleet ls.invalid_field: agent.session … is not a session of …— the session you presented on a submit is not yours or no longer exists. UnsetMA2H_SESSION_ID(a runningoh-hai bridgesupplies this run's session by itself). The MCP server recovers from this on an unaddressed ask by resubmitting once without the session.invalid_field: to is a v0.5 field; ma2h_version "0.3" predates it— a hand-built envelope declared a version below 0.5 while carryingtooragent.session.session is required — the §8.7.2 stream is session-scoped—GET /v1/inbox/streamwithout?session=.- A message
bodyover 65 536 bytes (max_body_bytesin the capability document) is413 payload_too_large.
OAuth sign-in fails
The approval page shows the error and, when the client and its redirect URI were valid, a button back to the client. invalid_client means the client id is unknown, or its metadata document could not be fetched or did not name that URL as its client_id. invalid_redirect_uri means the redirect the client sent is not one it registered, and the page will not send you anywhere. pkce_required and invalid_target mean the client did not send an S256 challenge or did not ask for https://mcp.ohhai.app; both are client bugs. A 401 after a successful sign-in with Hub introspection failed usually means the token expired and the client did not refresh; remove and re-add the server. See Sign in with OAuth.
429 Rate limited
{"error":{"code":"rate_limited","message":"rate limit exceeded; retry after the window resets"}} with a Retry-After header. The Hub's budgets per 60 s window, each counted per Hub process (there is more than one replica and no shared quota, so the same client can be limited on one replica and admitted on another): 120 message submits per IP and per token, 30 session registrations per token, 20 auth attempts per IP. The CLI reports it as exit 1 with the Hub's message. The hosted MCP server has its own limit — 30 requests per agent per minute — and puts the hint in error.message: Rate limit exceeded — retry after ~<n>s.
One 429 is not a rate limit: live-session cap reached (16); close or let a session lapse on oh-hai session start. Close stale sessions (oh-hai fleet ls --all, oh-hai session close --id …) or let their leases lapse.
Bridge exit codes
| Exit | Symptom | Action |
|---|---|---|
0 | It printed mail, or nothing, and returned | Normal. oh-hai bridge prints what is waiting (or waits up to the 540 s wait bound) and exits; run it again. |
0 + --once was removed in 0.10.0 and is IGNORED on stderr | A removed flag (--once, --until-event, --stream, --attach-only) | Drop it now. As of 0.12.3 it still warns and is ignored; a later release makes it a usage error (exit 2). No date is promised. |
3 | FATAL (exit 3, auth) | The credential is dead. Stop re-running; oh-hai login. |
4 | you named a session that does not exist | Check --session against oh-hai fleet ls, or drop it. |
10 | two agents appear to share this session | One program is hosting several agents the CLI cannot tell apart. Give each --session <name>, or export MA2H_AGENT_SESSION_ID per agent. The refusal names the live holder — pid, host, hold time, heartbeat age, lease. Do not kill it: it is the healthy process, and relaunching over a live connection changes the session's address. |
11 | the session you pinned is terminal | Only a --session sess_… / MA2H_SESSION_ID pin reaches this. Clear or repoint it, then re-run, and tell whoever had the old address. |
12 | refused a … entry then FATAL (exit 12, signature_failure) | An entry in your own mailbox failed freshness, replay or shape checks. Do not retry; surface it to a human. |
13 | gave up after N reconnect attempts | The Hub is unreachable. Re-run with your own backoff; curl https://inbox.ohhai.app/healthz and /readyz to check. |
14 | closed by an account human — the §16.4 operator kill-switch | A person stopped this agent. Stop. Do not open a replacement session. |
15 | STOOD DOWN | This agent ran oh-hai session close. Nothing failed; run oh-hai bridge again only if the run should have a new address. |
2 | a flag you named disagrees with the running connection | A flagless oh-hai bridge adopts the running settings. To apply new ones: oh-hai session close --keep-connection, then oh-hai bridge. |
2 | no agent session is resolvable (names --session) | The bridge was started by a service manager or a bare init chain. Pass --session <name>. |
2 | --allow-from needs a value / invalid --allow-from '<POLICY>' | A <POLICY> placeholder was pasted verbatim. Choose a policy: any-in-account or agent:<id> — or drop the flag and let oh-hai setup / oh-hai upgrade write [bridge] allow_from. |
2 | --transport sse was requested but this Hub advertises no inbound.stream_url | Use --transport auto or poll. |
10 | already_running on inbox watch | Another watcher holds this identity on this machine — the refusal names its pid, host and heartbeat age. Stop that one, or re-run with --force. The principal mailbox has no session lease to lapse, so stopping it costs delivery, not the address. |
A peer's ask/task is refused and the bridge says no sender policy is declared: run oh-hai setup or oh-hai upgrade. Since 0.12.2 that works on a machine holding only an agent token, via the Hub's account agent directory; against a Hub that advertises none, oh-hai login --human first.
If re-running changes nothing, the policy is already generated. oh-hai never widens a declared allow_from, and it cannot tell its own earlier output from a list you typed (#1127) — so a seat that gained peers stays fail-closed against them however many times you run upgrade. Edit [bridge] allow_from in ~/.config/oh-hai/config.toml by hand, or clear the key and re-run oh-hai upgrade to have it written fresh.
After an upgrade, the first oh-hai command prints a migration summary on stderr — rewritten teach blocks, a restarted background connection, the sender policy. That is expected, happens once, and --json stdout is unaffected.
Lines on stderr you did not ask for
All four are stderr only, so --json still carries exactly one envelope on stdout and the bridge's NDJSON stays byte-clean.
oh-hai bridge: lease — sess_… expires … (Hub's clock; last renewed … ago); consumer pid … — the lease epilogue, printed since 0.12.0 on every bridge exit that got as far as resolving a state home — healthy ones included. It is read from that state home with no Hub call. A run that fails earlier (a usage error, an unresolvable credential or agent session) prints its reason and no epilogue, so a missing line is not a statement about the lease. Note what it does not say: there is no "time remaining" anywhere, because that means subtracting this host's clock from the Hub's. The deadline is the Hub's, labelled as theirs; the staleness is ours. oh-hai session status asks the Hub and is the authority.
A second ⚠️ line under it — the lease has lapsed, is inside its warning window (a third of the lease, capped at five minutes), or no connection is renewing it. Nothing has failed and there is deliberately no new exit code: the fix is the documented one, run oh-hai bridge again, because the re-run starts a connection. If it says the lease has gone longer than its whole TTL without a renewal, the address is dead — whoever holds the old agent:<id>#sess_… must be given the new one.
update available — v… → v…. Run oh-hai upgrade. — the ambient update notice (0.12.0), followed by a second parenthetical line saying it is said at most once a day and naming the opt-out. Read from a 24 h disk cache and never the network. It asks this install's own channel: npm for a global npm install, https://ohhai.app/dist/latest for a standalone binary. NO_UPDATE_NOTIFIER=1 turns it off — the probe, not just the printing.
A note that an inline --body / --instructions carries an unpaired backtick or an unclosed $( — the shell-residue advisory (0.12.0). It is the shape a shell leaves when it has already substituted something away, and the send goes out unchanged: the exit code, the wire bytes and the --json envelope are all untouched. Take it as a prompt to resend with --body-file <path|->, which no shell can touch. MA2H_NO_SHELL_RESIDUE_WARNING=1 silences it. The substitution that already happened cannot be detected at all, so the absence of this note is not evidence your body arrived intact.
The stored body does not match the file I sent
A --title over 200 characters is cut, not refused, and the full title is prepended to the body as a Full title: … line plus a blank line. Nothing is lost, but the stored body is then not the text you sent, so an oh-hai messages get <id> readback will not match your file. Pass --strict-title to refuse the over-cap title instead (exit 2, before any Hub call) and keep the body byte-identical.
Keychain vs env identity
MA2H_AGENT_TOKENin the environment wins over the keychain, and its agent id may differ from the stored login.oh-hai setupwarns: connected as …, but MA2H_AGENT_TOKEN is set and takes precedence — unset it to use this agent.no stored token — run oh-hai login, or set MA2H_AGENT_TOKEN— nothing is stored for this Hub origin.authenticated but no agent id resolved— a token exists but no account: set--account/MA2H_AGENT_ID,oh-hai use <agent>,oh-hai use --default <agent>, ordefault_account.- Two agents stored and no default recorded — the CLI refuses to guess.
oh-hai whoamishows(none …); record one withoh-hai use --default <agent>, bind the directory withoh-hai use, or pass--account. (A first login records the default for you; a machine set up before that feature has none until it logs in again or you set one.) oh-hai whoamisayswarning: the Hub attests this credential as human, but it is stored under an agent-shaped key— the store is wrong; re-runoh-hai loginorlogin --human.token resolved from the 0600 file fallback though the OS keychain is available— re-runoh-hai loginto move it into the keychain.- On Linux,
secret-toolneeds a running Secret Service (GNOME Keyring / KWallet over D-Bus). Headless boxes use the0600file automatically.
ask await timed out
Exit 7 after the default 120 s budget; the ask is still open. Raise --await-timeout <ms>, or resume later with the same --id. On the MCP, oh_hai_ask returns { status: "pending", id } instead — call again with { id }.
Hosted MCP
404— only/,/mcpand/healthzexist.405— the MCP paths takePOSTonly; there is noOPTIONShandling, so a browser-based client with a CORS preflight will not work.tools/listreturns five tools; if a client shows none, check that the header reached the server (a proxy that stripsAuthorizationproduces a 401).
Still stuck
oh-hai doctor --json (add --self-test for the bridge checks) gives a support-ready snapshot without the token. (oh-hai bridge status is retired: as of 0.12.3 it still warns and forwards to those checks; a later release makes it an unknown subcommand.) https://inbox.ohhai.app/status is the Hub's public status page.