# CLI reference

> Every kabu command, option, limit and exit code, rendered from the CLI's own contract when the site is built.

Every command, option, limit and exit code of `kabu` 2.5.1, as the CLI's contract defines them. Scripts and agents may rely on all of it: options are only ever added, and changing or removing one is a breaking change. For a walkthrough, start with [the kabu CLI](/docs/cli).

## Commands

| Command | API calls | stdout |
|---|---|---|
| `kabu send [TITLE] MESSAGE` | `POST /v2/messages`; each `--file` first: `POST /v2/media/uploads`, then `PUT` the bytes | `{"ok", "id", "status", "attachments", "project", "project_source", "session"}` |
| `kabu ask QUESTION [MESSAGE]` | `POST /v2/messages`, then `GET /v2/messages/{id}?wait=25` until it settles | the answer, below |
| `kabu ask … --no-wait` | `POST /v2/messages` | `{"ok", "id", "status", "outcome": "pending", "project", "project_source", "session"}`; then `kabu status ID --wait 50` |
| `kabu send` or `ask` with `--dry-run` | `POST /v2/messages/preview`; nothing is uploaded or sent | `{"ok", "dry_run", "request", "files", "preview", "project_source"}` |
| `kabu status MSG_ID [--wait N]` | `GET /v2/messages/{id}` | the message; exit 0 whatever the answer, so read `status` and `resolution.decision` |
| `kabu whoami` | `GET /v2`, and `GET /v2/org` when the key may read it | `{"ok", "capabilities", "org", "org_name", "project", "project_source", "agent", "base_url", "config_path"}`; `project` is null when no rule names one |
| `kabu session start --title T` | `POST /v2/sessions` with a Live Activity; then `PATCH` for `--headline` or `--progress N/M` | the session; exit 3 `session_ended` if this `--external-id`'s session has ended |
| `kabu session update [SES_ID]` | `PATCH /v2/sessions/{id}` | the session |
| `kabu session end [SES_ID]` | `POST /v2/sessions/{id}/end` | the session |
| `kabu login --api-key KEY` | none | `{"ok", "config_path"}` |
| `kabu login --device` | `POST /v2/device_authorizations`, then `POST /v2/device_authorizations/poll` until approved | `{"ok", "config_path"}`, plus `org_name` and `project` when known |
| `kabu guide` (or `kabu help agents`) | none | a plain-text cheat sheet for coding agents |
| `kabu mcp` | `POST /mcp` for each JSON-RPC message read on stdin | each reply, one JSON-RPC message per line; see MCP bridge |

`kabu --help` lists the commands, exit codes and environment. `kabu COMMAND --help` (or `kabu help COMMAND`) lists every
option with its limit, and examples. `kabu --version` prints `kabu X.Y.Z`. `kabu` alone is a usage error (exit 2).
`kabu login` with neither flag asks for the key only when stdin is a terminal; `--api-key -` reads it from stdin. A
key is never shown in an error. `login --device` prints on stderr, first, `In the Kabu app, open Connect a computer and
enter the code: WXYZ-1234`, then a line with the link that approves the same code in a browser, and a line saying that
an admin or owner of the workspace approves it.

## Text

One argument is the message; two are `TITLE MESSAGE`. `--title` (or `--ask`, `--ask-text`) and `--message` set them by
flag. Without a title, the title is the message's first line, cut to 120 characters; without a message, the message is
the title. `-` as the message, `--code`, `--diff` or `--quote` reads stdin as UTF-8 (invalid bytes become U+FFFD), for
one of them at most. The title takes 120 characters and the message 8192, counted as the API counts (code points).
Longer text is refused, never cut. The request goes as UTF-8 JSON, and one over the API's 64 KiB (65,536 bytes) is
refused before any call.

## Options of send and ask

| Option (aliases) | Request body | Limit |
|---|---|---|
| `--project`, `-p` | `project` | 64; default: project resolution, below. kabu.py's `-p -1`, `0`, `1` or `2` (its priority) is refused: use `--priority` |
| `--agent`, `-a` | `agent` | 64; default: agent resolution, below |
| `--source`, `-s` | `source` | 120; default `KABU_SOURCE`, the saved `source`, then the hostname |
| `--session` | `session` | a `ses_…` id; default `KABU_SESSION` |
| `--priority` | `priority` | `low`, `normal`, `high`, `urgent`; kabu.py's `-1`, `0`, `1`, `2` are low, normal, high, high |
| `--expires-in`, `--ask-timeout` | `expires_in` | 60 to 2592000 seconds; an ask defaults to 300 |
| `--sensitive` | `sensitive: true` | |
| `--collapse-key` | `collapse_key` | 64 |
| `--accent` | `appearance.accent` | one of the contract's 13 accents |
| `--icon` | `appearance.icon` | 64; an SF Symbol name, which the app draws for this message (lowercase letters and digits in dot-separated words, such as `checkmark.seal.fill`), or a `med_…` id, which it does not |
| `--field KEY=VALUE` | `fields[]`: `{label, value}` | 12; key 40, value 200 |
| `--link LABEL=URL`, `--url URL` | `links[]`: `{label, url}` | 4; label 40 (`--url-title`, else `Open`, for a bare URL); an absolute URL up to 2048 |
| `--code`, `--language` | `blocks[]`: `{type: code, text, language}` | 8000; 30 |
| `--diff`, `--diff-path` | `blocks[]`: `{type: diff, text, path}` | 16000; 300 (a label: nothing is read) |
| `--quote` | `blocks[]`: `{type: quote, text}` | 2000 |
| `--action`, `--args JSON`, `--action-description` | `action`: `{name, args, description}` | 100; a JSON object up to 16 KiB; 2000 |
| `--as-audio`, `--voice` (`--tts-voice`), `--speed` (`--tts-speed`) | `voice`: `{speak: true, voice, speed}` | voice 60, speed 0.5 to 2.0; `--voice` and `--speed` imply `--as-audio` |
| `--file`, `--file-path`, `--attach`, `--attachment`, `--image` | `attachments[]`: `{media}`, after the upload, declared by extension as a type players open (`UPLOAD_TYPES`: `.m4a` is `audio/mp4`, never mimetypes' `audio/mp4a-latm`), else mimetypes', else `application/octet-stream` | 10 readable files (9 on a spoken notify), 1 byte to 2 GiB each |
| `--api-key`, `--base-url`, `--timeout` | none | see Configuration; `--timeout` 1 to 600 seconds, default 30 |
| `--dry-run` | none | |
| `--json` | none | accepted and ignored on every command but `login`: the output is always JSON (kabu.py's flag) |

Asks only (anywhere else they are refused):

| Option (aliases) | Request body | Limit |
|---|---|---|
| `--ask QUESTION`, `--ask-text QUESTION` | `title`; `--ask-text` makes `ask.type` `text` | 120 |
| `--type` | `ask.type` | `confirm`, `choice`, `multi`, `text`; default `choice` with `-c`, else `confirm` (a call with no command: below) |
| `--choice`, `-c`, `--option` | `ask.options[]`: `{id, label}` | 2 to 12, label 40; a confirm takes none, or two: the approve label, then the reject label. `ID:LABEL` sets the id when ID is a letter then up to 63 letters, digits, `_`, `.` or `-`, and no space follows the colon (`prod:Production`); otherwise the whole text is the label (`10:00`, `Retry: once`) and the id is the label in lower case, with `_` for every character but an ASCII letter or digit and none at either end (`Roll back` gives `roll_back`, `Retry: once` `retry__once`, `10:00` `10_00`) |
| `--allow-reply`, `--allow-free-form` | `ask.allow_reply: true` | |
| `--irreversible` | `ask.irreversible: true` | |
| `--no-wait` | none | |

Every option is validated before any HTTP call: limits, enums, ids, mutually exclusive and dependent flags, files
that exist. The request body is the contract's `MessageCreate`; the CLI sends `body`, never its alias `message`.

## Exit codes

| Code | Meaning |
|---|---|
| `0` | Done: sent; an ask approved, or a choice, multi or text ask answered; any other command succeeded |
| `2` | Usage or validation error; nothing was sent (zero HTTP calls). Also `local_error`: a local failure, such as a config file kabu cannot write |
| `3` | Network or API error, after retries; an ask kabu stopped waiting for, which is still open; an answer kabu does not know, never taken as approval |
| `10` | Ask rejected (from the app, Lock Screen, Live Activity, web or API), or a confirm answered with a typed reply instead of approve; `login --device`: the code was denied |
| `20` | Ask expired before an answer; `login --device`: the code expired |
| `21` | Ask cancelled before an answer, or deleted while kabu waited for it (`ask_gone`) |
| `130` | Interrupted |

## Output

stdout carries one JSON object and nothing else.

- `send`: `{"ok": true, "id": "msg_…", "status": "sent", "attachments": N, "project": "…", "project_source": {…},
  "session": null}`.
- `ask`, keys sorted: `{"choices", "decision", "id", "option", "outcome", "status", "text", "values", "via"}`, whatever
  the exit code. `outcome` is `answered`, `expired` or `cancelled`; `decision` is the API's, as given (the legacy `deny`
  reads `reject`), or null; `option` is the chosen option's id and `text` a typed answer. Expired and cancelled asks
  print `{"decision", "id", "outcome", "status"}`. The exit code says what to do, and only an explicit approve or the
  answer to a choice, multi or text ask is `0`:

  | The answer | `decision` | Exit |
  |---|---|---|
  | Approved | `approve` | `0` |
  | A choice, multi or text ask answered: `option`, `choices`, `text` or `values` | `respond` | `0`, whatever was chosen: branch on `option` |
  | Rejected | `reject` (or `deny`) | `10` |
  | A confirm answered with a typed reply (`--allow-reply`): read `text` | `respond` | `10` |
  | Expired; cancelled | as given, or null | `20`; `21` |
  | Anything else (`edit`, `ignore`, a decision kabu does not know, or none) | as given, or null | `3` |

- Every send and ask, `--no-wait` and the answer included, also prints `project`, the one sent (null when no rule named
  one), `project_source` (Project resolution) and `session`, the message's session as the API has it (null for none).
- `status`, `session`: the API's object as returned.
- An ask kabu stops waiting for is still open: the error adds its `id`, and the hint is `kabu status ID --wait 50`,
  never to ask again.
- An ask the API no longer has while kabu waits for it (a 404: it was deleted, as when its project is) can never be
  answered: kabu stops at once, with no retry, and exits 21 with `{"error": {"code": "ask_gone", "message", "hint",
  "status": 404, "id"}}`, whose hint is to ask again only if the answer is still needed.
- When the API answers `session: null` for the session kabu named, that session no longer exists (it was deleted with
  its project) and the message went without one: the output adds `session_gone`, its id, and kabu never names it
  again (Session, under Configuration).
- `kabu status` on a message the API does not have (404) keeps the API's error, exit 3, with a hint that it was
  deleted (as when its project is) or never existed, so it will never be answered. `kabu session update` or `end`
  with a session the API reported gone exits 3 with `session_gone` and makes no call; a 404 from either gets the same
  hint as `status`.

An error is one JSON line on stderr: `{"error": {"code": "…", "message": "…", "hint": "…"}}`. The message names the
flag and the allowed values or limit; the hint is the next step, usually a corrected command. API errors add `status`
(HTTP), and `param` and `request_id` when the API gives them; their `code` is the API's problem code, or
`network_error`. Nothing is ever coloured.

## MCP bridge

`kabu mcp` serves the API's MCP tools (`/mcp`, streamable HTTP) to a harness that starts it as a stdio server, such as
`claude mcp add kabu -- kabu mcp` or the Kabu plugin's `.mcp.json`. It finds the key and base URL as every command does
(Configuration, below), so the harness's config holds no key.

- Each line on stdin is one JSON-RPC message, POSTed to `{base URL}/mcp` with the key as a bearer token, unchanged
  but for one addition: a `tools/call` with no `project` argument, of a tool whose input schema in the server's
  `tools/list` has a `project` property, gets the project kabu resolves for that call (Project resolution) when rule
  1 to 5 names one, never a `<folder>@<host>` name, and only when the key is bound to no project: `kabu mcp` asks
  `GET /v2` once, when it starts, and adds nothing if the key has a `project` or the answer does not come. Requests run side by side, so a waiting ask never holds up another call; notifications and
  replies keep their order.
- **Presence (ADR 021, 022).** Nothing before its first `tools/call`. Then, when a harness started it (its parent is
  one, as for an MCP server; never a `kabu mcp` that a script in an agent session runs) and presence is on, `kabu mcp`
  gets or creates that harness's automatic session once, on the hooks' HMAC ladder
  (Presence: the same `external_id`, so one `ses_` per run, shared with the hooks and `kabu send`), adds it as `session`
  to the calls of the tools that take a project, unless a call names one, writes an `online` record from source `mcp`
  and starts `kabu presence run` when none runs. The hooks' `working` and `idle` beat that `online`. At EOF (or
  SIGTERM) it writes an `offline` record, which the reporter sends within 5 s; it never ends the session.
- Each reply is printed on stdout as one line: the JSON body, or each event of a `text/event-stream` body as it
  arrives, so the harness sees a long wait's progress notifications before its answer. An empty reply, such as a
  notification's `202`, prints nothing.
- The `Mcp-Session-Id` the server issues is sent on every later request, and replaced when the server sends a new one.
- Nothing is retried, so a tool call is never sent twice. A request with no answer from the API (a network error, an
  HTTP error status, a redirect, a body that is not JSON, or an empty body or event stream with no reply for its id)
  gets a JSON-RPC error with its id:
  `{"code": -32000, "message": "…", "data": {"code", "status"}}`, where `message` ends with the hint. The same error
  is logged on stderr as one JSON line. An id is answered once: when the API's answer came before the failure, such as
  a stream that breaks after its reply, the failure is only logged.
- stdout carries only JSON-RPC; anything else goes to stderr. Exit `0` once stdin closes and the requests in flight are
  answered, `2` with no key (`missing_api_key`) or a bad option, before reading stdin, and `130` when interrupted.

## Reliability

- One `Idempotency-Key` (a uuid4) per invocation on `POST /v2/messages`, the same on every retry, so a message is
  never sent twice. Uploads and session calls use keys derived from it.
- Up to 3 retries, on network errors, 5xx, 429 with `Retry-After` and 409 `idempotency_key_in_use`. Each waits
  `Retry-After` (up to 30 s; a longer one fails at once) or 0.5, 1 and 2 s. Any other 4xx fails at once.
- Each response may take `--timeout` seconds (default 30), plus the `wait` the API was asked to hold for.
- While an ask is pending, network errors, 5xx and 429 only delay the wait: kabu polls again every 5 s, up to
  `expires_in` + 120 s. Any other refusal, or that deadline, ends the wait with exit 3 and the ask's `id`, but for a
  404, which ends it with `ask_gone`, exit 21 (Output).
- A redirect is never followed, so the key never goes to another URL: a 3xx is an API error, exit 3 with its `status`
  (in `kabu mcp`, a JSON-RPC error). Set `KABU_BASE_URL` to the final URL instead.

## Configuration

| Setting | Order |
|---|---|
| API key | `--api-key`, `KABU_API_KEY` (or `AGENT_NOTIFIER_API_KEY`), the saved `api_key`; spaces and line breaks around it are dropped, and one with a space, line break or other character outside printable ASCII inside it is refused (exit 2, `invalid_api_key`), without showing it |
| Base URL | `--base-url`, `KABU_BASE_URL` (or `KABU_API_BASE_URL`), the saved `base_url`, `https://usekabu.com`; an `http://` or `https://` URL, else exit 2; a trailing `/v2` is dropped |
| Session | `--session`, `KABU_SESSION`; in an agent session that reports presence, its session (Presence); else `KABU_AUTO_SESSION=1` gets or creates one automatic session per host, project and agent (`automatic: true`, never `live`: it claims no run, so it stays idle with no Live Activity of its own, ADR 022; each ask in it has its own; `kabu session start` is the explicit, live run), in one try of up to 3 s. An ended or expired session is not used: the message goes without one, as does any message whose session call fails. A session the API reported gone (Output) is never sent again: kabu gets or creates, in its place, the auto session of this agent session (per host, project, agent and harness process), whatever `KABU_AUTO_SESSION`, and the output names both, in `session` and `session_gone`. An auto session names the message's project, or none when the message names none |
| Config file | `KABU_CONFIG_PATH`, else `$XDG_CONFIG_HOME/kabu/config.json`, else `~/.config/kabu/config.json`; written by `kabu login` with mode 600 |
| Presence | On unless `KABU_PRESENCE=0` (that agent's environment) or `kabu presence off` (every machine that shares this home); its records in a machine-local folder (`KABU_RUNTIME_DIR`), its identity in `KABU_HOME`, else `~/.kabu` (Presence) |
| Cache | `$XDG_CACHE_HOME/kabu/`, else `~/.cache/kabu/`: `sessions/` keeps each agent session's project (Project resolution), and `gone-<hash of the base URL>` the last 100 session ids that API reported gone. It holds no key; deleting it only makes kabu resolve again |

## Presence

Whether an agent is live (ADR 021, `docs/adr/021-agent-presence.md`): working, idle, waiting at the computer or gone.
`kabu --help` names these commands in a paragraph of their own, after Commands.

| Command | API calls | stdout |
|---|---|---|
| `kabu presence status` | none | `{"ok", "enabled", "off", "opted_out", "running", "pid", "instance", "generation", "dir", "lanes", "last_beat", "ack"}` |
| `kabu presence on`, `kabu presence off` | none | the status |
| `kabu presence run` | `POST /v2/sessions` once per agent session, `PUT /v2/presence` | nothing |
| `kabu presence install`, `kabu presence uninstall` | none; `--harness claude` or `copilot`, `--settings PATH` | `{"ok", "harness", "settings", "hook", "installed", "events", "changed", "backup"}` |


- **What leaves the machine:** state enums (runtime, attention, source), ids made by HMAC with the installation secret
  (the session's `external_id`, the `ln_` lane), the harness's name as the session's agent (`claude-code`) and the
  project the session runs in, as kabu send sends it. Never a prompt, path, tool name, host name, the client's title or
  its raw session id; the session's title is the constant `<harness> session`.
- **The hook.** `PRESENCE_HOOK`, written out as the plugin's `hooks/presence.sh` and by `kabu presence install` as
  `~/.kabu/presence-hook.sh` (`~/.kabu` is `KABU_HOME` when set), mode 0600. A harness runs it as
  `kabu_event=EVENT; (. FILE) || true`: a subshell of the hook's own shell sources it, so it starts no program (but
  `kabu presence run`, below), never reads its input (a harness may hold it open), has no unbounded loop, never calls
  the network and always exits 0. Events, in Claude Code's names or Copilot CLI's: `SessionStart` (`sessionStart`)
  idle, `UserPromptSubmit` (`userPromptSubmitted`), `PreToolUse` (`preToolUse`) and `PostToolUse` (`postToolUse`)
  working, `Notification` (`notification`) attention `terminal` until the next event (Claude Code's matcher passes only
  the types that need the user: `permission_prompt`, `elicitation_dialog`, `elicitation_url_dialog`,
  `worker_permission_prompt`, `agent_needs_input`), `Stop` (`agentStop`) idle, `SessionEnd` (`sessionEnd`) offline.
  `kabu mcp` writes records too, with `source: mcp` and `runtime` `online`, then `offline` (MCP bridge); a lane's state
  is its hooks' latest record when it has one, else kabu mcp's.
- **Records.** Each event is a new file, `l<harness pid>-<seq>.json` (0600), created no-clobber at the next free seq, in
  this user's machine-local folder `kabu-presence-$USER` (0700) of `KABU_RUNTIME_DIR`, else `XDG_RUNTIME_DIR`, else
  `TMPDIR`, else `/tmp` (on a Mac also the per-user temp folder): `{v, event, runtime, attention, seq, pid, sid, pane}`,
  where `pid` is the hook's parent (the harness), `sid` Claude Code's `CLAUDE_CODE_SESSION_ID` when that Claude is the
  parent, and `pane` `HERDR_PANE_ID`. A folder that is a link or not this user's is never used; no record is written
  through a link or over another file, so concurrent hooks lose no event and the state is the highest seq's. The hook
  starts `kabu presence run` at `SessionStart`, and at a turn's other events when none runs (`run.pid` there).
- **`kabu presence run`**, one per machine: an exclusive lock on `run.lock` in that folder, which is machine-local, so
  machines that share a home each run theirs. Every 2 s it folds each harness's records (keeping only the latest) and
  checks its process: one that exited reads offline at once, and one that is a zombie or whose id another process took
  within 10 s. A record older than its harness's process (another process had that id) is never adopted. It sends `PUT
  /v2/presence` with every live agent session (at most 20; the API takes one left out offline), once a minute or
  `next_beat_in`, and at once on a change, but working and idle wait 5 s to settle, never more than 10 a minute. A turn
  with no event for 15 minutes and no tool running reads idle. Each agent session's Kabu session is got or created once,
  automatic (`automatic: true`: idle, no Live Activity of its own, ADR 022): `external_id` `kp_` and HMAC(installation
  secret, harness ‖ session id, else its process id and start time), agent the harness, title `<harness> session`,
  project the one the harness's folder names (never a temp folder's or `<folder>@<host>`); it is kept beside the
  records, per API and key. The reporter is `instance` (`pin_…`)
  and `generation`, raised at every start, with the secret in `~/.kabu/presence/reporter-<machine>.json` (0600; one per
  machine: `KABU_MACHINE_ID`, else Linux's machine-id, the Mac's hardware UUID or the host name, hashed). A `stale` batch
  resumes above the API's generation; a session the API reports ended or gone is replaced by a new one. It exits after
  10 minutes with no agent session, once presence is off (its sessions sent offline first), or when its folder or
  `KABU_HOME` is gone. Every file it writes is new (0600) and renamed into place; it opens none through a link.
- **kabu send and ask join it.** In an agent session whose records exist and are newer than its harness's process (the
  harness is kabu's nearest harness ancestor), with no `--session` or `KABU_SESSION`, a message goes in that agent
  session's Kabu session, found or created as above, in one try of up to 3 s; `KABU_AUTO_SESSION` applies only without
  one. If the API answers `session_ended`, the message is sent again without it.
- **`kabu presence install`** writes the hook and adds it for the seven events to Claude Code's settings
  (`CLAUDE_CONFIG_DIR`, else `~/.claude`, `settings.json`: a group per event, `matcher` the types above for
  `Notification`) or Copilot CLI's (`COPILOT_HOME`, else `~/.copilot`: its native keys `sessionStart`,
  `userPromptSubmitted`, `preToolUse`, `postToolUse`, `notification`, `agentStop`, `sessionEnd`, each a flat
  `{type, bash, timeoutSec}`), or `--settings`. Entries that run `presence-hook.sh` are replaced, others kept; running it
  again changes nothing. Each change first saves a backup under a name no other has
  (`settings.json.bak-kabu-presence-<time>-<ns>-<id>`, 0600), and the file is written at once, never looser than 0600; a
  file that is not JSON is left alone (exit 2, `invalid_settings`). `uninstall` puts back the exact bytes from before the
  install when the file is as kabu left it (or removes the file kabu made), else takes out only kabu's entries; the hook
  file goes once no settings use it (`~/.kabu/presence/installs.json` keeps what was installed where). The Kabu plugin
  needs neither.

## Agent resolution

`--agent`, then `KABU_AGENT` (or `AGENT_NOTIFIER_AGENT`), then the `agent` saved by `kabu login --agent`, then the
harness that runs `kabu`:

1. The nearest ancestor process named `claude`, `copilot`, `codex`, `cursor-agent`, `gemini`, `opencode`, `aider`,
   `goose`, `amp` or `pi`. Process names only: never their arguments, which can hold secrets. kabu reads its
   ancestors from `/proc` on Linux and with one `ps` call elsewhere; a `ps` that cannot show start times still gives
   their names.
2. `AI_AGENT`, as a slug.
3. Markers harnesses set for the commands they run: `CLAUDECODE` or `CLAUDE_CODE_ENTRYPOINT` (`claude-code`),
   `CURSOR_AGENT` or `CURSOR_TRACE_ID` (`cursor`), `GEMINI_CLI` (`gemini-cli`), `CODEX_THREAD_ID` or `CODEX_SANDBOX`
   (`codex`), `PI_CODING_AGENT` (`pi`).
4. Otherwise `kabu-cli`.

## Project resolution

Without `--project`, `kabu` sends the project of the first rule below that names one. Every send, ask, dry run and
`kabu whoami` says which in `project_source`: `{"rule"}`, plus, where they apply, the `harness` process, the `dir` the
rule read, `via` (`dotenv` or `git`), the environment `variable` (only with `session_root`), and `cached: true` when
the project was read back from the agent session's cache. With `--project` it is `{"rule": "flag"}`.

| # | `rule` | The project |
|---|---|---|
| 1 | `env` | `KABU_PROJECT`, then the legacy `AGENT_NOTIFIER_PROJECT`, from the process environment |
| 2 | `session_root` | The one the agent session's root names |
| 3 | `cwd`, with `cached: true` | When the root names none: the one this session's first call got from rule 4 |
| 4 | `cwd` | The one the current folder names |
| 5 | `login` | The `project` saved by `kabu login --project` |
| 6 | `folder` | `<folder>@<short-hostname>`, such as `scratch@yshah-mn3`: the session root's folder, else the current one |
| 7 | `default` | None: the request names no project, so the API uses the key's project, else `default` |

- **Agent session.** The nearest ancestor process named as a harness (Agent resolution, step 1), such as `claude` or
  `copilot`. Its root is `CLAUDE_PROJECT_DIR` when set (Claude Code sets it for hooks and MCP servers, Copilot CLI for
  plugin hooks), else the harness process's working folder: the one it started in, or the one the user moved the
  session to (Copilot CLI's `/cd`). So `cd` in a command, which moves only that command's shell, never changes the
  project. With no harness among kabu's ancestors, rule 3 does not apply, and rule 2 only with `CLAUDE_PROJECT_DIR`.
- **What a folder names.** `KABU_PROJECT` (else `AGENT_NOTIFIER_PROJECT`) in `.env.local`, checked first, or `.env`,
  from the folder up to its git repository's top, then in the main worktree's root, as `.env` is usually untracked and
  so absent from linked worktrees; files outside the repository are never read (`via: dotenv`). Otherwise the git
  repository's name: the `origin` remote's, else the main worktree's folder name, the same from any nested folder or
  linked worktree (`via: git`). Otherwise nothing. A folder that no longer exists names nothing.
- **Temp folders.** A folder in `/tmp`, `/var/tmp`, `/var/folders` or `$TMPDIR`, symlinks resolved (so
  `/private/tmp` too), names nothing, but for a worktree of a repository whose main worktree is outside them, which
  names that repository's project. `cd /tmp/x && kabu send …` keeps the session's project, else the saved one, else
  none.
- **No `<folder>` of rule 6** is a temp folder, `/`, your home folder or a folder that no longer exists.
- **Kept per agent session.** Rules 2 and 3 keep what they named in the cache's `sessions/` (Configuration), one file
  per harness process, named by its process id and start time, with the session root it was named under. A later
  process that reuses the id never finds another session's project, so a file is only read while its process runs,
  and only while the session's root is the folder it was kept under; when the root moves, the session resolves again.
  A project is kept as a name, never a project id: a project deleted in Kabu is created again by the next message that
  names it. Files unused for 7 days are removed. Where a harness's start time cannot be read, nothing is kept.
- `kabu mcp` resolves the project for each tool call, as kabu send would (MCP bridge).

## Calls with no command (kabu.py compatibility)

A call whose first word is not a command is `send`, or `ask` when `--ask` or `--ask-text` is given, so `~/bin/kabu.py`
can become a one-line shim:

| kabu.py call | Same as |
|---|---|
| `kabu.py --project P --agent A --title T "message" --file-path X` | `kabu send T "message" --project P --agent A --file X` |
| `kabu.py --ask "Q?" -c Yes -c No` | `kabu ask "Q?" --type confirm -c Yes -c No`: Yes approves (exit 0), No rejects (exit 10) |
| `kabu.py --ask "Which env?" -c staging -c prod` | `kabu ask "Which env?" -c staging -c prod`: a choice, exit 0, `option` names it |
| `kabu.py --ask-text "Name?"` | `kabu ask "Name?" --type text` |
| `kabu.py "Build finished"` | `kabu send "Build finished"` |

Only a yes or no pair makes a confirm: `yes` then `no`, `y` then `n`, or `approve` then `reject`, in any case, with no
`--type` and no `--allow-free-form`. Its first choice exits 0 and its second 10, as kabu.py's 0 and 1. Any other
choices, and any with `--allow-free-form`, are a choice, which exits 0 and names what was picked in `option` (or the
typed answer in `text`). The rest is kabu's, not kabu.py's: an ask exits `0`, `3`, `10`, `20` or `21` (kabu.py exited
with the chosen index, 1 on reject and 124 on timeout), `-p` is `--project` (kabu.py: priority), and an unknown flag is
refused rather than ignored, with kabu's way when it has one (`--quiet`, `--id-only`, `--fyi`, `--category`,
`--metadata`, `--max-retries`, `--client-event-id`, `--check-status`).

A first word is never sent as a message when it is one of kabu.py's other commands, `daemon`, `listen`, `service`,
`steer`, `identity` or `project`: steering and project settings are not in kabu, so it exits 2. Neither is a word that
looks like a mistyped command, such as `kabu sned "hi"`, which is refused with a suggestion. Send such a word with
`kabu send`.
