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.md — clankie 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 linear → Follow Linear → Configure
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:8000 → http://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:
execution: themodel,provider, andeffortthat actually ran the turn, captured as it executed. A/modelor/effortchange under a live conversation belongs to the next turn to execute, not to the one in flight.usage:totalTokenssummed over the assistant messages the provider reported for this turn, andreports, how many reports contributed.
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:
/authand/connectsecret entry — provider keys, OAuth, Linear (MCP token and webhook signing secret), and email/discordsecret entry and lab-user ToS opt-in — Discord tokens never become flags/voice— realtime/TTS provider and brokered credentials/btw,/board,/jump,/conversation,/goal,/layout— live console state
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:
- Decode speed. A large model whose weights get paged out runs one or two
tokens a second regardless of the hardware's rating. Check
sysctl vm.swapusageon the host before blaming the captain. - Prefill. Every turn re-sends the system prompt and the tool schemas, so time-to-first-token at 8k-32k context is paid on each one, not once. A model that chats acceptably can still be unusable in a tool loop.
Revert with clankie model set <provider>/<model> and another
clankie restart captain; nothing about the switch is one-way.
Related
- Operator console — TUI, workspaces, slash commands
- Distribution — install layout and
clankie doctoron a release - Credentials — bot vs user vs internal tokens
- Architecture canonical homes
