CLI

The clankie <noun> <verb> command layer is the canonical local control product. Its noun modules return JSON-shaped results. The argv face prints those results; the fullscreen TUI is chrome over the same functions, with modals that collect flags, render the result, and navigate. Neither face shells out to the other or owns a second config writer (ADR 0012).

Live operator work stays on the service HTTP catalog already shared by the TUI, phone, relay, and menu bar: chat, play, memory, pairing, and conversations are not launcher configuration commands. This page is the contract for agents, scripts, and anyone driving Clankie without a TTY.

clankie help prints the same command index. On every install the file lives at {repoRoot}/docs/cli.mdclankie doctor names repoRoot.

Invocation

clankie                         # start the core service and open the console (TTY)
clankie --version               # also -V
clankie --chat <conversationId> # resume a server-owned operator conversation
clankie <command>               # headless; no TTY
clankie help                    # also --help, -h

--chat is stripped before headless routing. With no command, the launcher starts the clankie service if needed and opens the existing main Clankie conversation, regardless of the launch directory. It does not create a chat. Use --chat ID for another retained conversation, /new for a fresh chat, or /cd PATH to select a project conversation.

Conventions

Rule What it means
One JSON document on stdout Agents parse stdout. Progress and human narration go to stderr.
Exit 0 or 1 0 is success. 1 is failure. doctor always exits 0 — ok means the card was produced.
Secrets never as flags No API keys, Discord tokens, or operator bearers on the command line. /auth and /discord in the console, or the credential broker.
Fail closed, secret-free errors Failure messages never echo tokens, pairing codes, or response bodies.
Host CLANKIE_CONTROL_PLANE_URL (default http://127.0.0.1:4310). CLANKIE_CAPTAIN_URL is a compatibility alias.

--json is required only where the default is human-readable (pairing QR, device table, credential-rotate sentence). Everything else is already JSON.

Command stdout
health, status, doctor, restart, down, autostart … JSON
model …, effort …, image-model …, video-model … JSON
linear …, persona …, games …, fleet …, herdr …, workdir …, discord …, gateway … JSON (herdr open opens the terminal viewer)
play status JSON
send --conversation ID … JSON accepted-run receipt or refusal
memory …, metrics … JSON
play stop JSON when a session is stopping; the sentence Nothing is playing. when idle (still exit 0)
prompt …, memory-card … Plain text: the prompt or card itself, verbatim
seat Interactive (TTY); seat --dry-run is JSON
mcp JSON-RPC for a harness, never for people
pair, devices, operator-credential rotate Human text; pass --json
help This index (plain text)
--version clankie <version>

Do not edit ~/.config/clankie/clankie.json, ~/.config/clankie/settings.json, or Keychain entries by hand.

Commands

health / status

Probe every launcher-owned service and the operator credential. health and status are the same verb.

{
  "ok": true,
  "status": "ready",
  "host": "http://127.0.0.1:4310",
  "owned": false,
  "pid": 12345,
  "operatorCredential": { "present": true, "source": "store", "consistency": "store_only" },
  "services": [{ "id": "clankie", "state": "healthy", "owned": true }]
}

ok is true only when the clankie service is healthy and the operator credential is present without an env/store mismatch. Exit 1 otherwise. status is ready, a service state (unreachable, unhealthy), or operator_credential_<consistency>. Top-level owned and pid appear when the clankie row has them. The payload never includes fingerprints or secret values.

Service ids appear in dependency order: clankie, relay, discord-bridge, discord-user-session, activity, tunnel.

doctor

The install card (ADR 0142). Always JSON, always exit 0. ok means the card was produced. Missing optional tools are facts in remediations, not failures.

{
  "ok": true,
  "kind": "checkout",
  "version": "0.2.0",
  "repoRoot": "/path/to/this/install",
  "model": "xai/grok-4.6",
  "imageModel": null,
  "videoModel": null,
  "persona": { "displayName": "Clankie" },
  "discord": {
    "activeBody": "bot",
    "textIngressEnabled": true,
    "voiceEnabled": true,
    "userSessionEnabled": false,
    "machineGrantUsers": 0,
    "machineGrantGuilds": 0
  },
  "voice": { "realtimeProvider": "openai", "ttsProvider": "openai" },
  "gameplay": { "pokeagentMmoEnabled": false },
  "emailConfigured": false,
  "mcpServers": [],
  "credentials": [{ "id": "openai", "type": "api" }],
  "commands": { "herdr": { "present": false } },
  "herdrPlugin": { "bundled": true, "bundlePath": "…/integrations/herdr-plugin" },
  "laneTools": { "url": "http://127.0.0.1:4310/v1/mcp", "reachable": true },
  "remediations": ["Pick a captain model with `clankie model set provider/model` or `/model`."]
}

kind is checkout or release. Credential entries are ids and types, never secrets. commands currently probes herdr, ffmpeg, yt-dlp (version strings) and herdr-lead (PATH only — never execute herdr-lead --version). laneTools names the streamable-HTTP MCP route that serves a lane's tool bank (ADR 0152); reachable is true when it answers an unauthenticated probe with 401, so the route is served and wants a lane bearer.

restart [service]

Restart launcher-owned services in dependency order (ADR 0055). Default target is all. Progress lines go to stderr; stdout is JSON:

{
  "ok": true,
  "status": "ready",
  "target": "clankie",
  "host": "http://127.0.0.1:4310",
  "owned": true,
  "services": [{ "id": "clankie", "ok": true }]
}

Naming a service restarts it and anything that holds a live claim against it. clankie (captain) also restarts relay and the Discord body, because those processes cache presence and bearer state from this service instance. Stopping is different: down names one service and stops only that service.

Local HTTP services check listeners on their configured port (PORT for Clankie, CLANKIE_RELAY_PORT for the relay, and both CLANKIE_ACTIVITY_PORT and CLANKIE_ACTIVITY_PRODUCER_PORT for the activity). A scratch instance on other ports does not block them. This uses lsof, supplied by macOS and required in Linux installations for this check; if inspection fails, the launcher conservatively refuses matching unowned processes. Named activity tunnels check their configured tunnel name. Foreign processes are never signalled.

When Clankie runs this from his own operator-turn bash, the launcher waits for that turn to settle. Stdout then reports "status": "scheduled" with afterRun, and stderr says the restart is deferred. That is success (exit 0), not a no-op.

down [service]

Stop in reverse dependency order. Default all. Same stdout shape as restart, with "status": "stopped" on success.

autostart enable / autostart disable / autostart status

Start Clankie when you log in. enable writes the user LaunchAgent ~/Library/LaunchAgents/bot.clankie.autostart.plist and loads it into your gui domain. At login it runs this install's launcher as clankie restart clankie, so the service, the relay, and the selected Discord body start in dependency order and the launcher's supervision owns them from there. launchd launches it once (RunAtLoad, no KeepAlive), and only inside a logged-in session: a Mac waiting at the login window starts nothing. On a release install the agent records the current launcher path, so upgrades need no re-enable. It also records your PATH, XDG_CONFIG_HOME, and XDG_STATE_HOME as they were when you enabled it; run enable again after changing them. enable is idempotent (a loaded agent is booted out first) and disable unloads and removes the agent.

{
  "ok": true,
  "status": "enabled",
  "label": "bot.clankie.autostart",
  "plist": "/Users/me/Library/LaunchAgents/bot.clankie.autostart.plist",
  "loaded": true,
  "command": ["/Users/me/.local/share/clankie/current/bin/clankie", "restart", "clankie"],
  "log": "/Users/me/.local/state/clankie/autostart.log"
}

status is enabled, disabled, or stale (the agent file and launchd disagree; run enable). The job's own output lands in log; the services keep their usual per-process logs.

pair [--json] [--timeout SEC] [--review --days N [--count N]]

Mint a one-time pairing offer (QR + code + deep link) for the phone/desktop app. Pairing reuses a healthy app relay or starts a stopped one before minting an offer. If the relay cannot start, no offer is minted. --timeout covers startup and minting together and defaults to 30 seconds; an ordinary offer lives five minutes. A remote CLANKIE_CONTROL_PLANE_URL fails with unavailable: run pairing on that host so its launcher can verify the relay. The console's /pair runs this same command and accepts the same flags.

Human mode writes the QR and code to stdout. Those values are secret-bearing display data — never log or persist them. --json is the agent form:

{
  "ok": true,
  "code": "ABCD-EFGH",
  "deepLink": "clankie://pair/…",
  "expiresAt": "2026-08-30T12:00:00.000Z"
}

--review --days N mints a review offer for App Review or a TestFlight tester who will pair hours or days later: --count (default 3, max 10) independent single-use codes that each live N days (max 31, the public gateway's route window) and survive a Clankie restart. Human output is headed REVIEW OFFER and lists Code 1…N; --json is { "ok": true, "review": true, "expiresAt": "…", "offers": [ { "code", "deepLink", "expiresAt" } ] }. Mint review offers only after the public gateway release that accepts them; an older gateway drops the Mac connection on the first review route.

Failure with --json: { "ok": false, "status": "unavailable"|"unauthorized"|"expired"|"malformed"|"interrupted", "error": "…" }. Without --json, the same message goes to stderr and stdout stays empty when no offers were minted. If a review batch fails after minting some offers, the command still exits 1 and displays those live codes: JSON adds partial: true, review: true, and offers; terminal output starts with PARTIAL. Each code remains usable until consumed or expired.

devices [--json]

List paired devices. Human mode is a table whose SOURCE column reads review for devices paired through a review offer (revoke those after the review) and pair otherwise; --json is { "ok": true, "devices": [ … ] } with "review": true on those rows. Empty human output is No paired devices. The PUSH column shows enabled for an active device with chat access and an enabled delivery reference, otherwise off. This is the host's registration state, not proof that Apple delivered a notification. JSON includes the optional push object with registrationId, sequence, and enabled; disabled records retain their last version. The phone authorizes delivery and controls notification permission. Signing/storage setup is in the gateway guide.

devices revoke <id> [--json]

Revoke one device. Human: Revoked <id> (<name>). JSON: { "ok": true, "device": { … } }.

gateway [status] / gateway set --url URL --host-id ID / gateway disable

Read the public doorway binding or disable it. JSON includes publicGateway, the derived hostId, credentialPresent, enabled, settingsFile, and the restart command. Use the interactive TUI /gateway wizard to sign in with an invited email and one-time code; the rotating account credential goes to Keychain and the wizard restarts Clankie automatically. disable signs this Mac out and removes its installation binding.

set --url URL --host-id ID remains only for legacy static-bearer migration and local verification. It never accepts a secret as a flag.

linear status / linear follow on|off

With the webhook configured, accepted events appear in the Linear inbox conversation (linear-inbox) as External activity messages, including swarm posts delivered by the webhook. Following controls whether those messages wake Clankie:

Following Inbox delivery Automatic model turns
Off (default) Events stay visible in the conversation None from incoming events
On Events stay visible in the conversation New events wake Clankie there

Open the conversation with clankie --chat linear-inbox. It is created on the first accepted event, including while off. Ask Clankie to check the Linear inbox when you want him to read its retained messages; collecting them does not automatically load them into model context. Turning following on does not schedule a turn for every old message. Unread events survive retention; normal conversation retention bounds consumed history.

clankie linear inbox read (or clankie linear inbox) returns a JSON page in items: the oldest unread events, 20 by default (--limit N, up to 100), under 31 KB serialized. --headlines returns one line per event (cursor, time, headline) instead of the quoted payload; --before CURSOR returns the events just before that cursor, read or not, so history can be walked back from oldestCursor as deep as wanted. Reading leaves events unread. Review what was shown, then run clankie linear inbox ack CURSOR with the returned ackCursor; it moves the read boundary forward over events already offered, never past one unseen. Never acknowledge truncated output. Unacknowledged pages survive restart. GET /v1/linear/inbox?limit=&before=&headlines=1 reads; POST /v1/linear/inbox requires { "ackCursor": "..." }. Following controls waking, not collection.

clankie linear follow off suppresses new event-triggered turns and skips model turns still queued; their inbox messages remain. An already-running turn can finish. clankie linear follow on|off applies without a restart, and clankie linear status reads the switch. All three return JSON with ok, following, conversationId (linear-inbox), and settingsFile.

The inbox has its own model context. Linear events do not enter the default Clankie conversation or its bound Herdr seat. Removing the webhook stops inbox delivery; setting following off keeps delivery enabled.

Configure the webhook from /connect linearFollow LinearConfigure webhook. The flow prints the public URL and stores the signing secret in the credential broker (linear-webhook). In Linear's webhook settings, select all available activity events, including issues, comments, projects, and updates. An existing Comments-only webhook also needs its event selection expanded there. Setup does not enable following; Start following / Stop following is a separate choice under Follow Linear.

The consumer accepts signed create, update, and remove activity from any resource type and actor, except the webhook about an object Clankie himself just wrote through Linear's MCP, which is dropped at ingress. A wake carries one headline per new event; the stored message carries the resource, action, author, URL, data and previous values as bounded untrusted context. Shared-account agent posts are not attributed to the human. Clankie decides what merits attention; routine updates need no acknowledgment, dispatch or reply. A delivery supplies context, not new permission. ADR 0168 describes the decision.

The local operator API exposes GET /v1/linear/follow and PUT /v1/linear/follow with { "following": true | false }. Both require the operator bearer and return { "schemaVersion": 1, "following": boolean, "conversationId": "linear-inbox" }. The signed public ingress remains POST /v1/hooks/linear. Changing the local follow switch does not change which events Linear sends; the owner configures that subscription in Linear.

operator-credential rotate [--json]

Mint a new local operator bearer. Existing operator sessions are invalid immediately. JSON: { "ok": true, "status": "rotated", "source": "store" }. The new secret is not printed.

play status

The live embodiment session (GET /v1/embodiment/sessions/live). JSON { "session": … } or { "session": null }. Requires an operator credential; start the clankie service once if none exists.

play stop

Operator kill-switch (POST /v1/embodiment/sessions/live/stop). The play host winds down at the next turn boundary — this is not a process kill. A live session returns JSON. Idle is the sentence Nothing is playing. (exit 0, not JSON).

model [status]

Captain model and every config-declared provider. JSON:

{
  "ok": true,
  "model": "ds4/deepseek-v4-flash",
  "effort": "high",
  "providers": {
    "ds4": { "baseURL": "http://127.0.0.1:8000/v1", "models": ["deepseek-v4-flash"] }
  },
  "restart": "clankie restart captain"
}

ok is false and exit 1 when clankie.json has load issues (issues is then present). model and effort are null when unset. The running service does not pick up a write until clankie restart captain.

model add-local --id ID --base-url URL [--context N] [--models id,id] [--set]

Declare a credential-less OpenAI-compatible local runtime (ds4, Ollama, LM Studio, vLLM, llama.cpp) into global clankie.json. The TUI /provider → “add a local endpoint…” flow uses the same writer.

Flag Meaning
--id Provider id. Lowercased. Letters, digits, ., _, -; no slashes.
--base-url http:// or https://. A bare origin is rewritten to /v1 (http://127.0.0.1:8000http://127.0.0.1:8000/v1). Trailing slashes are stripped.
--context Fallback context window in tokens when the probe does not report one. Default 32768. Must be a positive integer.
--models Comma-separated model ids used when the probe returns nothing.
--set Select the first listed model as captain (providerId/firstId).

The probe is GET {normalizedBaseURL}/models with a 3-second timeout. Local runtimes are unknown to models.dev, so the endpoint itself is the catalog.

clankie model add-local --id ds4 --base-url http://127.0.0.1:8000 --set
{
  "ok": true,
  "providerId": "ds4",
  "baseURL": "http://127.0.0.1:8000/v1",
  "models": ["deepseek-v4-flash", "deepseek-v4-pro"],
  "model": "ds4/deepseek-v4-flash",
  "restart": "clankie restart captain"
}

If the probe fails and --models was given, the write still succeeds and the payload includes probeError. If the probe fails or lists nothing and --models was omitted, exit 1 with { "ok": false, "error": "…" }.

The local runtime is not a launcher-owned service. Start ds4, Ollama, or LM Studio yourself; clankie restart captain only reloads Clankie's config.

model set providerId/modelId

Select the captain. The ref splits on the first slash (model ids may contain slashes). JSON: { "ok": true, "model": "xai/grok-4.6", "restart": "clankie restart captain" }.

model refresh

Refresh the available model catalog. Use this when a newly released model is missing: the captain, gameplay, and commentary otherwise read the installed or cached catalog. The TUI's /model and /provider offer the same refresh. Restart the captain afterward with clankie restart captain.

JSON contains ok, source (network, cache, or bundled), updated, provider and model counts, and the restart command. A successful network refresh exits 0. If the network fails or fetching is disabled, the existing catalog remains usable, but refresh returns ok: false and exits 1. CLANKIE_DISABLE_MODELS_FETCH and an explicit CLANKIE_MODELS_PATH skip fetching.

For Astra, run clankie model refresh, then clankie model set openai-codex/gpt-6-astra and clankie effort set high. The supported efforts are low, medium, high, xhigh, and max. Captain, gameplay, and commentary use the selected model; voice and image/video generation keep their separate selections.

Verified transport settings (2026-09-04):

Provider/model Transport Configured context / maximum output
openai-codex/gpt-6-astra Codex Responses 400,000 / 128,000 tokens
openai/gpt-6-astra OpenAI Responses 1,050,000 / 128,000 tokens

The subscription context value is conservative, not a measured backend ceiling. An openai selection uses the subscription when available; disable the openai-codex provider to select the metered API transport explicitly. In a checkout, pnpm --filter @clankie/clankie verify-model provider/model@effort checks a captain tool-and-image turn, a gameplay action, and commentary using isolated settings. It makes live provider requests. Add --metered for the API transport or --json for a machine-readable receipt. --config-home PATH checks the selection previously written by the CLI under that configuration home. The owner's live selection remains unchanged.

effort [status]

Read the current captain model's stored effort override. JSON: { "ok": true, "model": "xai/grok-4.6", "effort": "high", "restart": "clankie restart captain" }. effort is null when Pi uses its model-supported default.

effort set LEVEL [--model provider/model] / effort clear [--model provider/model]

Set or remove the variant for the named model. Without --model, the currently configured captain model is the target. The TUI /effort modal obtains the supported levels from Pi and calls this writer.

The writer saves the requested effort. At execution, an unsupported effort is refused by name with the supported ladder, consistently across captain, gameplay, and commentary; it is never silently downgraded.

image-model [status] / image-model set provider/model / image-model clear

Read, set, or clear the image generation model. JSON is { "ok": true, "imageModel": "openai/gpt-image-2" }; the value is null when unset. Media generation loads this config per request, so no restart is needed. The TUI /image-model command calls the same functions.

video-model [status] / video-model set provider/model / video-model clear

The same contract for video generation, with a videoModel result field. The TUI /video-model command calls the same functions.

persona [status]

Return the complete owner-authored persona plus settingsFile and the restart command. Character configuration grants no authority.

persona set [flags]

Update one or more persona fields atomically:

Flag Value
--display-name 1–64 characters
--aliases Comma-separated names; none clears
--character-notes Up to 4,000 characters
--chattiness quiet, balanced, or chatty
--reply-policy addressed or all
--live-message-window Whole number from 0 through 100

JSON contains { "ok": true, "persona": { … }, "settingsFile": "…", "restart": "clankie restart captain" }. The TUI /persona modal calls this same writer.

games [status] / games set on|off

Read or set whether the PokeAgent MMO body is available. JSON contains the games.pokeagentMmoEnabled boolean, settingsFile, and "restart": "clankie restart captain". The TUI /games command calls this same writer.

fleet [status] / fleet set --notes TEXT / fleet clear

Read, set, or clear how the owner wants work routed across the agents Clankie leads — which harness is the workhorse, which one reviews, what never goes to which. Up to 4,000 characters of free text.

The default is empty, and empty means he picks a harness per job on his own. Nothing here ships with an opinion; this is where you add one.

It is free text rather than a table of roles because an enum of reviewer/implementer only covers the situations someone enumerated, and the useful ones are conditional ("never codex on Swift", "grok for a hostile read on work that already passed review"). The thing reading it is a model.

The notes reach him as the fleet prompt section, and only on lanes that hold a shell — a room that cannot dispatch would carry the section for nothing. They are preference, not authority: the section says plainly that he still reads the work and decides, and a note here can no more widen his reach than a warmer persona can. Unset renders no section at all.

JSON contains { "ok": true, "fleet": { "notes": "…" }, "settingsFile": "…", "restart": "clankie restart captain" }. The TUI /fleet command opens the same editor and /fleet status prints the same values.

clankie fleet set --notes "codex is the workhorse. claude when it needs skills or long context. grok for a hostile read on work that already passed review. never codex on Swift."

herdr [status|open] / herdr set --runtime auto|bundled|external / herdr set --session NAME

The TUI footer always shows the fleet he is bound to: herdr internal (bundled) for his own fleet, herdr external · NAME for one of the owner's sessions, or herdr unavailable when the service cannot answer. /status repeats it. It is re-read at start, after /herdr, and on /status.

In the TUI, /herdr opens a modal menu showing configured and active sessions. Pick an external session from Herdr's saved sessions (running ones first), select a runtime, or open the active session; the external runtime goes to the same session picker. After saving, choose Restart now to apply the binding or Later to keep it pending. Apply saved changes restarts Clankie, relay and Discord from the menu. Either restart then shows the binding he actually landed on, and warns when the saved session did not answer. Existing Herdr panes stay open. Argument forms such as /herdr status and /herdr set --session NAME remain available.

The binding is resolved at every service start and never written back (ADR 0170). He leads the session or socket the owner named; failing that, the Herdr session the service was launched inside; failing that, his own private bundled Herdr (ADR 0164). A candidate that does not answer is stepped over rather than fatal, so a session that stopped since the last start costs a fallback and never the boot. While he runs, a bound session whose socket stops answering is unbound: he starts his own runtime and points every child he spawns from then on at it.

bundled requires the native release binary or pnpm herdr:build in a checkout, and opts out of both the named and the surrounding session — no session is probed. Panes in the bundled fleet start the owner's login shell with the owner's environment: the private XDG roots that isolate that Herdr never reach an agent, so gh, git, mise and the rest behave as in any terminal. macOS permissions (screen recording, accessibility) follow the process that started the service, so a fleet descending from a terminal carries that terminal's grants; one started by the login-time autostart job may prompt for them once. set --session NAME selects external mode and resolves that named session on restart; set --runtime external keeps whichever session name is already saved. External mode never starts or stops the owner's server. set --runtime auto clears the named session, leaving the surrounding one or bundled. Apply changes with clankie restart captain.

clankie herdr status reports configured herdr, settingsFile, restart, and the running service's active binding (or unavailable). Settings hold the owner's intent and active holds what is live; the two differ whenever a named session is down. The authenticated operator endpoint GET /v1/herdr returns the running binding, following a fallback without a restart; pending settings do not redirect clients. /health includes owned Herdr's state and returns 503 during recovery.

clankie-herdr with no arguments is the shortcut for clankie herdr open. It attaches a native viewer to the selected, already-running local server. With arguments it is the fleet's own Herdr CLI: status, set, and open stay Clankie's, and every other verb is forwarded to the runtime he is bound to, with its binary, its socket, and its configuration. So clankie-herdr pane list reads the fleet, and clankie-herdr server stop ends a bundled fleet that outlives the service (ADR 0164). Running a bare herdr instead reaches whatever build is on PATH, which for a bundled fleet answers a protocol mismatch on a socket it cannot see. Use Ctrl+B, then Q to detach with the default bindings. Closing the viewer leaves Clankie and his workers running. The TUI's /herdr open opens the same viewer and returns to the conversation after detach. Native viewing requires a local service.

Every TUI reads the service's fleet, including from ordinary terminals and unrelated Herdr sessions. /jump, clickable pane IDs, and the optional herdr-lead /board commands target that fleet. Use the viewer to see a focused worker. The optional board requires herdr-lead installed and linked in the selected runtime. Pane-scoped messages and clankie stance carry the source socket in x-clankie-herdr-socket: unrelated pane IDs cannot attach to or change a worker with the same ID in another session.

workdir [status] / workdir set PATH / workdir clear

The captain's working directory — where his shell and sessions run when a conversation names no workspace. Unset (the default) means the operator's home directory. set expands a leading ~ and stores the absolute path. JSON contains workingDirectory (the configured value or null), effective (what the captain runs in after a restart), settingsFile, and "restart": "clankie restart captain".

reset --conversation ID

Archive an idle service-owned global or workspace conversation and start fresh model context under the same ID and title. For the root conversation:

clankie reset --conversation global-default

The TUI's /reset resets the selected conversation. /clear only clears the screen; /new creates another conversation. Reset preserves persona, settings, and durable memory, and clears the conversation's pending goals and watches. The transcript and Pi session remain in conversation-archives/reset-UUID, beside the service's conversations directory. JSON returns the fresh conversation and archiveId.

Reset requires an idle conversation with no open side conversations. A root bound to an external seat refuses reset: end that seat first because its model context belongs to the external harness. The API's reset operation requires expectedRevision; stale requests refuse without changing history.

send --conversation ID [--delivery steer|queue] (MESSAGE | --stdin)

Send to an existing operator conversation through the shared service API. The default steer joins Clankie's active Pi turn at its next input boundary; queue waits for a separate turn after earlier queued work. Either starts a turn when idle. Channel rounds and external seats keep their own delivery behavior (ADR 0091).

clankie send --conversation global-default "Focus on the failing test first"
clankie send --conversation global-default --delivery queue "Then update the docs"
cat notes.md | clankie send --conversation global-default --stdin

--stdin reads the message from standard input. Interior newlines are preserved; surrounding whitespace is trimmed by the shared message schema. Passing both MESSAGE and --stdin is refused.

The command reads the current revision, submits once, and prints the JSON receipt including runId; it does not wait for a reply. Exit 0 means accepted. A revision conflict or offline seat returns its JSON refusal and exit 1; inspect the conversation before resubmitting. Observe replies with clankie --chat ID or the conversation API. The running service and a local captain credential are required.

prompt [--lane LANE] [--sections identity,persona,reach,fleet,address,model]

The system prompt that lane's session starts from, printed verbatim as plain text. The intended consumer is a seat launcher in another harness, which reads it once at startup so the seat begins from the same words the service lanes do.

LANE is operator (the default), discord_voice, discord_presence, or gameplay, and must be the lane the bearer speaks for. The operator bearer comes from the credential broker, so this reads the operator lane.

Sections default to the five a session is built with, joined by one blank line:

Section What it is
identity instructions.md — who he is and how he works
persona The owner-authored character configuration
reach The machine-access or this-room paragraph for that lane
fleet Owner-authored routing preference; shell-holding lanes only, when set
address His own mailbox, when one is connected
model The card naming the model the service lanes run on (ask for it by name)

A seat that carries the identity some other way asks for the rest: clankie prompt --sections persona,reach,address.

memory [status] | search <terms...> | retain|release|forget <episodeId> | correct <episodeId> --summary TEXT

Inspect and curate episodes through the operator API. Output is JSON; success exits 0 and failure exits 1. status shows retention usage and the newest 20 episodes, including private notes. search matches all supplied terms against the note, source lane, and room, returning up to 20 newest matches and the total matched count. Quote a correction's summary as one shell argument.

retain keeps an episode beyond the 128-entry recent ring; release returns it to that ring and may immediately age out an old episode. The retained store holds up to 1,024 episodes and refuses another retain when full. Release or forget an episode before retrying; existing retained notes are never evicted to make room. correct replaces the note while preserving its source and date. forget deletes the episode from both recent and retained recall. /memory exposes the same controls in the console. See Memory for lane privacy and migration behavior.

metrics [--run ID] [--limit N]

Recent settled captain turns, newest first, from the durable ~/.clankie/captain/turn-settled.jsonl the service already appends. Reads through the operator API (GET /v1/captain/turn-metrics), so the CLI and the route answer the same rows. --limit is 1–100 and defaults to 20; --run narrows to one run id.

Each item carries the turn's counters — outcome, per-tool counts, first mutating tool, context occupancy — plus:

Both are null when unknown, and unknown is said out loud rather than defaulted. execution is null for turns settled before the capture existed or when the session had no model bound; usage is null when nothing was reported — never zero, which would read as a free turn. contextTokensStart/contextTokensEnd are context occupancy, not usage and not a charge; no dollar figure is inferred anywhere.

No transcript, tool argument, tool output, or credential appears in the output.

{
  "ok": true,
  "items": [
    {
      "schemaVersion": 1,
      "type": "captain.turn.settled",
      "conversationId": "…",
      "lane": "operator",
      "runId": "…",
      "outcome": "completed",
      "toolCount": { "bash": 6, "read": 2 },
      "mutatingCount": 1,
      "contextTokensStart": 21000,
      "contextTokensEnd": 48000,
      "execution": { "model": "gpt-6-astra", "provider": "openai-codex", "effort": "high" },
      "usage": { "totalTokens": 41200, "reports": 3 }
    }
  ]
}

memory-card [--lane LANE]

The memory card that lane's next run injects, printed verbatim as plain text. The intended consumer is a per-turn hook, so a seat in another harness carries the same recent past his own sessions do.

Filtered by lane exactly as the session's own injection is: operator-private episodes reach only the operator lane. Empty output means the lane has recalled nothing yet, which is not an error.

seat [--resume] [--plugin-dir PATH] [--dry-run]

Sit in Claude Code as Clankie (ADR 0152). Needs a TTY and claude on PATH. The launcher does the things the plugin cannot: it passes --settings with the permission allowlist for clankie commands and, when the plugin is installed from the repo's marketplace (clankie@clankie), enabledPlugins for this session only plus the channel development flag so wakes and escalations reach the session. The plugin stays disabled at user scope, because its forced output style would otherwise make every Claude Code session answer as him. When the plugin is not installed it loads the bundled integrations/claude-plugin with --plugin-dir (tools and skills, no channel). Inside a herdr pane it names that pane clankie once Claude Code is detected there, which binds the pane to his own persona rather than a fleet contact; a second pane claiming the name stays an ordinary fleet agent and is told so on stderr. The pane is un-named again when the session ends.

Every seat starts a new Claude Code session under a recorded id; --resume reopens the last one from the directory it was opened in. The service-side conversation the app pins is the same either way.

--dry-run prints the launch plan instead of launching:

{
  "ok": true,
  "command": "claude",
  "args": [
    "--name",
    "Clankie",
    "--settings",
    "{…}",
    "--plugin-dir",
    "…/integrations/claude-plugin",
    "--session-id",
    "…"
  ],
  "plugin": { "source": "plugin-dir", "path": "…/integrations/claude-plugin" },
  "channel": false,
  "sessionId": "…",
  "resumed": false,
  "cwd": "/Users/me/dev/project",
  "herdrPaneId": "w1:p2"
}

plugin.source is installed with channel: true after claude plugin marketplace add <repoRoot>/integrations/claude-plugin, claude plugin install clankie@clankie, and claude plugin disable clankie@clankie. The plugin README documents the install and what the plugin carries.

mcp [--lane operator]

The seat's stdio side: an MCP server on stdin/stdout that re-serves the service's lane tool bank (/v1/mcp), resolving the operator bearer from the credential broker so no secret lands in a harness config. The plugin's .mcp.json names it; a Codex MCP config names the same command. Only the operator lane has a bearer on this side. stdout is the wire: progress goes to stderr, and the process ends when the harness closes stdin.

It is also his channel. While it runs it long-polls /v1/seat/events and pushes each self-wake, herdr completion watch, and room escalation into the session as <channel source="clankie" kind="wake|watch|escalation" conversation="…" event_id="…">; that polling is what binds the seat as his head, and with no bridge polling the same turns run the pi operator lane. A reply tool answers an escalation by event_id; the reply lands in the escalating conversation as his own message. Claude Code loads the channel only when clankie seat passes its development flag; without it the tools still work and the events are dropped.

mcp --seat

A fleet pane's stdio MCP server: no tools, only the channel. A message to that agent (a DM from the app, or a group-chat turn) arrives as <channel source="clankie" kind="message" conversation="…" event_id="…"> instead of being typed into the pane. The bridge polls only when the parent claude argv loaded server:clankie-seat as a channel; otherwise it serves empty and does not bind. Claude Code binds that channel when the server is in the harness MCP config (claude mcp add -s user clankie-seat -- clankie mcp --seat) and the session is started with --dangerously-load-development-channels server:clankie-seat. --channels server:clankie-seat starts without the development-channels dialog but then rejects server: as not on the approved allowlist. The service's hire path persists the server and passes the dangerous flag for a claude seat.

stance <working|thinking|stuck|hauling|resting> [--note TEXT] [--for SECONDS]

For agents, not for people (ADR 0148). Say what you are doing with your own figure in the commons; the operator's app poses it and moves it accordingly, and prints your note on your Messages row.

Takes no seat argument by design: the seat is resolved from HERDR_PANE_ID in the caller's own environment against the live Herdr census, so this can only ever move the figure the caller is sitting in. --for defaults to 15 minutes and is capped at one hour — a stance is a live statement, and once it lapses the figure goes back to being posed by what its pane is observed to be doing.

{
  "outcome": "stated",
  "seatId": "…",
  "personaId": "…",
  "stance": { "pose": "stuck", "note": "waiting on the build", "statedAt": "…", "expiresAt": "…" }
}

{"outcome":"unseated"} means the pane holds no fleet seat — normal in a plain shell pane, and not an error.

discord [status]

Return stored and effective non-secret Discord configuration:

{
  "ok": true,
  "discord": { "activeBody": "bot", "systemActorUserIds": ["12345"] },
  "effectiveDiscord": { "activeBody": "bot", "systemActorUserIds": ["12345"] },
  "overriddenByEnvironment": [],
  "settingsFile": "/Users/me/.config/clankie/settings.json",
  "restart": "clankie restart"
}

discord is the stored value. effectiveDiscord includes environment overrides, whose variable names appear in overriddenByEnvironment.

discord set --field value […] / discord clear --field […]

Set several fields atomically, or reset fields to their schema defaults. Field flags are the settings.json camel-case names in kebab-case. Lists are comma-separated (none clears); booleans accept on|off, true|false, or enabled|disabled; integer fields require whole numbers. Zod validates the completed settings document and the settings writer rejects token-shaped values.

Group Fields
Application and roles application-id, guild-id, swarm-guild-id, ambient-role-ids, ambient-user-ids, approval-role-ids, owner-user-id
Machine grants system-actor-user-ids, system-actor-guild-ids, system-actor-channel-ids
Text and presence text-ingress-enabled, ingress-guild-ids, ingress-channel-ids, ingress-dm-policy, ingress-dm-user-ids, ingress-context-messages, tool-progress-channel-ids, presence-guild-ids, presence-channel-ids
Voice voice-enabled, voice-guild-ids, voice-channel-ids, voice-channel-id, voice-join-policy, voice-consent-policy, voice-transcript-logging-enabled
Body selection and lab active-body, user-session-enabled, user-session-guild-ids, user-session-channel-ids, user-session-voice-enabled, user-session-voice-channel-ids, user-session-dm-policy, user-session-dm-user-ids
Activity activity-application-id-gba, activity-tunnel-name, activity-tunnel-hostname

active-body is bot or user_session. These commands never accept Discord tokens and do not perform the lab-user ToS opt-in. The TUI /discord modal uses this writer for non-secret fields; its existing secret and opt-in flows stay on the credential broker and service HTTP catalog.

Services

Name on the CLI Process Aliases
all every service, in order (default)
clankie captain + HTTP API on :4310 captain, captain-eve, eve, control-plane, controlplane, cp
relay remote operator relay app-relay, phone
discord official bot discord-bridge, bridge
user-session personal-lab Discord body discord-user-session, lab
activity watch-me-play surface watch, viewer
tunnel cloudflared in front of the activity cloudflared

Unknown names fail closed without signalling any process.

Environment

Variable Role
CLANKIE_CONTROL_PLANE_URL Service origin for probes, pairing, devices, play. Default http://127.0.0.1:4310.
CLANKIE_CAPTAIN_URL Compatibility alias for the same origin.
CLANKIE_OPERATOR_TOKEN Test/CI override for the operator bearer. An env/store mismatch makes health fail. Remove it before rotating.
CLANKIE_LAUNCHER_PATH Path used to spawn a deferred self-restart; autostart enable records it as the login agent's program.
XDG_CONFIG_HOME Config root. Model/provider config is $XDG_CONFIG_HOME/clankie/clankie.json (default ~/.config/clankie/clankie.json).
XDG_STATE_HOME Process records and logs ($XDG_STATE_HOME/clankie/).

Console-only, not missing

These carry secrets, external consent, or live session chrome, so entry stays interactive in the console. The capability exists — only the flag does not:

There is no clankie start, clankie up, or clankie auth. Local model servers are not supervised.

Where a provider key lives

One credential store backs both surfaces: /auth <providerId> writes it, and every service this CLI starts reads it. Provider config in clankie.json never holds a secret — the schema rejects secret-shaped keys — so an endpoint that wants a bearer gets it from the store, keyed by the same provider id as the model ref.

A local endpoint that checks a key therefore needs two things, not one:

clankie model add-local --id ds4 --base-url http://127.0.0.1:8000 --models <id>
# then, in the console: /auth ds4

--models is required there because the add-local probe is unauthenticated: a keyed endpoint answers its GET {baseURL}/models with 401 and the probe reports Could not list models. A genuinely keyless local runtime needs no /auth step — it is served a placeholder bearer it ignores.

Pointing the captain at a local model

Start to finish, with the runtime already serving:

curl -s -H "authorization: Bearer $KEY" http://127.0.0.1:8000/v1/models   # the real ids
clankie model add-local --id ds4 --base-url http://127.0.0.1:8000 --models <id>
# console: /auth ds4              (only if the endpoint checks a key)
clankie model set ds4/<id>
clankie restart captain

Model ids come from the endpoint, never from a guess: a runtime that serves from a directory names the model after that directory, so ds4/deepseek-v4-flash is a 404 where the served id is DeepSeek-V4-Flash-0731-2.4bit-mixed.

Two things decide whether a local captain is usable, and neither shows up in clankie doctor:

Revert with clankie model set <provider>/<model> and another clankie restart captain; nothing about the switch is one-way.