CLI Reference¶
craze [flags]
craze prompt [text] [flags]
craze bridge [flags]
craze attach [flags]
craze ps [flags]
craze new [prompt...] [flags]
craze new --wait [prompt...] [--prompt-file <f>] [flags]
craze send <id> [prompt...] [--wait] [--prompt-file <f>] [flags]
craze wait <id> [--from <handle>] [--timeout <d>] [--max-message <n>] [--json]
craze cancel <id>
craze stop <id>
craze serve [flags]
craze auth login [provider] [--no-browser]
craze auth logout <provider>
craze auth list
craze providers [--hub] [--json]
craze version
craze completion bash|zsh|fish|powershell
craze help [command]
The default command is the interactive TUI. It refuses to start on a non-tty.
Flags¶
These flags apply to craze (the TUI). craze prompt takes most of the session
flags (not --continue); see craze prompt.
| Flag | Description |
|---|---|
--workspace |
Existing workspace directory (default: current directory) |
--model |
Model to start on: an ACP model id, or on native a model alias. Native resolves it against every model it knows — shipped or yours, connected or not — and a model whose provider has no key refuses to start, saying how to give it one. It applies to this start only, and is never remembered as a default; a new native session takes the effort last picked for that model in a session's /model, when the model still offers it (Model memory) |
--effort |
Effort to start at, where the model offers an effort setting: a level's id (high), the id in any case (HIGH), or its name (extra high) — tried in that order, and a step counts only when it matches exactly one level. Set after --model, so against that model's levels, and before the session takes any prompt. A value that matches no level, or more than one, or a model with no effort setting, leaves the model's own effort, with one line in the session's diagnostics (below). Applies to this start only — a --continue or --resume load included — and is never remembered |
--fast, --no-fast |
Start with the model's fast setting on, or off, where the model offers one (cursor's per-model fast). Neither leaves it as the model has it. Set as --effort is; a model without one is a line in the session's diagnostics (below) |
--agent-bin |
Path to the agent binary for this launch's own provider (or CRAZE_AGENT_BIN); a session of another provider uses its own. See Agent binaries |
--provider |
Provider: cursor, grok, gx (ACP agents) or native (runs inside craze). Empty is unset. Unknown id exits 2 |
--force |
Spawn the agent with --force / --always-approve (yolo). Default: on |
--no-force |
Disable yolo and handle permission requests |
--no-mouse |
Disable mouse reporting (wheel scroll and clicks) |
--no-background |
Keep the terminal's own background and text colours instead of the theme's. Same as background = false |
--no-host-status |
Report nothing to the herdr pane or roost tab craze runs in, and leave the agent's environment whole. Same as host_status = false. See Host status |
--theme |
TUI theme preset. See Configuration |
--ask |
Set session mode to ask after session/new |
--plan |
Set session mode to plan after session/new |
--plugin-dir |
Extra plugin directory whose commands and skills craze expands (repeatable). Relative to the workspace; a missing directory is a diagnostic (see below), not an error; ignored (with a diagnostic) on grok, gx and native |
--continue, -c |
Load the newest session in this workspace instead of starting a new one, skipping sub-agent sessions. No matching session exits 1 |
--version, -v |
Print the craze version and exit, as craze version does. Answered before any argument is checked |
--resume, -r |
Open a picker of the last 10 sessions in this workspace. Empty index exits 1 the same way; Esc in the picker exits 0 |
--ask and --plan are mutually exclusive. So are --fast and --no-fast,
and --continue and --resume (exit 2).
--effort and --fast/--no-fast are set inside the session's start, after
--model and --ask/--plan, so no prompt — typed, queued, or sent by
another client — reaches the agent before them; that holds whether the
session runs in a detached host or, under
detach = false, in the TUI's own process. A setting the agent refuses is a
line in the session's diagnostics, and the session starts without it; one the agent does not
answer within 15 s fails the start. Each one that is skipped or
refused is also a diag line in the session's
journal (effort_unmatched,
fast_unmatched, start_setting_refused).
The session's diagnostics (those lines, and a --plugin-dir one) go to the
host's log for the ordinary, detached craze, and
to stderr in process (detach = false) or for a craze serve run by hand.
--provider on the TUI skips the startup picker. Without it, $CRAZE_PROVIDER
then provider in the config file then cursor is the default, and the picker
lets you change it before Start. The picker holds its choice to the same flags
--provider is held to: picking native with --agent-bin or
CRAZE_AGENT_BIN set shows the message --provider native would exit 2
with, as an error row, and starts nothing (see TUI reference).
--agent-bin and CRAZE_AGENT_BIN are the resolved provider's: another
provider picked there runs the binary [agents] names for it, or the one on
PATH (Agent binaries).
craze prompt has no picker; it uses the same precedence. craze frame
ignores env and config and defaults to cursor unless --provider is passed.
./bin/craze
./bin/craze --provider grok
./bin/craze --workspace ../proj
./bin/craze --no-force
./bin/craze --theme gruvbox --no-mouse
./bin/craze --continue
./bin/craze --resume
./bin/craze --provider grok --continue
--continue and --resume¶
Both load a session out of the session index
(~/.craze/sessions.jsonl — see
Configuration), filtered to the current
workspace. An explicit --provider filters the search further; a
provider that only came from $CRAZE_PROVIDER or config.toml does not, so
a plain --continue finds the newest session for this workspace across
every provider, and picker rows show each row's own provider. Whichever row
is loaded, its own provider starts the agent — it overrides whatever
--provider would otherwise have resolved to, and it is never written back
as the persisted default (see
Configuration).
No matching row:
craze: no session to continue in /abs/workspace
craze: no session to continue in /abs/workspace for provider grok
exit 1, and no TUI is drawn. --continue skips sessions started for a
coding agent (craze new --wait's, marked unattended in the index): it
loads the newest session that is not one. A workspace holding only such
sessions refuses, and says how many there are:
With exactly one it reads (1 sub-agent session here: craze -r lists them).
The count is the marked rows --resume would list for that workspace and
provider filter. --resume shows them, after the other rows (they fill the
picker's ten only when fewer than ten others remain). A session index that exists but craze cannot
parse fails the same way, exit 1, naming the file; the index is never
rewritten by a failed read. --continue and --resume together is a usage
error, exit 2, and --resume on a non-tty gets the ordinary non-tty
refusal — checked before either flag is considered, so it fires even for a
workspace with no sessions at all.
A load the agent itself refuses (no session/load support, an unknown
session id, a working directory Grok or gx reject) is shown as an on-screen
error exactly like a failed session/new; the row stays in the index, craze
never falls back to starting a new session, and the exit code on quit is 1.
See /rename and
Resuming a session for what a restored session
looks like in the TUI.
A session already running in another craze¶
Everything below describes the claim as the in-process path makes it. With
detached hosts (the default) the
launching craze first attaches directly to a live host that serves the
session, and otherwise spawns a host, which makes this same claim and answers
held when another craze has it — the launcher then finds the holder and
attaches to it. The refusals are worded the same, but the detached launcher
prints no line before the TUI for a plain reattach; only when the command line
gave flags a new session would take does it print the ignored-flag note
(craze: that session is already running (pid N); attached to it (ignored:
--model)), after the TUI exits. The stderr line shown below is the in-process
path's.
--continue claims the row it loads before anything is built: its own lock
under ~/.cache/craze/locks/, independent of CRAZE_HOME and the runtime
directory (see Protocol reference). A row
that already has a craze id is claimed first, and a session another running
craze already holds is attached to instead of refused — the second
craze -c becomes craze attach --session <id>, resolved
through the holder's host id, after one line on stderr:
The flags a new session would take (--model, --effort,
--fast/--no-fast, --ask, --plan, --agent-bin, --provider) do not
apply to an attach; each one this command
line explicitly passed is named in the note — whether the flag was
changed on the command line, not what it is worth — e.g. (ignored:
--model, --provider). Nothing is built, spawned, bound, or claimed for it.
A nonempty, explicit --provider also does one more thing the others
don't: it filters the session index before the lookup that finds the row
to claim (above), so a --provider that excludes
the held session's own provider means that row is never found at all — the
ordinary no-match refusal fires (craze: no session to continue … for
provider …), and the attach path, note included, is never reached. An
empty, or all-whitespace, explicit --provider ('', ' ') does not
filter the index — only a value with visible content counts — so the row is
still found and the attach path is still reached — and --provider is still
named in the note, exactly like the others, because it was still changed on
the command line. Two cases keep the plain "session held" refusal instead of
attaching:
- A legacy row (no durable craze id yet, from before
crazeIdexisted) is refused before the claim, whoseEnsureCrazeIDwould otherwise mint and write its id for a load the command line can never start. - The spawn flags this command line gives are refused first, once this
run holds the claim:
craze -c --agent-bin Xfor a held session still attaches (an attach takes none of the spawn flags), but for a session this run just claimed itself, the same usage errors a new session's flags would give still apply, and the claim is released.
A holder whose live registry entry does not serve the claimed session (or
whose socket has not been bound yet) keeps the plain refusal instead, with
why appended: — it serves no control socket or — it serves another
session. A holder whose lock names no pid yet (pid ?, the instant after
its flock) also keeps the plain refusal:
exit 1, and no agent is spawned — nothing is built once the row's session is found to be held and cannot be attached to. Two narrower refusals cover the claim mechanism itself, not the session:
The first is another writer holding the session index past a two-second bound; the second is a legacy row with no durable id that left the index between being read and being given one (another loader may already hold it under an id this one does not know about — trying again re-reads the index).
--resume's picker claims each row the same way, when you press Enter
on it — but it never attaches itself: a held row's error names the holder's
pid, plus craze attach --session <hostId> when the holder's live entry
actually serves that session, or why it cannot be reached instead (— it
serves no control socket, — it serves another session); the picker keeps
running either way, in place of loading it.
An unreadable lock tree or session index does not fail one way. The initial
lookup — --continue's Latest, or --resume's scan for rows — still exits
1 if it cannot be read, the same as no session found at all: there is no row
yet to warn about. Once a row is in hand, two things warn on stderr and let
the load proceed unclaimed: a legacy row (one with no durable
crazeId) whose id cannot be written to the index, and any failure to take
the session's claim other than another craze holding it (the lock tree
unusable, the lock file unopenable or unwritable) — whether the row already
had an id or was just given one. An unclaimed load has no protection
against a second craze loading the same session; the lock exists to stop
one, not to lock you out of your own session because a filesystem
misbehaved. Three things still refuse instead: the session already
held by another craze, a busy index, and a legacy row that
vanished from the index before it could be given an id.
Native sessions¶
Native sessions are indexed and resumable on the same terms as cursor's and
grok's: --continue and --resume find and load a native row the same way,
and --plan/--ask set the resumed session's mode explicitly, overriding
whatever mode the transcript's own last mode_change recorded (see
Resuming a session).
--agent-bin/CRAZE_AGENT_BIN refuse a native row exactly as they refuse a
new native session — it has nothing to spawn — exit 2, before any index
write, crazeId mint, or session claim (--continue still reads the index to
find the row before the refusal is reached):
craze: --agent-bin cannot be used with provider native, which runs inside craze
craze: CRAZE_AGENT_BIN cannot be used with provider native, which runs inside craze; unset it
A row whose transcript file does not exist yet — its first prompt was cancelled before any output came back — is not a failed load: craze opens an empty session under that same id instead of refusing. This is the one deliberate exception to "a load never falls back to a new session" above — it is still the same session, continuing from nothing, not a different one, so nothing is silently switched out from under you.
A native transcript adds its own lock beneath the claim
above: a transcript another craze
process already has open refuses the load the same way. A transcript left
with a torn or cut-off tail by a crash is trimmed back to its last complete
step on open, and the dropped bytes are kept beside it in a
<stem>.torn-<UTC stamp> file — the transcript's name with its .jsonl
suffix removed, e.g. 20260926T093537Z_<id>.jsonl's torn tail lands in
20260926T093537Z_<id>.torn-20260927T101500Z — rather than discarded; a tail written by
a newer craze build — one this version does not recognize — is never
trimmed, and the load is refused instead, so nothing unrecognized is thrown
away.
Sessions outlive their terminal¶
The ordinary craze no longer runs the session inside the terminal's
process. It spawns a detached host — craze serve, in a
session of its own with no terminal — waits for the host to say it is ready,
and runs the TUI as that host's client over the control
socket. What follows:
- Closing the terminal keeps the session. A closed terminal (SIGHUP), a
SIGTERM to
craze, and a dropped ssh connection are all view closes: the TUI goes, the host and its agent stay, and a turn already under way keeps working. craze -creattaches.--continue(and a--resumechoice) of a session a live host serves attaches to that host directly: nothing is spawned, and no note is printed unless the command line gave flags a new session would take (craze: that session is already running (pid N); attached to it (ignored: --model)).craze attachreaches the same host./exitends the session, everywhere./exit, Ctrl+D and the second Ctrl+C ask the host to stop the session (session.stop): the agent is closed, the session stays resumable if it has an index row (it has one once it was prompted), and every other client attached sees it end. The TUI waits at most two seconds for that, in total; a second quit ends the wait at once. A host that cannot stop its session (a TUI-hosted one, or one from an older craze) is detached from instead, withcraze: that session runs in an older craze; close it there; any other failure printscraze: the session may still be running: <reason>. Exit 0 either way — except after a failed start, which exits 1.- An unattended host does not run for ever. A host with no client
attached and nothing in flight exits after
host_idle_exit(an hour by default), saving the session like any stop. A host whose session was never prompted has its idle limit capped at five minutes (after the startup grace, and only while nothing keeps it busy); one whose start failed, as soon as its launcher has been told. - A quit before the session is up leaves nothing behind. A host this launch spawned whose session never came up in the TUI (quit while starting, a failed start) is stopped; a host that was already running is never stopped that way.
- A host that could not come up is a start failure. The message names the
host's log (
~/.cache/craze/host-logs/<hostId>.log, see Host logs) and ends; CRAZE_DETACH=0 runs sessions inside craze instead; craze exits 1. A host that refused the session asked for (no such session, a held one that cannot be attached) shows the refusal as the in-process path does, from a picker as its error row.
The switches that turn this off (detach = false, CRAZE_DETACH=0, or the
control socket switched off) run the session inside the TUI's own process, as
before: closing the terminal ends it, and nothing outlives it. See
Configuration.
The control socket¶
Every host binds a control socket in the runtime namespace and serves its
session over it: the detached host craze serve by default,
or the TUI's own process when detaching is off
(see
Protocol reference), which is what
craze bridge and craze attach reach.
A launching craze binds nothing of its own; the in-process path binds only
for a run that actually starts the TUI — a --continue that attaches instead
(above) or refuses binds nothing and creates no socket of its own.
control_socket = false in config.toml, or CRAZE_CONTROL_SOCKET=0 (or
false) for one run, turns it off; see
Configuration. The session lock above
is still taken either way — it does not depend on the socket.
craze serve¶
Hosts one session with no terminal, over its control socket, until a client
ends it (session.stop), the process gets SIGTERM or SIGINT, or it has been
idle for too long. It builds the session
exactly as the TUI does: the engine, the claim on the session, the journal
and the session-index writes are the host's, and craze
attach (or the ordinary craze, which is a client of it)
joins it. You rarely run it yourself: the ordinary craze spawns one per
session (its own session, stdio on /dev/null) and attaches to it. Run by hand
it is the same host in the foreground, logging to stderr; SIGHUP is ignored.
It takes the session flags the TUI takes — --workspace, --model,
--effort, --fast/--no-fast, --agent-bin, --provider,
--force/--no-force, --ask/--plan, --plugin-dir, --continue/-c —
with the same refusals in the same words, and:
| Flag | Description |
|---|---|
--load |
Load a session by id instead of starting one: a craze session id, or <provider>:<session id> for an index row that has no craze id yet. It runs in the row's own workspace: an explicit --workspace naming another directory is exit 2, a row whose directory is gone is exit 1, and no such row is exit 1. --provider filters as it does for -c |
--log |
Write the host's diagnostics, its crash output and the agent's stderr to this file, rotated once at 4 MiB (default: stderr) |
--host-id and --no-host-status also exist, hidden: the spawner passes them
(the id it minted for the host, and the launching TUI's host-status choice).
They are internal, not a user interface.
A session another craze already holds is exit 1 — craze serve: that session
is already running (pid N). A control socket switched off is a usage error
(exit 2): a headless host is reachable through nothing else. A session that
cannot be claimed (an unusable lock tree) is exit 1 with nothing left behind.
A host whose start failed (an auth failure, say) stays up, listed and
attachable, so its client is told why, and goes as soon as it has none.
The host's log and its retention are under Host logs.
craze prompt¶
Run a headless ACP turn (and optional follow-ups). Text comes from the argument, or from stdin when it is not a tty.
./bin/craze prompt --json --provider grok --agent-bin ./bin/craze-fake-agent "hello"
echo "hello" | ./bin/craze prompt --json
./bin/craze prompt --json --follow-up "and then this" "start here"
| Flag | Description |
|---|---|
--workspace |
Existing workspace directory (default: current directory) |
--model |
Model to start on: an ACP model id (session/set_model after session/new), or a native model alias, resolved as the TUI's --model is, at the effort remembered for it. Never remembered. Without it a native run starts where a new TUI session would, on the remembered model (Model memory) |
--effort |
Effort to start at, where the model offers an effort setting: a level's id or name, matched as the TUI's --effort is (see Flags). Set after --model and before the first prompt; applies to this run only |
--fast, --no-fast |
Start with the model's fast setting on, or off, where the model offers one; neither leaves it as the model has it. Both together exit 2 |
--agent-bin |
Path to the agent binary for this launch's provider (or CRAZE_AGENT_BIN, then [agents] in config.toml: Agent binaries) |
--provider |
Provider: cursor, grok, gx (ACP agents) or native (runs inside craze) |
--follow-up |
Additional prompt on the same ACP session (repeatable) — the headless queue, see below |
--permission-decision |
Headless permission answer: allow-once or reject-once (repeatable) |
--force |
Spawn the agent with the provider's yolo flag (--force for Cursor, --always-approve for Grok). Default: on |
--no-force |
Disable yolo and handle permission requests |
--ask |
Set session mode to ask after session/new |
--plan |
Set session mode to plan after session/new |
--plugin-dir |
Extra plugin directory whose commands and skills craze expands (repeatable). Relative to the workspace; a missing directory is a diagnostic (see below), not an error; ignored (with a diagnostic) on grok, gx and native |
--json |
Write only JSON events to stdout |
Without --json, only the main session's assistant text is written to
stdout — a sub-agent's text is never printed, so a piped reply stays the
agent's own answer.
The text of each run is a paragraph of its own. A run that starts after a
tool call, a thought or the end of a turn is separated from the output so far
by a blank line, however the earlier text ended: nothing is added at the start
of the output or after a blank line, one newline after text ending in one, and
two otherwise (a chunk that begins with newlines supplies its own). A
sub-agent's tool calls and thoughts never start a run. --json events are
unchanged.
--follow-up is the queue¶
Every --follow-up is queued before the first turn and sent one per settled
turn, in order — the same queue the TUI's Enter fills. It holds 32 messages
of up to 32 KiB each; past either bound the run stops with an error rather
than sending a truncated message.
The chain still ends the way it always did: a stop reason that is not
end_turn, or an error, stops it with exit 1 and no further turn. SIGINT,
SIGTERM and SIGHUP (the hangup a closed terminal sends) clear the queue
first and then cancel, so nothing starts behind the signal; the run exits 1.
When the agent starts a turn craze did not prompt for — Grok's interject fallback (see Queued messages), or a native session's wake — the next queued message waits for it, up to a minute, and the run exits 1 with a note on stderr if it is still going after that.
JSON events¶
--json writes one JSON object per line:
type |
Carries |
|---|---|
text |
Assistant reply chunk; agent names the sub-agent when the chunk belongs to one |
thought |
Reasoning chunk; agent as on text |
user |
A chunk of a sub-agent's prompt (with agent), or a Grok interjection on the main session (with "interjection":true) |
command |
A plugin command or skill craze expanded into the prompt |
queue |
One change to craze's own message queue |
foreign_turn |
A turn the agent started without a craze prompt, bracketed |
compaction |
One end of a native session's compaction of its context; agent as on text |
tool |
Tool call create/update, merged by id; agent as on text; a sub-agent tool also carries task |
todos |
Todo list replace or merge |
permission / question / plan |
Blocking request. A permission is answered by --permission-decision (none left: rejected); a question gets its first option; a plan is accepted |
subagent |
Sub-agent lifecycle |
title |
The title the session took |
done / error |
Turn finished / turn failed |
Every line that came from the session's event stream carries seq, an integer,
as its second key — right after type. It is the number the session's
event log gave that event, the same one the session journal records for it, so
a headless caller can line the two up.
seq is increasing but not contiguous. The JSON above is a projection:
events it has nothing to say about (a mode change, a title-less metadata
update) are dropped, and a dropped event still took its number. Rely on the
order of two lines; never on the arithmetic between them.
The lines craze writes itself carry no seq at all, and craze never
invents one: a startup failure (an error line written before any session
existed) is the one left. The queue removed line for a row that left the
queue just as a signal landed used to be one too; it now carries a seq like
any other queue line, because taking a row and starting its turn are one
transaction in the engine (session control S1b). An absent seq means craze
said it, not the agent.
agent is the sub-agent's own session id (grok) and appears only on lines
that belong to one; main-session lines have no agent field. Grok streams a
sub-agent's session on the same connection, so its prompt, thoughts, tool
calls and replies arrive as tagged user, thought, tool and text lines.
subagent is the lifecycle,
{"type":"subagent","event":"spawned|progress|finished","id":…}, with
attemptId, parentId, status, description, subagentType, model,
toolCallId, durationMs, toolCalls, turns, tokens, output and
error alongside, each omitted when empty, plus transcript (whether the
provider streams one), which is always present — true or false. Grok's
lines come from its own lifecycle notifications; cursor
sends none, so craze synthesizes the same lines from the cursor/task
receipt. tool.task carries description, model, agentId, durationMs
and status (running, completed, failed, cancelled).
command is one plugin command or skill craze found on disk and expanded
into the prompt (see Slash commands):
{"type":"command","name":"watch-pr","qualified":"git-commands:watch-pr",
"plugin":"git-commands","kind":"command","path":"/home/…/watch-pr.md",
"text":"…"}. name is the entry's own name and qualified its
plugin:name spelling — the same row under two names when the bare one was
free; text is the block exactly as it went out on the wire, the only way a
headless caller can see what the agent was actually given. Plain mode
prints none of this.
queue is {"type":"queue","event":"queued|edited|removed|sent","id":"q-1",
"position":0,"version":0,"text":"…"}. position is the row's index when
the change happened, not where it sits now — so a sent line from the drain
carries 0 (the head was position 0 when it went), every line of a clear
carries 0 for the same reason, and a sent line for the TUI's send now
carries whatever row the user picked. --follow-up produces a queued line
per follow-up before the first turn and a sent line before each turn after
it. Plain (non---json) mode prints none of this.
foreign_turn is {"type":"foreign_turn","event":"started|ended","id":"…",
"text":"…"}; the id is Grok's interject-fallback-… prompt id, or wake-<n> for a
native session delivering a background sub-agent's result with no turn running. Nothing
drains between the two lines.
compaction is {"type":"compaction","phase":"started|ended",
"reason":"auto|manual|overflow","tokensBefore":…,"tokensAfter":…,
"error":"…"}, with agent for a sub-agent's: the native session summarizing
its conversation to free context, on its own near the model's window, after
a request the window refused, or for /compact [focus] sent as the prompt
(manual). A started line is followed by its ended one; the counts are
the context's estimated size before and after, 0 on a started line and
for the after of one that failed, which carries error instead. agent and
error are omitted when empty.
done is not EOF: it says the turn ended, not the sub-agents.
Lifecycle lines for a still-running sub-agent may follow the turn's done —
grok sends subagent_finished after prompt_complete when a turn is
cancelled — and a sub-agent spawned in one turn may report during the next.
Before every exit, craze prompt drains the stream: when no sub-agent was
ever spawned nothing is added; otherwise events keep being read and written
until every sub-agent is terminal, then 250 ms pass with no event, or 1.5 s
elapse — whichever comes first.
craze bridge¶
A pure byte pump for an SSH client: resolves the one
running craze session — or --session's — dials its control socket, and
relays stdin/stdout to it verbatim. It speaks no protocol itself; hello is
the client's job, not this command's.
| Flag | Description |
|---|---|
--session |
A craze session id, a provider session id, or a host id, or a short form of one (short ids) (default: the one running session) |
--hub |
Relay to this machine's hub instead, starting it when none runs. Not with --session |
--no-start |
With --hub only: attach to a hub that is already running and never start one. With none running it exits 3 (craze bridge: no hub is running). Without --hub it is a bridge error, exit 1 (craze bridge: --no-start needs --hub) |
--session's id is resolved against the registry under the bridge
process's own $HOME (~/.cache/craze/hosts/*.json; see
Protocol reference) — HOME wins over the
account database, so neither CRAZE_HOME nor $XDG_RUNTIME_DIR in an SSH
login's own environment can hide or misdirect it, but the SSH exec is
assumed to run with the same HOME as the tab that started the host:
true for an ordinary SSH login as the same user, not guaranteed in general.
With no --session, exactly one live host in total is the target; zero or
several is an error listing them on one line.
Short session ids¶
craze bridge --session and craze attach --session take the short id
craze ps prints, not only a whole id. The tiers are tried in
this order, and the first tier that matches decides:
- an exact match on any field of any live session: its craze session id, its provider session id or its host id;
- a craze session id's end (
craze ps'sSESSIONis its last eight characters); - a craze session id's beginning;
- a host id's beginning.
Within the deciding tier, two different sessions are ambiguous and refused
(the error lists them, as an exact clash always was); one session matching by
several fields counts once. A provider session id must be given whole: its
shape is the agent's. A suffix or a prefix needs at least four
characters; a shorter one matches nothing (no session <id>). The hub's
session.connect stays exact: the roster gives it whole ids. craze new
--load resolves a saved session's id against the session
index by tiers of the same kind.
The pump (roost's bridge rules):
- stdout carries only bytes read off the socket, flushed as they arrive.
- stdin's EOF half-closes the socket's write side (
CloseWrite) and keeps reading the socket — the session may still have plenty left to send. - From stdin's EOF the bridge looks at its stdout about once a second: a
reader gone entirely (a pipe whose reader closed, a Unix stream socket
whose peer closed, as when an SSH connection drops) closes the socket and
exits 1,
craze bridge: its reader went away (stdout closed), since a quiet session would never write the byte whose failure says so. The look starts at stdin's EOF; a reader lost while stdin is still open is found by the next failed write instead. A stdout that is neither a pipe nor a Unix stream socket (a file, a terminal, a TCP socket) is not looked at. - The socket's own EOF ends the pump and exits 0.
The error contract: every failure is one line on stderr, exit 1 (the one exception, --hub --no-start with no hub running, exits 3; see below). That
covers every failure before the pump starts — resolving the target,
dialing, the peer check — and "nothing on stdout" is true only for those:
flag parsing is included, so a stray removed config-file environment
variable (see Configuration) in an
SSH environment still reads as a bridge error in this same shape. Once the
pump is running, stdout carries socket bytes only (see below); a failure
mid-stream still exits 1 with the same one stderr line, but after whatever
socket bytes were already relayed to stdout:
craze bridge: no session running
craze bridge: no session <id>
craze bridge: 2 sessions running; pass --session <id>: <id> (grok, /a), <id> (cursor, /b)
craze bridge: session <id> is unreachable: <reason>
craze bridge: no session <id> is the contract line for an unknown
--session: a device reading it back knows the id it asked for is not (or no
longer) running here, not that something else broke.
--hub relays to the hub instead of one session's host,
starting it when none runs (found, like a host, under the bridge process's
own $HOME). The same pump and the same error contract apply; the client
says hello to the hub and then reads the roster
or splices itself to a session with
session.connect. Plain craze bridge is
unchanged. --hub --no-start attaches to a running hub only and never
starts one, so a caller asking "is there a hub to reach?" does not create the
very hub it is asking about. With a hub it splices exactly as --hub does;
with none it prints craze bridge: no hub is running and exits 3, the one
bridge failure that is not 1. A hub that is running but does not answer
(wedged) is ended, as everywhere, and nothing replaces it, so the answer is
the same: no hub is running. A wedged hub that cannot be ended (its start
token unreadable, or the signal refused) is a failure like any other: exit 1. Its own failures:
craze bridge: --hub and --session cannot be used together: the hub's session.connect names the session
craze bridge: no hub: <reason>
craze bridge: no hub is running
craze bridge: --no-start needs --hub
craze bridge: the hub is unreachable: <reason>
| Code | When |
|---|---|
| 0 | The socket closed cleanly (the session ended, or the far side hung up) |
| 1 | Every failure above but no hub is running: no session, an ambiguous --session, an unreachable socket, a read or write error. Never 255 (ssh's own exit code for a failed connection) |
| 3 | --hub --no-start and no hub is running (a wedged hub, ended, counts as none) |
| 127 | Not craze bridge's own exit: what an SSH client sees when the far end's shell falls off the published binary ladder (see Protocol reference) without ever reaching a craze to exec |
craze attach¶
Runs the full TUI over a running craze session's control socket: the session another craze process in this directory is hosting, or
--session's. Nothing is spawned, nothing binds a socket, no session claim is
taken, and no index row is written. Quitting it (/exit, Ctrl+D,
Ctrl+C when idle) ends the session on a host that can stop it,
as it does in every client; only a closed terminal or SIGTERM leaves it
running (see Quitting vs. the session ending).
| Flag | Description |
|---|---|
--session |
A craze session id, a provider session id, or a host id, or a short form of one (short ids) (default: the one session running in this directory) |
--theme |
TUI theme preset. See Configuration |
--no-mouse |
Disable mouse reporting (wheel scroll and clicks) |
--no-background |
Keep the terminal's own background and text colours instead of the theme's |
--continue/-c, --resume/-r, --provider and --model are refused —
an attach joins a running session rather than starting or loading one:
Resolution¶
With an explicit --session, the one entry it names (matched the way
craze bridge matches: by craze session id, provider
session id or host id, or a short id); an invalid --session — whatever its value, ""
included — is a usage error, exit 2, before a registry read builds a path.
No match is exit 1:
and several matches is exit 2, listing them (ids do not collide in practice, but nothing here assumes it).
With no --session, the live sessions whose workspace is the current
directory (both sides made absolute, cleaned and symlink-resolved, so
/tmp/x and macOS's /private/tmp/x are one directory): exactly one is the
target. Several is exit 2:
craze: 2 running craze sessions in /abs/workspace:
01a0bbe5… /abs/workspace My session
01a0bbe6… /abs/workspace
attach to one with: craze attach --session <id>
None in this directory is exit 1, naming the sessions running elsewhere (id, workspace, and title when the session index has one) plus the same hint — or just the first line alone when nothing runs anywhere:
craze: no running craze session in /abs/workspace
01a0bbe7… /abs/other Other session
attach to one with: craze attach --session <id>
A listing never connects to a session just to describe it: the workspace and title come from the registry and the session index alone.
Resolution runs before the terminal is checked, so a usage error or an
unambiguous exit 1/2 above is reported whatever stdout is; only once a target
is resolved does attach refuse a non-tty the same way the TUI does (a real
termios check per OS, so craze attach >/dev/null is refused too):
A resolved target that cannot be dialled — the socket is gone, or the peer check fails — is exit 1:
Viewer mode¶
The pickers, provider persistence, session swaps, host status reporting
(the herdr pane or roost tab belongs to the host) and session-index writes
are all the host's; an attach does none of them. Shell mode (!) still runs
locally, in the session's own workspace. The permission chip shows the
host's permission mode (its --force/--no-force) when the host advertises
it, and falls back to craze's own default (yolo) for an older host that does
not.
Keys¶
Identical to the host TUI's: the session — its queue and its
turn — is shared, so the first Ctrl+C while a turn works cancels
the turn and clears the queue for every attached client, exactly as it
does in the host TUI. Ctrl+C when idle, a second Ctrl+C,
Ctrl+D and /exit all end the session
(below).
Quitting vs. the session ending¶
Quitting craze attach is the same act as quitting the host's own TUI: Ctrl+C when idle, a second Ctrl+C,
Ctrl+D and /exit ask the host to stop the session
(session.stop). It ends for every client attached and stays resumable if
it has an index row; the attach exits 0 and prints nothing more (1 after a
failed start). Two things do not end it, and are view closes — the attach
detaches, exits 0, and the session goes on running on its host: SIGTERM, and a
closed terminal (SIGHUP).
A host that cannot stop its session — a TUI-hosted one (the in-process opt-out), or one from an older craze — is detached from instead, and one line on stderr says so:
A stop that went unanswered (the host stopped reading, or the two-second deadline passed) is exit 0 too, saying so:
An attach TUI whose session ended some other way — another client's stop, the host's own quit, the host's idle exit — prints one line on stderr once its own screen is restored, and exits 0:
A transport failure the client's redials could not recover from — the session may still be running, but this client lost it — is exit 1 instead:
craze -c of a running session¶
See A session already running in another craze:
--continue/-c (and a --resume choice) of a session already running in
another craze attaches to it through this same command, rather than
refusing.
craze ps¶
./bin/craze ps
./bin/craze ps --json
./bin/craze ps --no-hub
./bin/craze ps --saved [-C <dir>] [--json] [--no-hub]
Lists every running craze session of this user on this machine — every live
host in the registry under $HOME that has a
craze session id, whichever terminal or CRAZE_HOME started it — one row
each, from the hub's roster:
SESSION STATE PROVIDER MODEL DIR SINCE TITLE
0000a001 needs you cursor sonnet-4.6 ~/proj-a 2m fix the flaky test
0000c003 starting cursor - ~ 3h add the ps command
0000e007 idle native muse-spark-1.3-contributor ~/newer 5m -
| Column | |
|---|---|
SESSION |
The last eight characters of the session's craze id: its random end. A craze id is a UUIDv7, whose first characters are its clock's, the same for every session started within a minute or so. --json, craze attach --session and craze bridge --session take the whole id |
STATE |
The session list's state: needs you (an ask is open), working, starting (its host has not answered yet), failed (its start or its last turn failed), idle, or unreachable (listed, and its socket does not answer) |
PROVIDER |
The session's provider |
MODEL |
The session's current model id, as --model takes it (its row's model), cut with … to 32 cells; - when its host does not say (a craze from before plan 035) or has not answered yet. --json carries the whole id |
DIR |
The session's working directory, ~ for $HOME |
SINCE |
How long the session has been in its state, in one unit (42s, 5m, 3h, 6d); - when its host does not say (an older craze) |
TITLE |
The session's title, else its first prompt (the session index's title), else -: one line, cut with … to what is left of the terminal's width, or to 100 cells when stdout is not a terminal |
Rows are ordered by state, in the order above, then newest in its state
first. On a terminal no line is wider than it: when fewer than 10 cells are
left for the title, DIR gives up cells first (down to 10), its paths cut
from the left so their ends stay, and a terminal too narrow even for the
other columns has every line cut. With nothing running it prints no
sessions running.
| Flag | Description |
|---|---|
--json |
Print the hub's roster (its sessions.list result, the protocol's roster rows) on one line instead of the table; with --saved, the sessions.saved result |
--no-hub |
Read each session's host directly, without the hub; with --saved, the session index |
--saved |
List the saved sessions instead — not running, ready to reopen — newest first (below) |
-C, --dir <dir> |
With --saved, only the sessions that ran in this directory (made absolute and cleaned, then matched exactly). Without --saved it is a usage error, exit 2 |
--no-hub — and any failure to have a hub (none could be started, or it did
not answer within 10 s) — reads each host itself, exactly as the hub would
(one round of the same poll; the same rows), and says so on stderr:
craze ps: --no-hub: reading each session's host directly
craze ps: the hub is not available (<reason>); reading each session's host directly
With --json, that roster's epoch is "" and its cursor 0: it is no
hub's. craze ps exits 0 unless that read fails too — the registry cannot be
read — which is exit 1 with the reason on one line.
Saved sessions¶
craze ps --saved lists this craze directory's saved sessions: those in
its session index that no running host
serves — what the session list's saved rows are,
chosen by the same rule — newest first, at most 200:
SESSION PROVIDER DIR AGE TITLE
4c1d7e90 cursor ~/proj-a 2h fix the flaky test
legacy01 grok ~/proj-b 3d -
| Column | |
|---|---|
SESSION |
The last eight characters of the session's craze id, as craze ps shows a running one; a row from a craze before craze ids shows its provider session id's last eight |
PROVIDER |
The session's provider |
DIR |
The directory it ran in, ~ for $HOME; cut from the left on a narrow terminal, as craze ps's is |
AGE |
How long since it was last active, in one unit (42s, 5m, 3h, 6d) |
TITLE |
Its title, else -: one line, cut with … to the terminal's width (100 cells when stdout is not a terminal) |
One row per session, its newest — the row a reopen loads — listed only
when its provider is one this craze can resume (a session whose newest row is
a newer craze's, of a provider this one does not know, is not listed at all)
and the row is well-formed (a hand-edited row whose craze id is not one a load
takes — [A-Za-z0-9._-], at most 128 bytes — is not:
bounds), and sub-agents' sessions are left out (the session list shows
them, tagged). A session whose host is stopping is listed at once. A session
whose directory has gone is still listed: nothing is stat'ed. With nothing
saved it prints no saved sessions; with more than 200 (the index holds up
to 500), stderr says craze ps: more saved sessions than these 200: only the
newest are listed.
It lists through a running hub's sessions.saved
and never starts one: the index is this craze directory's to read too. It
asks only a hub whose hello says reopen —
the capability that says a hub can list saved sessions and
reopen them. With no hub running, with --no-hub, or when
the hub cannot answer — one from an older craze, whose hello lacks
reopen — it reads the index itself, through the same function the hub
answers with, and says so on stderr:
craze ps: no hub is running; reading the session index directly
craze ps: --no-hub: reading the session index directly
craze ps: the hub cannot list saved sessions (an older craze's); reading the session index directly
craze ps: the hub is not available (<reason>); reading the session index directly
--json prints the sessions.saved result on one line — {"sessions":[…],
"more":…}, each session's id what reopens it — byte for byte the same
through the hub or from the index. An index that cannot be read lists
nothing, exit 0 — the hub's log says why, or stderr when craze ps reads the
index itself; a registry that cannot be read, or one holding a live host's
entry that cannot be read, is exit 1 (a running session could not be told
from a saved one).
The hub¶
The hub is one process per user and craze directory (CRAZE_HOME), craze
hub: it serves the roster of every running
session and a splice to any one of them, on a
socket of its own in the runtime namespace. Nothing needs starting by
hand: craze ps, craze new, craze bridge --hub and the
TUI's session list start it when none runs —
re-executed, in a session of its own, its stdio on /dev/null — and the next
one finds it. A hub that does not answer is replaced.
It exits by itself 60 seconds after the last host has gone and the last
client has disconnected — one nobody ever connected to included — and
while any session runs it stays. Killing it loses nothing: hosts run on
without it, and the next craze ps starts another. A hub that outlives an
upgrade which removed its craze (Linuxbrew) cannot start sessions: its
refusal names its pid to kill (for an older craze's hub, craze new adds
it), and it serves on until then (Upgrading while sessions
run).
Its log is ~/.cache/craze/host-logs/hub-<ns>.log, beside the
host logs (under the process's own $HOME;
<ns> names the craze directory), rotated once at 4 MiB like a host's. On
macOS it says, as the hub starts, which login session the hub runs in —
macOS login session: GUI (audit flags 0x2030), or not GUI for one first
started over ssh — because every session it creates inherits it
(craze new says why that matters).
craze new¶
./bin/craze new "fix the flaky test"
./bin/craze new -C ~/projects/lumen --provider grok --effort high "add the ps command"
./bin/craze new --title "ps command" "add the ps command"
./bin/craze new --json
./bin/craze new --load 4c1d7e90
Starts a session in the background — in its own host, through the hub's
session.create — in the current directory or
-C's, and returns once the session has started, its first prompt (the
arguments, joined by spaces) sent to it when there is one:
The id is the session's short id, as craze ps shows it. When
the prompt was sent and its answer lost, a second line says the prompt may
not have reached it: the session exists, and may be working on it. The
session runs on as any detached session does — craze attach, the session
list (←) and craze ps find it — until its host's idle exit.
| Flag | Description |
|---|---|
-C, --dir <dir> |
Start the session in this directory (default: the current one) |
--provider <id> |
The session's provider. Without it the hub's configured default — provider in config.toml, the one the last session to start persisted — and with none configured the create is refused (plain mode: craze new: no default provider yet; pass --provider cursor, grok, gx or native, exit 1; --json passes the hub's refusal through). An unknown id is a usage error, exit 2, plain even with --json |
--model <id> |
The model to start on. Without it the session uses the agent's own default: craze keeps no default model of its own for an ACP provider |
--effort <value> |
The effort to start at, where the model offers one (as the session flag) |
--fast / --no-fast |
The fast setting to start with, where the model offers one |
--no-force |
The session's agent asks for permission, and a client answers (the default is a plain launch's: --force's bypass — config.toml has no permission setting) |
--title <title> |
Name the session from its start, pinned as /rename pins a title: every row that describes it carries it from the first one — in craze ps, the session list and the hub's roster (a roster subscription opened before the create may first receive a connecting placeholder with no row yet, before the titled reachable row), and no title the agent or the first prompt would give it replaces it (a later /rename does). Normalized as /rename normalizes one, to exactly what the session stores and the index holds — escape sequences, control characters, the zero-width space, word joiner and byte-order mark, and bidirectional embeddings, overrides and isolates dropped (ZWJ and ZWNJ kept), invalid UTF-8 as U+FFFD, one line with whitespace collapsed, at most 120 characters, the ends trimmed (session.create lists it exactly); one that comes to nothing (--title "", blank, only such characters) is a usage error, exit 2. Needs a hub whose create takes a title (below). With --wait the host this starts is named the same way |
--json |
Print the hub's answer (session.create's result: the session as the roster lists it, and the prompt's outcome; with --load, session.reopen's) on one line |
--wait |
Start an unattended session without the hub, send it the prompt and wait for its turn's result (below) |
--load <id> |
Reopen a saved session through the hub instead of starting one (below): an id craze ps --saved prints, or a whole one |
--timeout, --max-message, --prompt-file, --remove-prompt-file,
--plan and --ask are --wait's alone: without it each is a usage error
(craze new: --plan needs --wait, exit 2).
The session's host finds its agent binary as a session the list starts for
another provider does — [agents] in config.toml, then PATH: a hub never
hands a host the launch's --agent-bin or CRAZE_AGENT_BIN. It persists its
provider as the next plain launch's default, as every new session does.
A refusal is the hub's own words on one line, exit 1 — a session whose start
failed (the session did not start: …, its host's error), a
directory that does not exist, no provider. When a start failed because the
agent exited with a non-zero status or a signal after craze sent it
initialize, the error ends with the agent's own last words: its last
non-blank stderr lines, from what craze had captured when the failed start
collected them — first waiting up to half a second for its copy of that stderr
to finish — sanitized and folded onto the line with /, at most 512 bytes,
and not redacted — acp: agent exited: exit status 1: Error: Your macOS
login keychain is locked. / Run security unlock-keychain and try again. for
cursor on a locked keychain. An agent that exits before craze sends
initialize, or exits 0, gets craze's own words only. Everything the agent
printed is in the host's log. So is a session that started and
refused its first prompt: it runs on, idle, and the line says why. A hub from
before craze new says this hub (craze <v>) cannot create sessions; it exits
once no session runs and no client is connected, or stop it now with kill
<pid> (no session ends): it is not replaced, and the next craze new after
it has gone starts this craze's. Likewise --title against a running hub from before
titles (its hello has no createTitle) is
this hub (craze <v>) cannot set a title at create; it exits once no session
runs and no client is connected, or stop it now with kill <pid> (no session
ends), exit 1 (with --json, that message as the error object's): nothing is sent
to it, and no session is created without its title — run it again once that
hub has exited or been stopped, or create without --title and /rename the session.
A hub whose craze an upgrade removed (Linuxbrew deletes the old version's
directory) cannot start the session's host, and says so: this hub's craze
(<path>) was replaced by an upgrade: stop this hub with kill <pid> (no session
ends) and run this again; it goes on serving until you do. A hub of another
craze version than this one that cannot start the host — an older hub says
only the session host cannot be started — gets the way out added after its
words, from its hello: ; the hub (pid <pid>) runs craze <v> and this is
craze <this>: if craze was upgraded, stop that hub with kill <pid> (no
session ends) and run this again. That addition is the stderr line's alone:
with --json the object is the hub's error as it sent it. See Upgrading
while sessions
run.
Each run creates under a request id of its own, and a connection to the hub that ends before the answer (the hub restarted mid-create) is tried once more under the same id, so the new hub answers the session the first one started rather than start a second.
--json failures. With --json, every failure other than a usage error
(exit 2, plain) also prints one JSON object on stdout before the exit; the
stderr line and exit 1 are as without --json. The one exception is stdout
itself failing: a result that cannot be written (stdout closed, a full disk)
exits 1 with the write's error on stderr, and stdout may hold nothing or part
of the object. A refusal by the hub is the
wire's error, whole, under one key, so a script reads .error.data.code,
.error.data.reason and .error.data.cause where it reads them on the wire:
{"error":{"code":-32000,"message":"the session did not start: acp: agent exited: exit status 1: Error: KEYCHAIN LOCKED / Run unlock and retry.","data":{"code":"not_accepting","reason":"start_failed","cause":"acp: agent exited: exit status 1: Error: KEYCHAIN LOCKED / Run unlock and retry."}}}
Any other failure (no hub to be had, a -C that is no directory, an answer
that does not decode) is the message alone:
A success has no error key, and a session that started but refused its first
prompt is still the success object (exit 1, as before). Check the exit code,
then .error.
On macOS a session the hub creates runs in the hub's login (security)
session, and a hub keeps the session of whichever command first started it
for as long as it runs — and it runs while any session does. The login
keychain, which cursor needs, is unlocked only in the GUI login session, so
start craze, or its hub, from a terminal in your Mac's login session — a
Roost tab, Terminal.app — for agents that need it. A hub first started over
ssh cannot start a cursor session: the start fails, and its error ends
with a hint saying so and naming the hub's pid, …; this session runs outside
your macOS login session (its launcher was started over ssh): if the agent
needs the login keychain, as cursor does, stop the hub with kill 4242 (no
session ends) and run this again from a terminal in your Mac's login session
(a Roost tab, Terminal.app). kill <hub pid> ends only the hub: every
session runs on, and the next craze new starts a hub where it runs. The
hint is added only when the agent exited, and it says "if": being outside the
login session does not prove the keychain is what stopped the agent.
So over ssh to a Mac, cursor needs the GUI login session: run craze from a
terminal on the Mac, or through shed-host-agent, not from the ssh login. A
craze or craze prompt run over ssh that starts cursor itself (an explicit
--provider cursor, or --continue/--resume of a cursor session) now names
the reason while it starts instead of sitting at starting… with nothing to
say. craze prompt writes one line on stderr and carries on:
craze prompt: cursor may not start here: this craze runs outside the macOS login session (over ssh), where cursor may not reach the login keychain; run craze from a terminal on the Mac
and the TUI draws the same words as a note in the transcript (cursor may not
start here: …). It is a warning, never a refusal: nothing is stopped, and a
cursor that can reach the keychain some other way starts as usual.
craze new --load¶
./bin/craze ps --saved
./bin/craze new --load 4c1d7e90
./bin/craze new --load 0190ab12-cd34-7ef0-8123-456789abcdef --json
Brings back a saved session — one whose host has gone (an
idle exit, a reboot, a closed terminal) — through the hub's
session.reopen, and returns once its start
is done:
The session is loaded in a host of its own (craze serve --load), in the
directory it ran in and with the provider it ran with, as the session list
resumes one. When a host serves the session already — another client
reopened it, or it never stopped — the hub joins that host instead, starts
nothing and says so:
Either way the session then runs as any detached session does: craze
attach, craze send, the session list and craze ps find it, and with
nobody attached its host idles out (within 5 minutes while it has had no new
prompt). It sends no prompt: craze send it one.
The id. It is resolved against this craze directory's session index —
running sessions included, so a reopen of one says already running — with
the short session ids' tiers, the first that matches
deciding:
- an exact id: a craze session id, a provider session id, or
<provider>:<providerSessionId>(theidcraze ps --saved --jsonprints for a row from a craze before craze ids); - a craze session id's end —
craze ps --saved'sSESSION— or, for a row with no craze id, its provider session id's end (whatSESSIONshows for one); - a craze session id's beginning.
A suffix or a prefix needs at least four characters. Two sessions in the
deciding tier are refused, exit 1, naming them (craze new: 2 saved sessions
match 1111: aaaa1111 (cursor, /w), bbbb1111 (grok, /v)); none is craze
new: no saved session <id> (craze ps --saved lists them), exit 1. The whole
id goes to the hub.
--json is the only other flag it takes: it prints session.reopen's
result — {"session": <roster row>, "joined": …} — on one line. Like plain
craze new, it never reads stdin. A prompt, and --prompt-file,
--provider, --model, --effort, --fast, --no-fast, --no-force,
--title, -C/--dir, --wait, --plan, --ask,
--timeout and --max-message, are usage errors, exit 2 (craze new: --load
does not take --provider: a reopened session keeps its own): a reopened
session keeps its own directory, provider and settings.
It waits up to 4 minutes for the hub's answer (the hub's own bound is
180 s). A refusal is the hub's words on one line, exit 1: a session whose
directory has gone (that session ran in <dir>, which is no longer a
directory), one whose start failed (the session did not start: …, its
host's error), one still held by a host that has not finished stopping
(that session is held by host <id> (pid <pid>), which has not finished
stopping; try again). With --json each failure is one JSON object, as
plain craze new's are. A connection to the hub that ends before the answer
is tried once more with the same id: a reopen is safe to send again, and ends
on whatever host serves the session then.
A running hub from before reopen (its hello has no
reopen) is this hub (craze <v>) cannot
reopen a saved session; it exits once no session runs and no client is
connected, or stop it now with kill <pid> (no session ends), exit 1: nothing
is sent to it. A spawn_failed from a hub of another craze version gets the
same upgrade remedy as plain craze new's.
craze new --wait¶
./bin/craze new --wait --provider native --model chatgpt/gpt-6.1-sol "fix the flaky test in stats/"
./bin/craze new --wait --timeout 9m --max-message 20000 --provider grok --prompt-file /tmp/task.md --remove-prompt-file
./bin/craze new --wait --plan --provider cursor --json "how would you split core.py?"
Hands a task to a new session and prints its result: what a coding agent runs
to delegate work to a model craze can drive. It is another path than plain
craze new's — no hub: the session's host is started here, as the TUI
starts its own, and runs unattended:
- Questions the agent asks are answered with their first option, as
craze promptanswers them, and a plan it presents ends the turn there: it is the result (plan), never accepted on the agent's behalf — so the agent cannot go on to carry it out unread (each is listed in the result,auto-answered:, a plan asreturned). Permissions are allowed under the default bypass; with--no-forcea permission request stays open for a person to answer (needs_input, exit 4). - No background work: a native session runs every sub-agent and every shell command in the foreground, so a turn is whole when it ends.
The command starts the host, sends it the prompt and follows the turn the
prompt became to its end, printing the result exactly as
craze wait prints one — the turn's last message and a footer,
or one JSON object with --json — and exiting with its status's code
(Waiting commands): 0 completed, 3 failed or cancelled, 4
needs_input, 5 running at --timeout. The session runs on once the turn has
ended, until its host's idle exit:
craze send gives it the next prompt, with the context of the
first; craze attach, craze ps and the session list find it like any
session.
The host is detached from the command: in a session of its own, so it
outlives the shell and the process group that ran it, and out of the
command's process tree — craze starts it through a short-lived process that
exits at once. So stopping the command — Ctrl+C, SIGTERM, or a tree kill
such as Claude Code's TaskStop, which signals every process descended from
the one it stops — never ends the host: the command cancels its own turn
(Signals, below) and the session goes on. The host is handed the
environment a session the hub creates is handed: none of CRAZE_PROVIDER,
CRAZE_AGENT_BIN, TMUX, HERDR_* or ROOST_*. So it finds its agent
binary in [agents] in config.toml, then on PATH, as such a session does.
Its session index rows carry unattended,
the settings it started with.
| Flag | Description |
|---|---|
-C, --dir <dir> |
The session's directory (default: the current one) |
--provider <id> |
The session's provider; without it provider in config.toml, and with none configured exit 1 (no default provider yet; pass --provider …) |
--model, --effort, --fast / --no-fast, --no-force |
As plain craze new's |
--title <title> |
As plain craze new's: the session's name from its start, pinned, normalized the same way (one that comes to nothing is a usage error). No hub is asked, so no hub's createTitle is needed: the host this starts is named directly |
--plan, --ask |
Start in plan or ask mode — for a native session, --ask refuses edits and the shell, and --plan refuses edits but the agent's own plan file and lets the shell through; a task that says "read-only" is a request, not a rule. The turn ends at the plan, on every provider: the result is completed with the plan (plan), the session still in plan mode. A native session accepts its own plan and stops; on cursor and grok craze accepts nothing — it answers the plan cancelled and ends the turn (its stop reason cancelled, the plan auto-answered returned) — since an ACP agent told its plan is accepted goes on to carry it out. A plain craze wait afterwards reports such a turn the same way while its host still holds the turn's events, and cancelled once it does not; craze wait --from <handle> — the handle craze send prints — recognises it whenever its replay is honoured |
--prompt-file <path> |
Read the prompt from this file, - for stdin (The prompt) |
--remove-prompt-file |
Delete --prompt-file's file once it has been read |
--timeout <duration> |
Stop waiting after this long, counted from the command's start — the host's start included: running, exit 5, with a handle. Before the prompt was sent, the session is stopped instead, exit 1, timed out before the prompt was sent |
--max-message <n> |
As craze wait's |
--json |
Print the result as one JSON object; a failure as {"error":{"message":…}} |
Signals. The prompt is the command's own, as a handle's turn is
craze wait --from's: Ctrl+C, SIGTERM or SIGHUP remove it while it is
queued (dropped) or cancel its turn by its id (cancelled), exit 3, within
2 seconds; a second signal exits at once. The signal is the command's alone —
the host, out of its process tree, never receives it — so the session runs on
for craze send. One that comes before the prompt
was sent — while the host starts, or the session does — stops the session the
command started: cancelled, exit 3, its error interrupted before the prompt
was sent; the session it started was stopped — or, when a second signal
already came, … was told to stop: the stop is sent, and the command does not
wait for the host to go. A prompt whose answer was lost, or cannot be read,
may have reached the session, which is then never stopped.
A provider that cannot start is exit 1 with the host's own words, and the session is stopped:
craze new: session 8327352e did not start: agent binary not found: /opt/cursor/agent; install it, or set [agents].cursor in ~/.craze/config.toml; the session it started was stopped
A prompt whose answer was lost with its connection is never sent again: exit
1, the prompt may not have reached it; craze wait <short> follows the
session.
craze send¶
./bin/craze send 8327352e "now add a test for it"
./bin/craze send 8327352e --wait --timeout 9m --max-message 20000 --prompt-file /tmp/next.md --remove-prompt-file
./bin/craze send 8327352e --json "and the docs"
Sends a prompt to a session — any id craze ps prints, or the
index's for one that has ended — queued behind any turn it is running, as a
prompt typed in its TUI is. Without --wait it prints where the prompt went
and the command that follows it, and exits 0:
queued q-2 (position 1)
next: craze wait 8327352e --from 0199c0de-1111-7000-8000-397e175bfe79.41.row-q-2
started turn-3 when the session was idle. position counts from the next
prompt to run; it is taken from the stream, or else from the session's queue
as it stands, and is left out — queued q-2 alone — when the prompt has
already left the queue by then. A turn whose start the stream did not show
before the command gave up looking prints next: craze wait <short>, with no
handle. With --json, one object: status (started or queued),
session, short, provider, turn or row and position (absent when
unknown), handle, attach.
With --wait it follows this prompt to its turn's end — not a turn ahead
of it in the queue — and prints its result as craze wait does, with the same
statuses, exit codes and signals as craze new --wait: a
signal removes the prompt while it waits in the queue (dropped) or cancels
its turn (cancelled), and one before the prompt was sent is cancelled
too — the session left as it was.
A session that has ended is loaded again. When no running session has
the id, it is looked up in the session index
— the newest row of each session, by the same short, prefix and whole-id
rules (a row written before craze ids existed is not addressable; a session of
a provider craze cannot load again has no row: no session <id>; several
match: exit 1, naming them) — and a host is started to load it, as craze new
--wait starts one. A session craze new --wait started is loaded unattended
again, with the mode, permission mode, model and effort it started with (its
row's unattended); a session of your own is loaded as the ordinary
interactive session --resume would give you, so craze send is also a way
to message a saved session. The same happens when a running session's host is
closing it — stopped, or idling out: craze waits, up to 10 seconds, for that
host to go (or to keep the session) and loads it in its own; a host that is
gone by the time craze dials it is looked past the same way. Two craze
sends loading one session at once end up in one host, each prompt its own
turn. A session whose directory is gone, or that its agent refuses to load,
is exit 1 with the host's own words. The host a reload starts is never
stopped by craze send, whatever becomes of its prompt — another craze send
may have found it and prompted it meanwhile — not even when craze send gives
up while that host is still starting (a signal, --timeout): it stays up like
any session's host (an interactive one idles out; an unattended one until
craze stop).
| Flag | Description |
|---|---|
--wait |
Wait for the prompt's turn to end and print its result |
--prompt-file <path> |
Read the prompt from this file, - for stdin (The prompt) |
--remove-prompt-file |
Delete --prompt-file's file once it has been read |
--timeout <duration> |
Give up after this long, counted from the command's start (a reload included). With --wait, running at its end, exit 5, with a handle |
--max-message <n> |
With --wait, as craze wait's (a usage error without it) |
--json |
One JSON object: the result with --wait, where the prompt went without it; a failure as {"error":{"message":…}} |
The prompt¶
craze new --wait and craze send take their prompt from exactly one
source:
--prompt-file <path>— a file, or stdin for-;- the words after the command's arguments, joined by spaces;
- stdin, when neither is given and stdin is not a terminal (
/dev/nullcounts as one).
Words and --prompt-file together are a usage error (exit 2), and so are no
prompt at all, a prompt that is empty once the whitespace around it is gone,
and one over 32 KiB — the most a queued prompt may be — each refused before
anything is started or sent. --remove-prompt-file deletes the file once it
has been read, whatever the prompt turns out to be (empty, too long), and
never a file it could not read; a file it cannot delete is said on stderr, and
the command goes on. The prompt is read under the command's --timeout and
its signals: a source that does not end — a pipe held open, a FIFO nobody
writes — ends the command, exit 1, timed out reading the prompt from stdin
or interrupted while reading the prompt from <path>, before anything is
started. Plain craze new keeps its own rule: its words, or no prompt — it
never reads stdin.
craze wait¶
./bin/craze wait 8327352e
./bin/craze wait 8327352e --timeout 9m --max-message 20000
./bin/craze wait 8327352e --from 0199c0de-1111-7000-8000-00000000a001.41.turn-3 --json
Follows one turn of a running session and prints its result. The id is any
form craze ps prints, resolved as
short session ids are; the session's whole id is what
goes on the wire.
craze wait <id>follows the turn the session is working on — one craze started, or one the agent runs on its own — from where it is when the command attaches, and with nothing running reports the last turn at once. A session that has had no turn isidle. Its result is partial: what happened before the attach was not seen, so its tool calls, files and time count from what the session still holds, and an idle session's last turn has no stop reason and nousage— the session's last usage report need not be that turn's (a turn withdrawn before it opened reports none). It never touches the session: a signal (Ctrl+C,SIGTERM,SIGHUP) exits 1, and the turn goes on.--from <handle>follows exactly the turn or queued prompt a handle names, from the handle's place in the session's stream, so a turn that has ended since is still reported: whole when the handle's place is before the turn began — a queued prompt's, or a turn's that its own command started — and partial when the handle was taken mid-turn (plaincraze wait's). Arunningorneeds_inputresult prints one (next:, or.handlewith--json). A handle is<incarnation>.<seq>.<target>— the session's host incarnation, a place in its stream, andturn-<n>,turn-<n>.row-<id>(a turn that came from a queued prompt),row-<id>(a queued prompt) orforeign-<id>(a turn the agent ran on its own, its id base64url-encoded) — letters, digits,.,_and-only, so it needs no quoting. A prompt that was queued keeps itsrow-<id>handle once its turn runs: followed from it, the wait follows the prompt into its turn — and on into its next run, should the agent's own turn refuse it and put it back. A turn met mid-way — plaincraze wait's, or one picked up again from where the session stands after a reconnect the host could not replay — is named by itsturn-<n>, since a handle names only what its place in the stream can follow; when that turn came from a queued prompt, the prompt is named beside it (turn-<n>.row-<id>), and a wait from the handle still follows the prompt into its next run should the turn be refused. A handle is ownership: a signal while--fromwaits cancels its own work and nothing else — a queued prompt is removed (dropped), its running turn cancelled by its id (cancelled, or whatever it ended as first) — within 2 seconds, or exit 1,cancel sent, not confirmed; a second signal exits at once, without waiting even for its detach from the session. A turn the agent runs on its own is never cancelled this way, nor is anything in a host that is not the one the turn ran in: a cancel that would land in a restarted host is not sent (exit 1,the session's host changed …).- While the command waits for the session to start (an attach to a
session still coming up), a signal ends it at once — exit 1,
interrupted before session <short> answered, the session untouched — as--timeout's end does (timed out before session <short> answered). - As the command exits it detaches from the session, waiting at most a few seconds for the host's answer (after a signal, what is left of its 2 seconds); a signal then ends the detach at once.
The flaky test raced the cache's eviction; I made eviction wait for readers.
---
completed · 27s · 5 tool calls · $0.04 · context 16.6k/272k
files: stats/core.py
session 175bfe79 · craze attach --session 175bfe79
The message is the turn's last assistant message — the reply after its
last tool call, thought or plan — and (no final message) when the turn wrote
none: never an earlier turn's. A plan the turn presented follows it, under
plan:. The footer's first line is the status, the turn's time and its tool
calls, then — for a native session — what the turn cost (unpriced when none
of it had a price; + when some did not) and how full the context is. Then,
each when there is one: error: (a failed turn), stopped: (a stop other than
end_turn), files: (the paths the turn's edits touched), one
auto-answered: line per question or plan craze's approval policy answered,
ask: and blocked by: (needs_input), the session, and next: — the exact
command to run next for a running or needs_input result.
| Status | Exit | |
|---|---|---|
completed |
0 | The turn ended end_turn, with no error |
idle |
0 | The session has had no turn |
failed |
3 | The turn ended with an error, or with a stop other than end_turn |
cancelled |
3 | The turn was cancelled |
dropped |
3 | The queued prompt was removed before it ran |
needs_input |
4 | An ask is open that nothing here answers — a permission under --no-force, a question or a plan on an interactive host — the turn's own, or, for a queued prompt, the one holding up the turn ahead of it (blocked by:). Answer it in craze attach, or craze cancel or craze stop the session; the session is left as it is |
running |
5 | --timeout passed; the turn goes on, and craze wait <id> --from <handle> follows it. Nothing looked at once --timeout has passed — an ask the turn opened among them — changes this answer |
| Flag | Description |
|---|---|
--from <handle> |
Follow exactly the turn or queued prompt the handle names, from its place in the stream; a signal then cancels it |
--timeout <duration> |
Stop waiting after this long, counted from the command's start (9m, 30s): running, exit 5, with a handle. Default: no limit |
--max-message <n> |
Print at most n characters of the message and the plan together, never more: the message first, then the plan with what is left — cut back to the end of its last whole line when one ends within that, and left out when nothing is left. When anything is cut or left out, the plain output ends its message and plan with [… cut: full result in <path>], and the whole result — the message, plan and footer, or the JSON — is written to ~/.cache/craze/results/<short>-<inc>-<turn>.md (0600), where <inc> is the last 8 characters of the incarnation the turn ran in. A reloaded session numbers its turns from turn-1 again, so this keeps each turn's file its own. If that file cannot be written, the output is cut all the same: the marker reads [… cut: the full result could not be saved: <reason>], stderr says so, and --json names no messageFile. Default: no limit |
--json |
Print the result as one JSON object on one line |
With --json, one object. These keys are always there: status, session
(the whole id), short, provider, model and attach; and with a turn or a
queued prompt, turn and/or row.
| Key | |
|---|---|
message, toolCalls, durationMs, files |
On completed, failed, cancelled and dropped, always: "", 0 and [] when there is none |
stopReason, error |
How the turn ended; stopReason is absent when unknown (a partial result for an idle session) |
plan |
The text of a plan the turn presented, accepted automatically or not |
usage |
The last usage report before the turn's end (native sessions), as the stream's usage section carries it: {contextTokens, contextWindow, turn, session}. A partial result settled from where the session stands carries only what was seen of the turn itself — none for an idle session's last turn |
autoAnswered |
[{kind, summary, choice}]: each question or plan craze's approval policy answered |
ask, blockedBy |
needs_input: the open ask, {id, kind, summary}, and for a queued prompt the turn holding it up |
handle |
running, needs_input, and a failure with one |
partial |
true when some of the turn's events were not seen |
messageCut |
true when the message was longer than 1 MiB: its end is kept |
messageFile |
--max-message's file, holding the whole result, when it cut or left out the message or the plan; message and plan are then what was kept |
A key that does not apply is absent, never null or false:
{"status":"completed","session":"0199c0de-1111-7000-8000-397e175bfe79","short":"175bfe79","provider":"native","model":"chatgpt/gpt-6.1-sol","turn":"turn-2","message":"The flaky test raced …","stopReason":"end_turn","toolCalls":5,"durationMs":27100,"files":["stats/core.py"],"usage":{"contextTokens":16590,"contextWindow":272000,"turn":{…},"session":{…}},"attach":"craze attach --session 175bfe79"}
A dropped connection is redialled and the stream picked up where it was; a
host that can no longer replay that far answers with where the session stands,
and the result is then partial. A failure is exit 1 with one line, and with
--json one {"error":{"message":…}} object on stdout first — handle beside
it when there is one — as craze new's failures are; a usage
error stays plain (exit 2):
craze wait: no session <id>
craze wait: the session has ended (craze send <short> <prompt> reloads it)
craze wait: interrupted before session <short> answered
craze wait: timed out before session <short> answered
craze wait: timed out before the turn could be followed; craze wait <short> --from <handle> follows it
craze wait: the session ended (craze send <short> <prompt> reloads it) (handle <handle>)
craze wait: the turn's ending was not observed (handle <handle>)
craze wait: the session's host changed before the cancel reached it; the turn's ending was not observed (handle <handle>)
craze wait: lost the connection to session <short>: <reason>; the turn runs on in its host; craze wait <short> --from <handle> follows it
craze wait: cancel sent, not confirmed; craze wait <short> --from <handle> follows it
has ended is an id no running session has, which the session index still
holds; with --from, the handle is named beside it, as it is with every
failure once there is one. ended is the session ending while the command
waited — a turn the session's close cut short included (its stop reason
closing is the session's end, not the turn failing) — and its handle still
names the turn. not observed is a turn whose end cannot be learnt: its host
restarted (turn ids start again in a new host, so nothing the new host says is
taken for the turn's), or the turn ended and others ran since, out of the
host's reach.
craze cancel¶
Cancels the turn the session is working on — whoever started it, a turn the
agent runs on its own included — and prints cancelled turn-3 (cancelled
alone when no turn of craze's ran: the agent's own turn, or an ask open between
turns). With nothing running it prints nothing to cancel, exit 0. The
session goes on, and so does its queue: the next queued prompt starts. The id
is resolved as craze wait's; a failure is exit 1 with one line.
craze stop¶
Ends the session — its turn cancelled, its queue cleared, its host gone — and
prints stopped 8327352e once its host has taken the stop. It stays in the
session index, to resume. A session a craze TUI hosts cannot be stopped from
here (exit 1): close it there. The id is resolved as craze wait's.
craze auth¶
Manages the API keys of the native provider's
model providers, and the ChatGPT plan's
sign-in. A key is stored as its provider's api_key in
native/providers.toml in the craze directory (~/.craze, or $CRAZE_HOME),
written at 0600; a provider's environment variable, when it holds a usable
key, is used before the stored one. No key is checked with its provider
when it is stored — a wrong one shows on first use — and for a key none of the
three commands makes a network request, but for one: a key stored for
Anthropic is followed by the account's model list, fetched with it
(below). What they do to the file is under
Keys.
The ChatGPT plan (chatgpt) takes no key: it is funded by signing in with
ChatGPT, so for it login signs in, logout
signs out, and list shows the sign-in. Those talk to OpenAI's sign-in
service, and nothing else does.
A provider is named by its id or its display name, in any case: fireworks,
Fireworks, "z.ai coding plan". The providers are the
catalog's and any of your own in
providers.toml (a directory whose models.toml says
catalog = false has only
its own). All three exit 1 when there is no craze directory (neither HOME
nor CRAZE_HOME is set). An argument too many, or a flag a command does not
take, is exit 2, and what was typed is not repeated back: it may be a key.
craze auth login¶
Stores a provider's key. On a terminal, craze asks for it with a prompt that
does not echo (Fireworks API key:) — after a numbered list of the providers
to pick from when none is named, the connected ones marked (connected). The
echo is off from before the first prompt is drawn until the key is read, so
nothing typed at either prompt shows, however quickly it comes: at the list,
only a number on it is written back, so a key pasted there by mistake is never
displayed. Ctrl-C at either prompt leaves the terminal's echo on. When stdin
is not a terminal, the key is stdin's first line (at most 8 KiB), so a script
can pipe it in. Surrounding whitespace is trimmed.
craze auth login fireworks # prompts; the key is not shown as you type
craze auth login # pick from the list, then the prompt
printf '%s\n' "$FIREWORKS_KEY" | craze auth login fireworks
Saved the Fireworks key in /home/you/.craze/native/providers.toml.
FIREWORKS_API_KEY is set in this environment; craze uses it before the stored key.
The second line only when that variable is set. Nothing is saved, and craze exits 1, when the key is empty, shorter than 8 bytes, or overlaps craze's redaction marker — the error names the rule, never the key.
craze auth login anthropic then fetches the Anthropic account's model
list (GET /v1/models, at most 30 seconds) with
the key that funds Anthropic — the one just saved, or ANTHROPIC_API_KEY when
that variable is set — so the next native session offers the account's models
from its start:
A list that cannot be fetched — a key Anthropic rejects, no network — is one
note: on stderr instead, naming the status and never the key; the key stays
saved, craze exits 0, and a native session fetches the list when it opens. No provider named
without a terminal, or a name that matches none, is exit 2; the name is not
repeated back, in case what was typed there was the key. Another provider's
stored key that cannot be used is kept as it is and named in a note on
stderr, so it can be replaced or removed next.
Signing in to the ChatGPT plan¶
craze auth login chatgpt — or ChatGPT plan picked from the list — signs in
with ChatGPT instead of asking for a key: no key is read, or asked for, for
the plan. craze prints an address to open in a browser, where you sign in to
ChatGPT and approve craze, and waits for the browser to come back to
http://127.0.0.1:1455/auth/callback, a listener craze runs for the length of
the sign-in. On a Linux desktop (DISPLAY or WAYLAND_DISPLAY set) or on
macOS, and not over SSH, it opens the address in your browser itself.
When the browser is on another machine, the page it is sent back to does not
load there: copy that page's whole address from the browser's address bar and
paste it at craze's Redirect address: prompt. Like the key prompt, it does
not echo — the terminal's echo is off from before the address is printed until
the sign-in ends — so a key pasted there by mistake is never displayed. An
address craze accepts is confirmed by where it leads alone, never its query
(the one-time code):
--no-browser opens no browser and runs no listener, so the pasted address is
the only way in: the way to sign in from an SSH session or a container. With
stdin not a terminal, each line of it is taken as a pasted address.
The address is printed plain, on one line — no box, no escape sequences — so
any terminal can select it whole, and one that finds links in text can open
it. It is the fallback for a terminal where /connect's box gets the address
to neither the clipboard nor a link (see the TUI
reference).
craze auth login chatgpt # opens the browser on a desktop, else prints the address
craze auth login chatgpt --no-browser # paste the redirect address from any machine
Sign in with ChatGPT to use your ChatGPT plan in craze. Open this address in a browser and approve craze:
https://auth.openai.com/api/accounts/authorize?client_id=…
craze is waiting for the browser to come back to http://127.0.0.1:1455/auth/callback.
If the browser is on another machine, that page will not load there: copy its whole address from the address bar and paste it here.
Redirect address:
Signed in to ChatGPT as you@example.com.
You're using your ChatGPT plan.
Eligible usage in this app uses your ChatGPT plan. Manage usage in your ChatGPT settings: https://chatgpt.com/settings/usage
ChatGPT plan models: chatgpt/gpt-6.1-sol, chatgpt/gpt-6-astra, chatgpt/gpt-6-sol, chatgpt/gpt-6-luna, chatgpt/gpt-5.6-sol, …
The two notice lines appear once, the first time an account signs in with plan usage; a later sign-in prints the account and the models alone. The models are the account's own list, fetched then and there (a native session fetches it again when it is a day old); a list that cannot be fetched is a note on stderr, and the sign-in stands. Signing in again reuses craze's registration with ChatGPT, so the browser asks only which account to use.
A pasted line that is not this sign-in's address is refused — never repeated
back — and craze goes on waiting: one that is not an address at all (a key,
say) is told the plan takes none, and the address of an earlier sign-in's
redirect is told That is the redirect of an earlier sign-in attempt. Use the
address shown above. When the browser comes back to craze's listener from a
different sign-in attempt — an earlier one's approval, say — craze says so,
once: The browser came back from a different sign-in attempt. Use the address
shown above. When craze could not listen at all, it says why before what to
paste: craze is not listening for the browser: another program is using
127.0.0.1:1455. With --no-browser, or when stdin is not a
terminal and no listener runs, the end of stdin with no address accepted is
exit 1. Ctrl-C cancels the sign-in and closes the listener, exit 130, and
changes nothing; it, SIGTERM and SIGHUP leave the terminal's echo on. Declining in the browser is exit 1, as is any error from
ChatGPT's sign-in service, which is named by its step, its HTTP status and its
OAuth error code — never a token, which craze never prints. A step that runs
out of time is named too: craze auth login: exchange: timed out after 30s.
An account that signs in without allowing craze to use its plan — the
permission ChatGPT asks for alongside the sign-in — is signed in with plan
usage off: craze keeps no tokens, its models cannot be used, and the next
craze auth login chatgpt asks ChatGPT to show the permission again:
Signed in to ChatGPT as you@example.com, but ChatGPT plan usage is off: the account did not allow craze to use its ChatGPT plan, so the plan's models cannot be used.
To turn it on, run craze auth login chatgpt again and allow ChatGPT plan usage when ChatGPT asks.
The sign-in log¶
Every sign-in — craze auth login chatgpt and the TUI's
/connect alike — is recorded in the craze directory's
native/logs/signin.log, one JSON line per thing that happened, from the
attempt's start to its one outcome and the model list fetched after it: the
place to look when a sign-in did not finish. The logs directory is 0700 and
the file 0600. craze rotates the file once, to signin.log.1 (replacing
the one before), when a line would take it past 1 MiB, so a log of craze's own
writing stays near 2 MiB across the two. A file something else grew past that
size is rotated whole when craze next writes, so the two can then hold more.
Several craze processes may write it at once; each line is appended whole, under a lock.
The log is value-free: a record holds the time, which surface wrote it
(cli or tui), the attempt's id — 8 random hex digits, the same on every
record of one attempt — the event, and the fields below: fixed words, HTTP
statuses, a port, counts and milliseconds. It never holds an address or its
query, a pasted line, an authorization code, a state, a nonce, a PKCE value,
the host id, a token, an email, the account's id, the client id, or any text a
server sent. craze checks every field as it writes it, and writes invalid for
a value outside its fixed set, so the log is safe to share when asking for
help.
{"time":"2026-10-03T12:34:56.789Z","surface":"cli","attempt":"0a1b2c3d","event":"begin","mode":"paste_only","reason":"port_busy","port":1455,"registration":"new","elapsed_ms":3}
{"time":"2026-10-03T12:35:26.789Z","surface":"cli","attempt":"0a1b2c3d","event":"paste_refused","refusal":"mismatch","part":"state","elapsed_ms":30000}
{"time":"2026-10-03T12:35:41.789Z","surface":"cli","attempt":"0a1b2c3d","event":"redirect_received","via":"paste","elapsed_ms":45000}
{"time":"2026-10-03T12:35:42.789Z","surface":"cli","attempt":"0a1b2c3d","event":"signed_in","usage":"plan","registration":"new","elapsed_ms":46000}
{"time":"2026-10-03T12:35:43.589Z","surface":"cli","attempt":"0a1b2c3d","event":"models_fetched","models":8,"client_version":"0.160.0","elapsed_ms":800}
| Event | What happened | Its fields |
|---|---|---|
begin |
A sign-in started. | mode: listening (craze waits on 127.0.0.1 for the browser) or paste_only. reason, when it does not listen on 1455: requested (--no-browser), port_busy (another program has the port), listen_failed. port. registration: new (craze registers with ChatGPT) or reused. |
browser_opened, browser_failed |
craze opened the address in a browser, or could not. | — |
address_copied |
A click on the address, or Ctrl+Y, in /connect copied it; whether a clipboard took it is not known. |
— |
listener_refused |
The listener refused a request on its callback path, the first of its kind in the attempt (the rest are counted). A request to any other path — a browser's favicon — is never recorded. | refusal: other_attempt (the browser came back from a different sign-in attempt), not_ours (another request without the sign-in's state), over (the sign-in already had its redirect). status: 400, or 410 for over. |
listener_refusals |
At the attempt's end, how many times one kind was refused, when it was more than once. | refusal, status, count. |
paste_refused |
A pasted line was refused. | refusal: too_long, not_address (an API key pasted out of habit, say), mismatch, over. part, of a mismatch: scheme, userinfo, host, port, path, or state (the redirect of an earlier attempt). |
redirect_received |
The browser's redirect arrived. | via: listener or paste. |
signed_in |
The sign-in finished. | usage: plan, or off (plan usage not allowed). registration. |
declined |
The sign-in was declined in the browser. | step authorize, code access_denied. |
failed |
The sign-in failed. | step: begin, authorize, redirect, exchange, discovery, jwks, id_token or install. class: refused (ChatGPT said no), timeout (the step ran out of time, or waited too long for another craze's lock), network, dns, tls, http_redirect, bad_reply, bad_redirect, id_token, filesystem, cancelled, signed_out, sign_in_again, plan_usage_off, usage_limited, config or other. For a refusal, status and code (an OAuth error code craze knows, or unrecognised); for an id_token, the check that failed (signature, iss, aud, exp, nonce, subject, …). |
cancelled |
The sign-in was stopped before it finished. | reason: signal (Ctrl-C, SIGTERM, SIGHUP), no_input (stdin ended), esc, dialog (the box closed another way, or the session was switched), shutdown (craze quit, or the session ended), context, done. |
models_fetched, models_empty, models_failed |
The plan's model list after a sign-in: fetched (models, how many), an empty reply that kept the list before, or the failure (step, status, code, class). |
client_version: the version craze asks for the list with. |
Every attempt has exactly one of signed_in, declined, failed and
cancelled. A record may also carry elapsed_ms and dropped. elapsed_ms is
the time since its attempt began on an attempt's own events (begin,
redirect_received, signed_in, failed and the rest), but on the model
events (models_fetched, models_empty, models_failed) it is how long that
fetch itself took, which is not measured from the attempt's start. dropped is how many records were
lost just before it because the log was busy. A log craze cannot keep — its
directory a symbolic link, writable by other users or another user's, or the
file not a plain file of its own — is one note: on stderr as the command
ends, and the sign-in goes on without it. A sign-in never waits on the log: it
is written beside the sign-in, and at the end craze waits at most a second for
what is still to be written; records it could not write in that second are
lost, and are the same one note:.
craze auth logout¶
Removes a provider's stored key, and only the key: whatever else its entry sets stays. Exit 0 whether or not there was one.
With nothing stored the first line is No stored Fireworks key.; the second
is there only while a variable still funds the provider. No provider named is
exit 2. craze auth logout anthropic keeps the account's model list: it holds
no key, and without one it funds nothing.
craze auth logout chatgpt signs out of the ChatGPT plan: craze asks ChatGPT
to revoke the sign-in (waiting at most 10 seconds) and deletes its tokens from
this machine whatever the answer, keeping its registration so the next sign-in
reuses it:
Signed out of ChatGPT: the sign-in is revoked, and its tokens are deleted from this machine.
An access token already issued may keep working for up to an hour, until it expires.
When ChatGPT does not confirm the revocation, the first line says remote
revocation not confirmed and that craze can be disconnected in ChatGPT's
settings, under Apps. Not signed in, it prints Not signed in to ChatGPT.
A key someone wrote for the plan in providers.toml, which it never uses, is
removed as well.
craze auth list¶
One row per provider, by display name: its name, its id, and how it is
connected — env <VAR>, stored key or not connected, in the order a
session tries them. The ChatGPT plan's row is its sign-in: signed in as
<email> · ChatGPT plan · renews automatically (its tokens are renewed as
they near expiry, with no action from you), plan usage disabled — run craze
auth login chatgpt, or not signed in. It is read from the registration and
the token file's presence; no token is read, and none is ever printed.
Anthropic anthropic env ANTHROPIC_API_KEY
ChatGPT plan chatgpt signed in as you@example.com · ChatGPT plan · renews automatically
Fireworks fireworks env FIREWORKS_API_KEY
Meta meta stored key
OpenRouter openrouter not connected
Z.AI Coding Plan zai-coding-plan not connected
Then notes, on stderr, each starting note:: a stored key that cannot be
used, an entry of yours that repeats what craze
ships (delete it to follow craze's
updates), the warnings a native session prints when it starts, and — when the
model table does not load, a broken models.toml say — why. The rows need
only providers.toml and the catalog, so they are listed either way. No key,
nor any part of one, is ever printed.
craze providers¶
./bin/craze providers
./bin/craze providers --json
./bin/craze providers --hub
./bin/craze providers --hub --json
Lists the agent providers craze can start a session of — cursor, grok, gx when
it is installed, and native — with each one's state on this machine and, for a
provider that is not ready, why and what to do about it. These are agent
providers, what --provider picks; craze auth
list lists the native provider's model providers and
their keys.
PROVIDER STATE REASON FIX
cursor unavailable cursor-agent not found on PATH install cursor-agent, or set [agents].cursor in /home/you/.craze/config.toml
grok ready - -
native needs setup no model provider has a key craze auth login (an API key, or "craze auth login chatgpt" for a ChatGPT plan)
| State | When |
|---|---|
ready |
It can start here |
needs setup |
Native, with no model whose provider has a key: none is stored or exported, or the ChatGPT plan is signed in with plan usage off. craze auth login connects one |
unavailable |
It cannot start here, and nothing in craze can fix that: the binary is missing, cursor is outside the Mac's login session, or native's model files cannot be read |
How each is judged:
- cursor, grok and gx are ready when their agent binary resolves the way a
session of theirs would find it (Agent binaries):
$CRAZE_AGENT_BINfor the provider a plaincrazewould start ($CRAZE_PROVIDER, elseproviderinconfig.toml, else cursor), then[agents].<provider>, then the provider's own name onPATH. A binary that is not found makes it unavailable, and the reason names where craze looked:cursor-agent not found on PATH, or the override and its path,[agents].cursor /opt/x/cursor-agent not found(the fix then says to point that override at an existing binary). gx is a fork few machines have, so when it is not found it is not listed at all. - cursor, on macOS, is also unavailable when this craze runs outside the
Mac's login session — started over
ssh— where cursor may not reach the login keychain (this craze runs outside the macOS login session (over ssh), …; fix:run craze from a terminal on the Mac). A missing binary is reported first. Elsewhere the session is not known, and nothing is marked for it. - native is ready when its model table loads and a model's provider has a
key — what a native session would start on. With none it needs setup.
providers.tomlormodels.tomlthat cannot be read makes it unavailable; the reason names the file and nothing from it, since the error can quote a pasted key's name, andcraze auth listshows the error. So does a machine with no home directory (no home directory for native's keys).
A reason and a fix are each one line of at most 200 cells. Nothing is cached:
each run reads config.toml, looks the binaries up and reads native's files
again, with no network request and no keychain access. The TUI's provider
dialog and the session list's /provider show the same states and refuse a
provider that cannot start when it is picked there (Provider
states); everywhere else the states only report: an
explicit --provider <id> (like --resume and --continue) starts a provider
whatever its state, with the same start error as ever when it cannot start.
$CRAZE_PROVIDER (like config.toml's provider) only chooses the provider
dialog's default, which the check gates like any other row.
| Flag | Description |
|---|---|
--json |
Print the providers on one line, as JSON, instead of the table |
--hub |
Ask the running hub instead: its own view, its default provider and its recent directories (below). It never starts a hub |
{"providers":[{"id":"cursor","label":"cursor","state":"unavailable","reason":"cursor-agent not found on PATH","fix":"install cursor-agent, or set [agents].cursor in /home/you/.craze/config.toml"},{"id":"grok","label":"grok","state":"ready"},{"id":"native","label":"native","state":"needs_setup","reason":"no model provider has a key","fix":"craze auth login (an API key, or \"craze auth login chatgpt\" for a ChatGPT plan)"}]}
Each entry has the provider's id (what --provider takes), its label (what
the TUI shows), its state — ready, needs_setup or unavailable — and a
reason and a fix, present exactly when the state is not ready: the
objects the hub's
sessions.createOptions answers with.
--hub¶
--hub asks the running per-machine hub — what craze new and a phone
create through — instead of answering from this process. The hub judges each
provider as a session it creates would start: its binary from [agents] in
the hub's config.toml, then the hub's PATH, never $CRAZE_AGENT_BIN;
native from the hub's craze directory; and, on macOS, cursor from the hub's
login session, so a hub first started over ssh shows cursor unavailable
even when this terminal is in the Mac's login session (the fix names the
hub's pid to stop). After the table come the provider a craze new without
--provider starts and the directories sessions last ran in, newest first:
PROVIDER STATE REASON FIX
cursor unavailable cursor-agent not found on PATH install cursor-agent, or set [agents].cursor in /home/you/.craze/config.toml
grok ready - -
native needs setup no model provider has a key craze auth login (an API key, or "craze auth login chatgpt" for a ChatGPT plan)
default: grok
recent: /home/you/projects/lumen
recent: /home/you/projects/shed
default: - is no default (none set, or one craze does not know) and
recent: - no directory. --hub --json prints the hub's
sessions.createOptions result as the hub wrote it: providers,
defaultProvider when there is one, and recentDirs (dir, usedAt).
--hub finds a running hub and never starts one — over ssh, a started hub
would be the very ssh-born hub it is asking about. With none running it says
no hub is running and exits 1 (craze ps starts one). A hub from an older
craze, which cannot answer, is reported (this hub (craze …) cannot list what
it can create; it exits once no session runs and no client is connected, or
stop it now with kill <pid> (no session ends)), exit 1, and left running.
craze providers exits 0 whenever it answered, whatever the states say; 1 on
an error, --hub with no hub to ask included; and 2 for an argument or a
flag it does not take.
craze version¶
Print the craze version and exit.
craze completion¶
Prints a shell completion script for craze, generated by cobra, the command
library craze is built on. Load it the way your shell loads any completion
script; craze completion <shell> --help says how for that shell. craze help
[command] is the same text as craze [command] --help.
Exit codes¶
| Code | When |
|---|---|
| 0 | Session started; quitting after a usable session, including an error during the session; also Esc on the --resume picker (no session was ever started) |
| 1 | Session never started (no login, agent would not come up — a binary that is missing says agent binary not found: cursor-agent is not on PATH (also tried agent); install it, or set [agents].cursor in <config> — or a load the agent refused), a runtime failure, or nothing to --continue/--resume — no matching session, or a session index craze could not read |
| 2 | Usage error: an unknown or malformed flag, a stray argument, an unknown command or provider or theme, a flag value or combination craze refuses (--ask with --plan, --continue with --resume), missing prompt text, or a non-tty TUI. The non-tty refusal is checked before --continue/--resume are considered. craze bridge is the exception: it exits 1 for every failure, usage errors included |
A usage error never echoes what was typed (a mistyped word may be a pasted key): craze: unknown command; see craze --help (with (did you mean "ps"?) when a command name is close), craze ps: takes no arguments; see craze ps --help, craze prompt: unknown or malformed flag; see craze prompt --help. A flag with an invalid value keeps its own message.
craze frame uses 2 for a bad key script and 3 for a wait timeout; that
command is hidden. See Testing.
Waiting commands¶
craze wait, craze new --wait and
craze send --wait exit with their result's status, so a
script can branch on the code alone:
| Code | Status | When |
|---|---|---|
| 0 | completed, idle |
The turn ended end_turn with no error; wait on a session that has had no turn |
| 1 | — | craze failed: no such session, the session has ended, unreachable, a lost connection or an ending not observed (with the handle), the session ending while it waited, a cancel not confirmed after a signal; for new --wait and send, a session that could not start or load, --timeout before the prompt was sent, and a prompt whose answer was lost (the prompt may not have reached it, never sent again) |
| 2 | — | Usage error — for new --wait and send, no prompt, two prompts, an empty one or one over 32 KiB too |
| 3 | failed, cancelled, dropped |
The turn ended without completing; the queued prompt was removed before it ran; a new --wait or send --wait interrupted before its prompt was sent |
| 4 | needs_input |
An ask is open that nothing here answers |
| 5 | running |
--timeout passed; the turn goes on (craze wait <id> --from <handle> follows it) |
craze cancel and craze stop exit 0 when they did what they say —
nothing to cancel included — 1 on a failure, and 2 for a usage error.