View Markdown

CLI reference

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.

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.