Configuration¶
craze persists the theme and the last successfully started provider in
~/.craze/config.toml, and, once a session has a prompt or a title, a row
about it in the session index (~/.craze/sessions.jsonl) that
--continue, --resume and /rename use — see Session
index. Every session also writes a journal
of what happened in it, which can contain secrets and can be turned off.
config.toml also holds the switches journal, control_socket, detach,
host_idle_exit, terminal_title, background and host_status, the
[agents] table, and the
[compat.claude] table that says which of
Claude's own files a native session reads. Session flags (--workspace, --model, --effort, --fast / --no-fast,
--force, --ask / --plan, --agent-bin) are per-invocation; the agent
binary each ACP provider runs can also be set once, in [agents].
Theme precedence¶
An explicit --theme beats the config file, which beats craze-dark.
| Source | When it wins |
|---|---|
--theme <name> |
Flag was passed (including --theme "") |
~/.craze/config.toml |
No --theme on the command line, and the file names a theme |
craze-dark |
Neither of the above |
Presets¶
Seven presets. While craze runs it sets your terminal's own default
background and text colours to the theme's and restores them when it exits;
background = false or --no-background leaves them alone — see Terminal
colours.
| Name | Notes |
|---|---|
craze-dark |
Default |
craze-light |
|
tokyo-night |
Aliases: tokyonight, tokyo |
dark |
|
light |
|
catppuccin |
|
gruvbox |
craze and crazedark are aliases for craze-dark; crazelight is an alias
for craze-light.
Ctrl+G or /theme opens a names-only picker that repaints the live screen as
the cursor moves; Enter keeps the theme and saves it, Esc puts back the one
that was active when the picker opened. /theme <name> sets and saves one
directly.
The craze directory¶
craze keeps its own files in one directory, ~/.craze by default, under
fixed names:
| File | Holds |
|---|---|
config.toml |
The saved theme, provider, and other settings — see Config file |
sessions.jsonl |
The session index |
journal/ |
One directory per workspace of session journals |
attachments/ |
The images pasted into the composer — see Attachments |
native/ |
The native provider's files: providers.toml (your API keys), models.toml, recent.json, auth/chatgpt.json (the ChatGPT plan's tokens), the ChatGPT and Anthropic accounts' model lists, the sign-in log and sessions/ (native session transcripts) — see Native models and providers. Made on first use |
CRAZE_HOME moves the whole directory. With CRAZE_HOME=/some/dir, craze
reads and writes /some/dir/config.toml, /some/dir/sessions.jsonl and
/some/dir/journal/ and nothing under ~/.craze. A relative value stays
relative to the working directory craze runs in, and a leading ~ or ~/
means your home directory. No variable moves a single file on its own.
CRAZE_HOME moves only craze's own directory. The user-level skills and the
plugin caches craze scans (see Slash commands) are
still found under HOME, so a test or a container that wants those isolated
as well sets HOME too.
Earlier builds read CRAZE_CONFIG, which named the config file. It has
been removed, not kept as an alias: while it is still set, craze,
craze prompt, and craze frame exit with status 2 and a message that names
it, before reading or writing anything. Unset it and set CRAZE_HOME to the
directory the file was in — CRAZE_HOME=/some/dir for /some/dir/config.toml.
A config file with any other name has to be renamed config.toml.
Attachments¶
An image pasted into the TUI's composer (see Images) is
processed and stored in attachments/ inside the craze
directory: ~/.craze/attachments/, or
$CRAZE_HOME/attachments/. The directory is created 0700 and holds only
files named <16 hex digits>.<png|jpg|webp>, mode 0600, written atomically.
The name is the start of the content's SHA-256, so pasting the same image twice
stores one file. craze refuses to read from a directory that is a symlink, is
owned by another user, or has any mode other than exactly 0700, and an attachment path in a
message that points anywhere else is sent to the agent as text, not read.
Every TUI start sweeps the directory, quietly: files older than seven days are
deleted, and then the oldest files go until at most 500 MiB is left.
Deleting the directory by hand is always safe; an image that has gone by the
time the message is sent goes as text saying no longer available. What the
agent receives is described under what the agent
receives.
Config file¶
The saved theme lives in config.toml in the craze
directory, ~/.craze/config.toml by default.
craze rewrites the file only when a setting it saves (the theme or the provider) changes; a start that leaves both as they are writes nothing. When it does write, comments and layout are not kept; every key is, including keys craze does not know. A file craze cannot parse is never clobbered, and the save that refused to overwrite it is reported in the transcript. A malformed file does not prevent startup; the theme falls back to the default.
craze frame never reads this file, and never reads $CRAZE_PROVIDER. See
Testing.
Claude content: [compat.claude]¶
A native session reads the instruction, rule, skill, command, plugin and
sub-agent persona files in Claude Code's layout (AGENTS.md and CLAUDE.md,
.claude/rules, .claude/skills, .claude/commands, .claude/agents, and
the Claude-enabled plugins), so a repository set up for Claude Code behaves the
same here. The [compat.claude] table in config.toml
turns each class off:
[compat.claude]
instructions = true # AGENTS.md, CLAUDE.md and their companions, rules included
rules = true # .claude/rules, ~/.claude/rules
skills = true # this project's and your own skills
commands = true # this project's and your own commands
plugins = true # what Claude-enabled plugins ship: skills, commands, personas
agents = true # sub-agent personas, from every source
Every key is a bool and defaults to true: a key that is absent, whether the
whole table is missing or only that key, leaves its class on, and so does a
config.toml craze cannot parse. A key written as false turns its class off
even in a table that leaves the others out (rules = false alone turns off
rules and nothing else). false stops a native session reading that class. instructions = false removes the
whole instructions section of the prompt, rules included, where rules =
false removes only the rule files. skills and commands govern this
project's and your own entries, a plugin's being plugins' alone. agents =
false removes the personas from all three of their sources, a plugin's
included.
A value that is not a bool leaves the class on and adds one line to the
session's diagnostics: config.toml compat.claude.<key> is not a bool; leaving
it on. These keys fail open, unlike journal and control_socket: a typo
must not strip a repository's instructions out of a session. They affect the
native provider only; cursor and grok read their own files. See What the
model is told and Slash
commands for what the classes contribute.
Session index¶
~/.craze/sessions.jsonl is the on-disk index behind --continue,
--resume and /rename (see CLI and
TUI). It is always the sibling of the config
file in the craze directory, so CRAZE_HOME carries
the index along with the config, and craze frame, which isolates HOME and
unsets CRAZE_HOME for its run, isolates it too. There is no separate
environment variable for it.
One JSON object per line:
{"sessionId":"…","provider":"grok","cwd":"/abs/workspace","title":"fix flaky pty test","pinned":false,"crazeId":"…","createdAt":"2026-09-15T22:04:11.5Z","updatedAt":"2026-09-16T00:12:03.2Z"}
-
sessionIdis the provider's id for the session, and it changes with every load into a new agent session.crazeIdis craze's own durable id: the engine mints it and carries it back in on every load, so it survives a load and a host restart. A row an older craze wrote has none and gains one on its next write; a newer craze's row is read by an older one, which keeps the key. -
cwdis matched by exact string against the workspace's absolute path — no symlink resolution — so a session started from one path is not offered under another that resolves to the same directory, cursor included even though its ownsession/loadis permissive across directories. titleis the session's stored name: the agent's own title, the first line of the first prompt as a fallback, or a/rename— or the title the session was created with (craze new --title, asession.createtitle), written as a/rename's is.pinnedmarks a/rename(or that created title), which always wins over a later agent title.- A row exists only once there is something to show it for: craze creates it on the session's first prompt or its first agent-supplied title, whichever comes first — never merely on start, unless the session was created with a title, whose row is written as soon as the session has started — so a session started and quit without a prompt or a title leaves nothing behind.
- The index is capped at 500 rows; a write past the cap drops the oldest
rows by
updatedAt. - An unknown provider id already in the file (written by a newer craze) is
kept on disk but never offered by
--continueor--resume. craze promptnever writes to the index — headless sessions are not resumable.-
unattendedmarks a session craze started for no one to attend —craze new --wait's, the way a coding agent hands craze a task — with the settings it started with, whichcraze sendpasses again when it loads the session after it has ended:{"sessionId":"…","provider":"cursor","cwd":"/abs/workspace","title":"fix the test","pinned":false,"crazeId":"…","unattended":{"mode":"plan","permissionMode":"bypass","model":"composer-2.5","effort":""},"createdAt":"…","updatedAt":"…"}modeis"",planorask;permissionModebypass(the default) orprompt(--no-force);modelandeffortas they were given,""when they were not. The key is there only on such a session's rows. It is never removed or changed once written, and a row of the same session written later — loaded into a new agent session — carries it too. It records how the session started, not how it runs next: opening it from--resumeor the session list, orcraze sendto a session of your own, runs an ordinary interactive session.A marked row is a sub-agent's session:
--continue(andcraze serve -c) skips it and loads the newest session that is not one, the--resumepicker lists such rows after the others, the picker and the session list tag themsub-agent, and when the index reaches its 500-row cap they are dropped before any other row (the oldest first).
A --continue or --resume never changes the persisted default
provider. Loading yesterday's grok thread is not a decision about
tomorrow's default: provider in config.toml is only touched when
--provider was passed explicitly alongside --continue/--resume, the
same as an ordinary run.
Session journal¶
Every session craze starts writes a journal of what happened in it, one JSON object per line, under the craze directory:
The directory below journal/ is the workspace's own path with its separators
turned into dashes, wrapped in --…-- — the same name the native harness gives
that workspace's session store, so the two pair by eye. The file name is the
UTC time the session was built and its incarnation id, minted once per run of
a session, which the first line of the file repeats as incarnation. It is not
the craze session id that craze ps shows; that id is in the file's
craze_session diag line.
One file per session, and no process ever appends to a file another process
wrote. A craze prompt run leaves one; a TUI run leaves one per session it
starts. --continue and --resume start a new session, so they get a new
file, into which the agent's replay of the earlier conversation is written
again, flagged as replay; the earlier file is never touched. The file is
created with the first line worth writing, so a craze that is opened and quit
without ever starting a session leaves nothing behind. craze frame never
journals.
| Line | Holds |
|---|---|
header |
Format and codec versions, the incarnation id, the craze version, OS and architecture, the provider, the agent binary asked for, the workspace, force, interactive, mode, and the pid |
session |
The provider's own session id, the id it was loaded from on a resume, and the binary craze actually spawned |
event |
One line per event the session emitted, with its seq and the whole event — the same numbers craze prompt --json prints (see CLI) |
prompt / prompt_end |
Your prompt or interjection as typed, before craze expanded any slash command into it; then how that turn ended — stop reason, or error class and message, and its duration |
diag |
Everything that is not transcript: the agent's own stderr, a start that failed, a start setting (--effort, --fast) that could not be applied, an event too large to record, a reader craze dropped, and the line that closes the file |
gap |
Written when the journal could not keep up: exactly what was lost, so a reader is never quietly short of events |
A journal never slows a turn down and never fails a session. If it falls
behind, it records what it lost as a gap rather than skipping it quietly; if
a write fails outright — a full disk — journaling stops for that session and
craze says so once on stderr. The session itself carries on either way.
Journals can contain secrets¶
The journal itself adds no redaction: it records what the session admitted.
Native already replaces the provider keys it knows with
[craze:redacted-credential] — in a ! command's output before the prompt is
recorded, and in its tools' results — but any other secret can be on disk in
plain text: a token you type, a credential craze does not know in a shell
command's output, a file the agent read, an error message. That is the point of the record, and it is why the files are
0600 in 0700 directories, both no broader than: a restrictive umask
narrows them further, and a directory that already exists is used as it is,
never tightened. Treat a journal as being as sensitive as the session it came
from.
craze never uploads a journal, or any part of one, anywhere. They are local files. A running session reads its own journal back — that is how a client that reconnects is given the events it missed — but craze does not read a file from an earlier run: once a session ends, its journal is only there for you.
craze does not prune them yet either: they accumulate until you delete them. As a sense of scale, a measured turn with one tool call cost 6 KB against cursor and about 50 KB against grok, which streams its reply token by token; a fifty-second cursor turn that read dozens of files cost 224 KB.
Turning it off¶
journal = false in config.toml, or CRAZE_JOURNAL=0 (or false) for one
run, and craze writes no journal and creates no journal directory. Either
switch turns it off on its own, and neither can turn it on against the other:
CRAZE_JOURNAL=1 does not defeat journal = false.
The opt-out fails closed, as control_socket and detach do, unlike
terminal_title, background and host_status, which fail open. A
journal holds prompts and tool output, so anything craze cannot read as a
plain true means off, not on: a config.toml it cannot read or parse, a
journal key that is not a bool (journal = "false" is a string), and a
CRAZE_JOURNAL it cannot read as a bool. Each of those prints one line saying
so; an explicit false is your own choice and is silent. Journaling is on only
when the config parsed and journal is absent or exactly true.
The control socket¶
Every host binds a Unix-domain control socket and serves its session over it
— what craze bridge and craze
attach dial into. The host is the detached craze
serve the ordinary craze spawns (see Detached
hosts), or, with detaching off, the TUI's own process. See the
protocol reference for the runtime namespace,
the registry, and where the socket itself lives; the registry and its locks
sit under the bridge process's own $HOME, not CRAZE_HOME — an SSH login
is assumed to share that $HOME with the tab that started the host,
true for an ordinary SSH login as the same user, whatever the tab had set
for the other variables. Binding
happens only once a run is actually starting a host or a TUI: a --continue
refused by SQ16 binds
nothing.
control_socket = false in config.toml, or CRAZE_CONTROL_SOCKET=0 (or
false) for one run, turns it off; a craze with no socket serves nothing for
another process to reach, though the session's own SQ16 lock is still taken
either way — it does not depend on the socket.
The opt-out fails closed, the journal's own rule and
for the same reason: this is an access switch, so anything craze cannot read
as a plain true means off. CRAZE_CONTROL_SOCKET, when set, decides
alone: an unreadable-as-bool value turns the socket off with its own one
line on stderr, and config.toml is then not even read, so at most that one
line prints. Only when CRAZE_CONTROL_SOCKET is unset (or reads as true)
does config.toml get read at all, and there the same rule applies — a
config.toml it cannot read or parse, or a control_socket key that is not
a bool, prints its own one line. An explicit false, from either switch, is
your own choice and is silent. The socket is served only when the config
parsed and control_socket is absent or exactly true, and
CRAZE_CONTROL_SOCKET (when set) reads as true — either switch turning it
off is final; neither can turn it back on against the other.
A failure to bind at all — the runtime directory is unusable, or the
socket's own path would be too long for sun_path — is not the opt-out and
is not a privacy switch: it prints one line, craze: control socket off:
<why>, embedding the underlying error's own text as is — in the ordinary
case that is one stderr line, but nothing here promises the error text
itself is free of a line break (a runtime path containing one, say). On the
in-process path (CRAZE_DETACH=0, detach = false) the run carries on
exactly as it would with the opt-out set. A detached
session cannot: its host exists to serve that socket, so craze serve fails
to start and the TUI shows the failure (craze: the session host could not
start: …; CRAZE_DETACH=0 runs sessions inside craze instead) and exits 1 when
dismissed. CRAZE_RUNTIME_DIR (below) is the fix when the reason is length;
CRAZE_DETACH=0 runs the session inside craze meanwhile.
Detached hosts¶
By default the ordinary craze runs the session in a detached host — craze
serve, in a session of its own — and the TUI is that
host's client, so the session outlives the
terminal. Three switches turn that
off and run the session inside the TUI's own process, as craze did before:
detach = falseinconfig.toml.CRAZE_DETACH=0(orfalse) for one run. It cannot turn detaching on againstdetach = false.control_socket = false, orCRAZE_CONTROL_SOCKET=0: a detached host is reachable only through its socket, so with the socket off detaching is off too, silently (the control socket's own switches speak for it).
The opt-out fails closed onto the in-process path, like the other access
switches on this page: anything craze cannot read as a plain true means
off — a config.toml it cannot read or parse, a detach key that is not a
bool, a CRAZE_DETACH that is not a bool. Each prints one line
(craze: detached sessions off: <why>); an explicit false is your own
choice and is silent. CRAZE_DETACH is read trimmed, and empty counts as
unset.
With detaching off, closing the terminal ends the session, and there is no agent view of it beyond the run's own control socket.
Host idle exit¶
host_idle_exit in config.toml says how long a detached host may sit idle
before it exits on its own, saving the session like any stop (it stays
resumable with craze -c):
host_idle_exit = "1h" # the default
host_idle_exit = "20m" # any Go duration: "90m", "2h30m", "45s"
host_idle_exit = "0" # exit as soon as the host is eligible
host_idle_exit = "never" # any case; never exit for being idle
The value is a string. An absent key is "1h", silently; a file that cannot
be read or parsed, a value that is not a string, or one that is neither a
duration nor never (or is negative) is also "1h", with one line in the
host's log. The host reads it once, as it starts.
Idle means both: no client attached, and nothing in flight. A client is
one attached to the session (a craze attach, a TUI, a bridge); the session
list's polling does not count, nor does a peer that has half-closed its
connection. Something in flight is a turn, a queued prompt or setting, a
send-now armed, an open ask (a permission or a question: an ask pins its
host until someone answers it or the session is stopped), a sub-agent or a
background child running, or a session still starting or replaying. Any of
those, at any one-second look, restarts the clock, as does a turn that
ended since the last look. The stop is decided atomically: the host refuses
new work while it counts, so a client that attaches at that instant is either
counted or refused, never half-attached.
Three cases have their own limit:
- A startup grace. The clock does not start until a client has attached or 60 seconds have passed since the host started, so a host spawned for a client that has not dialled yet is not reaped first.
- Never prompted: five minutes at most. A session that has had no prompt
has nothing to resume, so its limit is the smaller of
host_idle_exitand five minutes — a loaded session nobody has prompted included. - A failed start: none. A host whose session could not start (an auth
failure, say) stays only long enough for its launcher to attach and be told
why; its limit is 0, even under
"never".
Independently of the limit, a host whose socket file or registry entry has
vanished (/run/user removed at the last logout, a registry swept by hand)
stops at once, the ordinary way: it can no longer be reached, and stopping
saves the session.
Host logs¶
A detached host writes its own diagnostics, its crash output and its agent's
stderr to ~/.cache/craze/host-logs/<hostId>.log (under the process's own
$HOME, beside the registry — not CRAZE_HOME).
The launcher names it whenever a host could not start. A spawned host's
--log is always that file; craze serve run by hand logs to stderr unless
--log says otherwise.
- The log is rotated once: at 4 MiB the current file becomes
<hostId>.log.1(replacing an older one), and a new<hostId>.logstarts. - Beside it,
<hostId>.pgidsrecords the process groups of the host's agents while it runs, for its launcher's last-resort cleanup; it is removed when the host stops cleanly. - Every host sweeps the directory as it starts: the files of a host that is gone (decided by its own host lock, never by its registry entry) and that nobody has touched for 7 days are removed. A live host's files are never removed, whatever their age.
- The directory is
0700, and a--loginside it must be named for the host that opens it.
A host's log can hold what its agent printed to stderr; treat it like the journal.
A start that fails because its agent exited — with a non-zero status or a
signal, after craze sent it initialize — also carries the agent's last
non-blank stderr lines in its error, from the stderr craze had captured when
the failed start collected it — first waiting up to half a second for its copy
of that stderr to finish — sanitized, folded onto one line with /, at most
512 bytes (a longer last line keeps its end). They go wherever a
detached host's start failure goes: a client attached to it,
session.create's data.cause, craze new's line. An agent that exits
before craze sends initialize, or exits 0, gets craze's own error only. The
lines are not redacted: they are the end of what the host's log holds, and
an agent's stderr can hold a token, so only that tail leaves the log. This is
the detached host's alone: the in-process TUI (detach = false) and craze
prompt say craze's own error only, so the two TUIs word the same failed start
differently.
On macOS a host runs in the login session of whoever started it: a detached
TUI's host in its terminal's, a host the hub created in the hub's — and a hub
keeps the session of whichever command first started it, while any session
runs. The login keychain, which cursor needs, is unlocked only in the GUI
login session, so start craze, and its hub, from a terminal in your Mac's
login session (a Roost tab, Terminal.app). Over ssh to a Mac that means
running craze from a terminal on the Mac, or through shed-host-agent, not
from the ssh login. craze now names the reason while it starts: a craze or
craze prompt that starts cursor itself from such a login says 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 (a note in the TUI, a line on stderr for craze prompt) instead of
sitting at starting…. A host outside it whose agent
exited that way ends its start error with a hint saying so: for a host the hub created
it names the hub to stop (kill <hub pid> ends only the hub; no session
ends), and for one a TUI launched it says to start the session from the login
session's terminal.
Model catalog cache¶
cursor, grok and gx name the models they offer only once a session has
started. So that the session list's /model can
offer them before one has, every detached host records the model catalog its
agent installs — at session/new, when a loaded session's replay ends, and
whenever the agent changes it — in ~/.cache/craze/catalogs/<provider>.json
(under the process's own $HOME, beside the host logs — not CRAZE_HOME: the
catalog is your account's, whichever craze directory is in use). Native's
models are its model table and are never cached.
{"version":1,"observedAt":"2026-09-30T12:00:00Z","provider":"cursor",
"models":[{"id":"composer-2.5","name":"Composer 2.5"}, …]}
- Each provider's file keeps the newest observation: a host takes
<provider>.lock(anflock), reads the file'sobservedAt, and replaces the file — atomically — only when its own observation is newer, so of two hosts writing at once the older can never win by finishing last. A host writes only when the catalog differs from the one it last recorded, and never a catalog with a model the file cannot hold (no id, a name or id that is not one line): the whole observation is left out — the file keeps what it had — and the host's log names the model, rather than recording the rest as though it were the whole catalog. - A file larger than 256 KiB, or one craze did not write (another version, another provider's, no models, text that is not one line), is ignored as if there were none, and the next write replaces it.
- The directory is
0700and validated like the rest of~/.cache/craze; a failed write is one line in the host's log and never stops the session. - It is only a list of choices, labelled with its age: deleting the directory is safe, and the next session of each provider writes it again.
Terminal tab title¶
terminal_title = false in config.toml turns off every terminal tab-title
write (see Tab title); the default — including when the
key is absent or the file cannot be parsed — is true. craze prompt never
sets a tab title, and craze frame has no terminal to write one to.
Host status¶
Inside a herdr pane or a roost tab, craze reports its own state to that host,
which then tracks craze like any other agent. That is all craze sends: its
state, a one-line reason (a card's header or an error's first line), and the
provider and model, never conversation content. A --continue or --resume
replay is not reported — the host first hears from craze when the restored
session is ready. The end of a turn reaches the host a quarter of a second after
the turn ends, so a message that drains from the queue the moment a turn ends
never shows as a finished turn in between.
Every exit craze can see — /exit, Ctrl+D, Ctrl+C, SIGTERM, and SIGHUP,
the terminal hangup a closed tab or window sends — releases the pane or tab
before the agent is shut down, waiting at most a second for the host to take
it, so an agent slow to exit cannot hold it. SIGKILL gives craze no
chance to, and leaves its last state in place until the pane or tab closes.
After the TUI exits, craze prints its own lines on stderr: a socket it could
not reach costs one host status: herdr: … or host status: roost: … line.
It prints the agent's own diagnostics too, but only when the run failed —
the session never started, the TUI run itself failed (a recovered panic
included), or the agent exited on its own. On a clean exit craze prints
nothing but its own lines.
herdr¶
The pane's sidebar, herdr agent list and herdr agent wait see an agent named
craze: idle once the session is up and after each turn, working while a
turn runs, and blocked while a permission, question or plan card is waiting
on you — or when the session fails to start, or a turn ends in an error (until
the next send). A blocked state carries the one-line reason. The provider and
model go along as the pane's provider and model metadata tokens, which
craze clears when it releases the pane.
craze reports only when herdr's own variables say it is in a herdr pane:
HERDR_ENV=1, with HERDR_SOCKET_PATH and HERDR_PANE_ID both set.
If craze runs in a herdr pane but herdr never shows it, run
herdr pane get <pane>: an agent_session from another source (such as
herdr:cursor) means an agent hook tied the pane to its own session, and
herdr ignores craze's reports while it stands. A fresh pane has none.
roost¶
craze claims the tab under the source craze when its agent session starts,
and the tab shows it the way it shows any agent: no agent state while the
session sits ready, running while a turn runs, needs input with a
notification while a permission, question or plan card is waiting on you, and
failed with a notification when a turn ends in an error (until the next
send); both notifications carry the one-line reason. A turn that ends normally
leaves the tab idle with a "Turn complete" notification; a turn you cancel
leaves it idle with none. roostctl wait --state sees the same states, a failed
turn as needs_input. The claim carries the model, craze.provider and
version metadata keys. A session that never starts has no session id to claim
the tab with, so roost hears nothing about it.
craze reports only when roost's own variables say it is in a roost tab:
ROOST_SOCKET set, and ROOST_TAB_ID a positive integer with nothing around
it — the same rule roost's own hooks apply.
roostctl tab set-state on craze's tab wins. roost drops every report craze
sends for that session from then on, craze never claims the tab back for it,
and one host status: roost: … line on stderr after exit names the tab's
owner. craze's next session — a new one in the same run, or the next run —
claims the tab again. set-state none is different: it leaves the tab with no
owner at all, which craze cannot tell from a restarted roost, so craze claims
the tab back when it next reports after finding it unowned.
The agent's environment¶
The agent loses each host's hook gate. While craze reports to a host, the
agent it spawns (cursor-agent, grok or gx) gets craze's environment without the
one variable that host's agent integrations gate on: HERDR_ENV for herdr,
ROOST_AGENT_HOOK for roost. Otherwise a herdr or roost cursor or grok hook
installed on this machine would fire inside craze's child and take the pane or
tab from craze: herdr's would tie the pane to the agent's own session, and
herdr would silently ignore every report craze sends for the rest of the
session; roost's would claim the tab as cursor or grok. Everything else
stays — HERDR_SOCKET_PATH, HERDR_PANE_ID and HERDR_BIN_PATH, and
ROOST_TAB_ID and ROOST_SOCKET — so the herdr CLI and roostctl still
work from inside the agent. The cost is that herdr's agent skill, run inside
the child, believes it is not inside herdr, and roost's own agent hooks do
nothing inside the child.
host_status = false in config.toml, or --no-host-status on the command
line, turns reporting to both hosts off and leaves the agent's environment
whole, so the agent's own herdr and roost hooks work again. The default —
including when the key is absent or the file cannot be parsed — is true;
outside a herdr pane or roost tab there is nothing to report to either way.
craze prompt and craze frame never report.
Terminal colours¶
The theme is more than craze's own cells: while craze runs it sets the
terminal's default background and text colours to the preset's (OSC 11 and
OSC 10) and hands them back on exit (OSC 111 and OSC 110), so the
picker's live preview moves the background too. The terminal's configured
defaults come back, not whatever colours happened to be set before craze
started.
background = false in config.toml, or --no-background on the command
line, turns that off: craze then draws on your terminal's own background and
writes none of those four sequences. The default — including when the key is
absent or the file cannot be parsed — is true, and only a literal false
disables it; there is no flag to force it back on. It is also off
automatically whenever craze is painting no colour at all (NO_COLOR,
TERM=dumb). A terminal that ignores OSC 11 simply keeps its own
background; a hard kill (SIGKILL) leaves the theme's colours set until the
terminal is reset, the same as the tab title.
Provider precedence¶
Ids are cursor, grok, gx, and native. An empty string (--provider "",
$CRAZE_PROVIDER="", provider = "") is unset, not unknown.
gx is charliek/grok-build, a
third-party fork of the Grok CLI that speaks the same ACP dialect as
grok — same wire protocol, same auth, same skills and plugins — so
everything on this page and in the rest of the docs about Grok's behaviour
applies to it too. It currently shares grok's ~/.grok home (config, auth,
sessions, skills); that is the fork's current behaviour, not a guarantee
craze makes.
gx is gated separately from the other ids: it appears in the startup picker
when a binary for it resolves, or when it is the resolved default (the default
is always included, so Esc never starts a provider the picker did not show)
— see Environment below for lookup order; cursor, grok and
native are always offered, so the picker can never be empty. --provider gx
works regardless of whether a binary resolves, and fails at spawn — agent
binary not found: … — if it does
not; this matches how craze prompt --provider gx and craze frame
--provider gx already behave, since neither consults the picker. The picker
asks the question a gx session would (Agent binaries):
[agents].gx, else gx on PATH — and --agent-bin or $CRAZE_AGENT_BIN
only when gx is itself the resolved default, since they are that provider's
binary. Then the override is exclusive, so one pointing at nothing hides gx
even when gx is itself on PATH. native runs in process (see Native
compaction below and Native sessions)
— there is no binary to resolve, so it is never gated the way gx is.
For the TUI and craze prompt, the first hit wins:
--provider <id>when the flag was passed (Changed).$CRAZE_PROVIDERif non-empty.providerin~/.craze/config.tomlif it is a known id.cursor.
Unknown --provider exits 2. Unknown env or config id warns and falls back to
cursor; the fallback is not written back. After a successful Start, the TUI
and craze prompt save the provider that actually started (an explicit picker
choice overwrites a stale unknown id; Esc on the fallback default does not).
--agent-bin / $CRAZE_AGENT_BIN override the binary of the launch's own
provider (Agent binaries). They do not select the dialect.
Agent binaries¶
Each ACP provider's agent binary can be set in config.toml, as an absolute
path:
[agents]
cursor = "/opt/cursor/bin/cursor-agent"
grok = "/home/me/.local/bin/grok"
gx = "/home/me/src/grok-build/bin/gx"
A session of an ACP provider runs the first of:
--agent-bin, then$CRAZE_AGENT_BIN— both only when the session's provider is the launch's own: the provider the command line resolved (--provider,$CRAZE_PROVIDER,providerin this file,cursor), or, for a--continueor--resumeload, the loaded session's;[agents].<provider>;- the provider's own name on
PATH:cursor-agentthenagentfor cursor,grokfor grok,gxfor gx.
So a session of any other provider — one the startup picker or the session
list starts, or a saved one of another provider the list resumes — never runs
the binary a launch named for its own: the launcher passes --agent-bin only
to a session host of the launch's own provider, and leaves CRAZE_AGENT_BIN
out of every other host's environment. The hub removes it
from its own environment, so a session it starts uses [agents] and
PATH.
A value that is not an absolute path (bin/grok, ~/bin/grok) or not a
string is ignored, with one line on stderr naming it. A path that is set and
does not exist is not a fallback: that provider's sessions fail to start —
agent binary not found: … — as an --agent-bin naming nothing does.
native runs inside craze and has no key. craze frame never reads this
file.
Native models and providers¶
The native provider (--provider native) knows a set of models before you
write anything: craze ships a model catalog inside the binary, and an
upgrade of craze is how that catalog changes. Your own two files in
~/.craze/native/ (or $CRAZE_HOME/native/) are laid over it — they hold
keys, changes to shipped entries, and models you add, never a copy of the
catalog — so a release that adds, renames or retires a model reaches every
machine with no edit.
The shipped catalog¶
The catalog's providers, and the environment variables each takes its key from, tried in order:
| Provider id | Name | Key variables |
|---|---|---|
anthropic |
Anthropic | ANTHROPIC_API_KEY (Anthropic) |
chatgpt |
ChatGPT plan | none: signs in with ChatGPT |
fireworks |
Fireworks | FIREWORKS_API_KEY |
meta |
Meta | META_API_KEY |
openrouter |
OpenRouter | OPENROUTER_API_KEY |
zai-coding-plan |
Z.AI Coding Plan | ZHIPU_API_KEY, ZAI_API_KEY |
Its models — aliases, wire model ids, context windows, efforts and costs — are
in
internal/harness/modeltable/catalog.toml;
a session's /model lists those whose provider has a key (which models, in
what order). Anthropic's are also the account's own:
see Anthropic.
OpenRouter's models are its ~<family>-latest routers where OpenRouter has
one: openrouter/gpt-sol-latest sends ~openai/gpt-sol-latest, which
OpenRouter answers with the newest GPT Sol, so a new release in the family
reaches you with no craze upgrade. A model whose family has no router, such
as openrouter/mimo-v2.6-pro, uses its numbered id. To stay on one numbered
model, add it as a model of your own in models.toml,
with its wire id (say wire_model = "openai/gpt-6.1-sol").
openrouter/gemini-4-argon is Gemini 4 Argon, Google's flagship reasoning
model, for complex coding and long agent work. It takes images, offers effort
low, medium (its default) and high, and costs $2 per million input
tokens and $10 per million output, against Gemini Flash Latest's $0.75 and
$3.75. OpenRouter's Gemini Pro router does not point at it yet, so it is on
its numbered id. It is not OpenRouter's start model: pick it with /model,
--model openrouter/gemini-4-argon, a default_model pin or a sub-agent's
model.
Meta's models are Muse Spark 1.3, muse-spark-1.3 and its cheaper
contributor tier muse-spark-1.3-contributor (Meta's start model), and
muse-wonder-1.0, Muse Wonder 1.0, Meta's most capable model, for long,
complex coding and agent work. Spark 1.3 offers effort from minimal to
max; its contributor tier from minimal to xhigh, since Meta documents
max for Spark 1.3 and Wonder only. Both default to high. Muse Wonder is in preview at Meta, costs more
($4 per million input tokens and $20 per million output, against
Spark 1.3's $1.25 and $4.25), has no contributor tier, and offers effort from
minimal to max (default high). It is not Meta's start model, so a
machine with only a Meta key starts on Spark until you pick Wonder: with
/model (which a new session then remembers),
--model muse-wonder-1.0, a default_model pin or a sub-agent's model. Muse
Wonder Full is a Muse Code subscription model, not on Meta's API, so craze
does not offer it.
Meta's provider speaks Meta's Responses API, through the
openai-responses driver, not Chat Completions.
On Chat Completions Meta withholds a model's reasoning from an API key, so
every step of a tool loop reasoned from scratch. On the Responses API craze
gets each step's reasoning back encrypted and replays it on the next request,
so a tool loop keeps its reasoning. It also sends the session's id as
prompt_cache_key, without which Meta caches nothing. To put Meta back on
Chat Completions, see Going back to Chat
Completions.
The catalog has no default model. It ranks its providers instead, in
provider_order, and gives each a start model: a new session that
remembers nothing, and has no default_model pin of yours
with a key, starts on the start model of the first provider in that order
that you have connected (where a new session
starts):
| Rank | Provider | Start model |
|---|---|---|
| 1 | zai-coding-plan |
glm-5.3 |
| 2 | chatgpt |
the first of chatgpt/gpt-6.1-sol, chatgpt/gpt-5.6-sol the account lists |
| 3 | fireworks |
fireworks/ember-1 |
| 4 | openrouter |
openrouter/gemini-flash-latest |
| 5 | meta |
muse-spark-1.3-contributor |
| 6 | anthropic |
the newest Opus the account lists (anthropic/claude-opus-5-5 until it lists a newer one), at medium |
So a machine with only a Fireworks key starts on fireworks/ember-1, one with
only a Z.AI key on glm-5.3, one with only an Anthropic key on
anthropic/claude-opus-5-5, and one signed in to the ChatGPT plan with a
Fireworks key as well on the plan's model. provider_order in your
models.toml changes the order; default_model picks one model outright.
With the catalog, a machine needs only a key: store one with
craze auth login, or export one of the variables above, and run
craze --provider native. With no key at all, a native session refuses to
start and names craze auth login (an API key, or craze auth login
chatgpt for a ChatGPT plan), the variables to set and the file an inline key
goes in.
Your two files¶
Both files are optional, each on its own, and each starts with version = 1.
providers.tomlholds keys and provider settings. It is the only one of these two files that holds a secret (the ChatGPT plan's tokens are kept apart, inauth/chatgpt.json: see The ChatGPT plan): keep it0600(craze tightens a looser one when it reads it, andcraze authwrites it so) and never paste it anywhere.models.tomlholds model settings and models you add, and no secrets.
An entry for something the catalog ships changes only the keys it writes:
# providers.toml
version = 1
[providers.fireworks]
api_key = "fw_..." # an inline key (craze auth login writes this); an exported variable still wins
# models.toml
version = 1
default_model = "muse-spark-1.3-contributor" # optional: your pin, where a new session starts when it remembers nothing
provider_order = ["fireworks", "zai-coding-plan"] # optional: replaces craze's provider order (below)
[models."fireworks/kimi-k3"]
default_effort = "max" # everything else stays as shipped
An entry for anything else is yours, and must be complete: a provider needs
driver (openai-compat, the Chat Completions API, with a base_url;
openai-responses, the Responses API with a key,
with a base_url; or openrouter, without one; the chatgpt and anthropic
drivers belong to craze's own providers of those ids), and a model needs
provider and wire_model:
# providers.toml
[providers.local]
name = "Local" # optional display name
driver = "openai-compat"
base_url = "http://127.0.0.1:8080/v1"
env_keys = ["LOCAL_API_KEY"] # optional, tried in order
# models.toml
[models."local/qwen"]
provider = "local"
wire_model = "qwen3-coder"
name = "Qwen3 Coder (local)" # optional, as is every key below
context_window = 131072
efforts = ["low", "high"]
default_effort = "high"
A model alias is at most 256 bytes: a models.toml with a longer one does not
load, and says which entry. A model's efforts list at most 8 levels, each at
most 32 bytes and one printable line (no newline, control or bidi character);
one past those does not load either, naming the entry and the key. A model's other optional keys are documented below:
max_output_tokens, cost,
vision and tool_profile;
models.toml also takes
[compaction] and
[subagents]. craze never rewrites models.toml,
and rewrites providers.toml only when you run craze auth or
store a key with /connect.
How the files merge¶
- Field by field. A key your entry writes replaces the shipped value; a key
it leaves out keeps it. A list (
efforts,env_keys) replaces the shipped list whole, and an explicit empty one means empty:efforts = []is a model with no effort control,env_keys = []a provider with no variables. A key written with an empty value (name = "",default_effort = "") is that value, not "unset". - Cost, rate by rate. Each
[cost]rate you write replaces the shipped rate on its own; one you leave out keeps it. An inherited rate cannot be cleared. A long-context tier merges the same way —long_context_aboveand each[cost.long_context]rate on its own — andlong_context_above = 0turns a shipped tier off, its rates with it. - Efforts and the default effort. An
effortslist without adefault_effortkeeps the shipped default if the new list has it, and otherwise the model has none. - Driver and base URL. A
driverthat differs from the shipped one drops the shippedbase_url(sodriver = "openrouter"alone is a valid override). An entry that changes the endpoint —driverorbase_url— without writingenv_keysdoes not inherit the shipped variables: the shippedFIREWORKS_API_KEYbelongs to Fireworks' own endpoint and never pays for yours. craze still treats it as a key, though: exported, it is kept out of the commands a native session runs and redacted from what they print, as every key is (Keys), so it does not reach your endpoint that way either. default_modelis yours alone: your pin, when it names a model. The catalog has no default model, so with none a new session starts by the provider order (where a new session starts). Earlier versions shippedfireworks/deepseek-v4p1-flashas the catalog's default; adefault_modelline naming it, written then, is now a pin like any other: delete it to follow the provider order.provider_orderreplaces the catalog's order whole: a new session that remembers nothing and has no funded pin tries only the providers it lists, in its order, each on that provider's start model, which only the catalog sets. An empty list (provider_order = []) tries none of them. A name that is not a provider is passed over with no warning.
A release never breaks a load. What a file gets wrong on its own still
stops a native session, naming the file, the table and the key: TOML syntax,
a key craze does not know, a version other than 1, an inline key that
cannot be one (below), or a value no entry could hold (an unknown driver, a
negative window, an effort listed twice, more than 8 efforts or one over 32
bytes or not one printable line, a default_effort outside the
efforts the same entry lists, a cost out of range, a provider listed twice in
provider_order). What an entry gets wrong only against the catalog is
dropped with a warning at session start, naming
the file, the table and the key, and the session carries on: a model whose
provider no longer exists, a default_effort the model no longer offers, a
partial entry for a model craze no longer ships, a default_model or
[subagents] target that is not a model. Dropping your entry for a shipped
model restores the shipped entry, so every provider's start model always
survives.
An entry for a shipped model or provider that repeats every value it writes is legal, but it pins those values: a later catalog's change to them will not reach you. Delete it to follow craze's updates.
Keys¶
For each provider, craze takes the first of its env_keys variables that is
set to a usable key, and otherwise its inline api_key. A value that cannot
be a key — shorter than 8 bytes, or overlapping craze's redaction marker — is
skipped as though unset, with one line at session start naming the variable
(never the value); an inline api_key like that is a load error instead.
Every key craze knows, from every provider, is redacted from tool output, and
the commands a native session runs are started without the variables that hold
one. That covers every variable the shipped catalog or your providers.toml
names as a key variable, even one no provider takes its key from any more —
a shipped provider's after you point it at your own endpoint or give it
env_keys of your own: it no longer funds anything, but its value is still a
credential. A provider with no usable key leaves its models unusable, not the
others.
craze auth login <provider> stores a key as that
provider's inline api_key, craze auth logout
<provider> removes it, and craze auth
list shows how each provider is connected: by a
variable, which always wins, by its stored key, or not at all. In a native
session, /connect stores a key the same way login does.
No key is checked with its provider when it is stored; a wrong one shows on
first use, as native: <Name> rejected the API key (HTTP 401); replace it with
"craze auth login <id>", followed by , or check and the provider's key
variables when it has any — except that craze auth login anthropic then
fetches the account's model list, so a key Anthropic rejects is
a note there and then. When they write, login,
logout and /connect:
- Rewrite
providers.tomlwhole, at0600(a new directory is made0700). Comments and layout are not kept — the header they write says so — but every key of every entry is,sourceand an explicitenv_keys = []included. - Change one provider's
api_keyand nothing else. A key the rules above refuse is not stored. Another provider's stored key that cannot be used is kept exactly as it is and named in a note, so two broken keys can be fixed one at a time (native sessions start again once both are). - Remove only the key. An entry left with nothing else in it goes; one that sets a name, an endpoint or variables stays.
- Refuse a
providers.tomlthat is a symlink — edit its target by hand — and never writemodels.toml. - Take turns: each holds a lock beside the file,
providers.toml.lock, while it reads and writes.
They know the catalog's providers and your own in providers.toml; in a
directory whose models.toml says catalog = false,
only your own.
Keys stored while a session runs¶
A running native session takes up a provider you give a key to while it runs —
an api_key written into providers.toml by craze auth login, /connect,
by hand, or by another craze — when it next looks: at the start of its next
turn, or at once when the TUI asks (/connect after a save, /model when it
opens; see the TUI). A
session that nobody asks, in a terminal that is not a TUI, takes the key up at
its next turn. What changes is which models are offered and can be switched to:
a conversation's compaction and sub-agent settings stay as it opened them.
The running session also watches that file to redact what it
holds: at the start of every turn (a prompt, a /compact, or the delivery of
a background sub-agent's result) it checks the providers.toml of the craze
directory it started with, and when the file has changed it learns each
inline key it did not know and redacts it from then on, as it redacts every
key it knows — tool output and spill files included. A value that cannot be a
key (shorter than 8 bytes, or overlapping the redaction marker) is ignored
with one line naming the provider, never the value; a file that no longer
parses is one line too, and nothing in it is learned until it is fixed.
If a newly stored key turns out to be inside what the session sends with every
request — its system prompt (which names the working directory), its tool
definitions, or its plan file's path — none of that can be taken back, so the
session refuses every later turn with a newly stored API key appears in this
session's frozen prompt; start a new session.
The redaction starts at the next turn, so there is a window: a key stored during a turn is learned when the next one starts. From then on a shell command still running — a background command's included — redacts it from the rest of its output and from its saved output file, but not from what it printed before. A sub-agent already running — a background one goes on while the conversation idles — redacts it at once from what its tools return next, so a file it reads afterwards reaches its model with the key redacted; what its tools returned before the key was learned is not rewritten. One started after the key was learned redacts it from the start.
The ChatGPT plan¶
The catalog's chatgpt provider, ChatGPT plan, runs OpenAI's models on
your ChatGPT subscription through OpenAI's Sign in with ChatGPT: requests are
billed to the plan's usage, not to an API account, and count against the
plan's limits (on Plus, a five-hour limit shared with your other apps that use
the plan). Manage that usage in ChatGPT's settings.
It takes no key. It names no variable and stores no api_key: it is
funded by signing in with
craze auth login chatgpt or, in a
native session, with /connect — the same sign-in, inside
the TUI, into the TUI's own craze directory — and
craze auth list shows the sign-in. A
providers.toml entry for it may set only name; a driver, base_url,
env_keys or api_key written there is dropped with a warning naming the
key. The chatgpt driver belongs to this provider alone: an entry of yours
that names it under another id is dropped with a warning (a load error in a
catalog = false directory, which has no
ChatGPT plan).
Its models are the account's own, not the catalog's: the list ChatGPT
offers the signed-in account, fetched when you sign in and again, in the
background, by a native session that starts with a list over a day old, or one
fetched by a craze that asked for a different client version (see below). Each
is chatgpt/<slug> — chatgpt/gpt-5.6-sol, say — for --model and
/model, named as ChatGPT names it with (ChatGPT plan) after, and listed
in /model together, in the account's order. Its context window is the one
the list gives; its efforts are those the list offers among none,
minimal, low, medium, high, xhigh and max, and it takes images
when the list says it does. It has no cost (the status row shows tokens
alone) and no /fast. Signed out, or with plan usage off, the plan's models
are not offered; a session already on one keeps it in its picker, as with any
provider that loses its key.
The shipped catalog's [chatgpt_defaults] table holds craze's own settings for
the plan's models, laid over the account's list: start, the plan's start
model in the provider order, an ordered list
(gpt-6.1-sol, then gpt-5.6-sol; the first the account lists wins), and
per-slug name, efforts (which can only
narrow the account's), default_effort and tool_profile (gpt-6-astra
defaults to low). It ships in the binary like the rest of the catalog; it is
not a models.toml setting.
ChatGPT lists its newer models only for a client that says which version it is, so craze asks for the list as one pinned version that it has tested; the list's file records it, and a list fetched with another version is fetched again at the next sign-in or session start. A model the list says needs a newer client than the one this craze pins is not offered. The pin ships in the binary and is not something to set.
Its files are in the native directory (~/.craze/native/, or
$CRAZE_HOME/native/), each 0600, the sign-in's under auth/, a directory
craze makes 0700:
| File | Holds | Secret | Signing out |
|---|---|---|---|
auth/host-id |
This machine's id for ChatGPT (urn:uuid:…), made before the first sign-in |
no | kept |
auth/chatgpt-client.json |
craze's registration with ChatGPT: the client id it was issued, the account's id and email, whether the account allowed plan usage, whether the one-time notice was shown | no | kept |
auth/chatgpt.json |
The tokens: access, refresh and id token, and when they expire | yes | deleted |
auth/chatgpt.json.lock |
The lock every change of the two files above is made under | no | kept |
chatgpt-models.json |
The account's model list, with the account it belongs to | no | kept |
chatgpt-models.json.lock |
The lock a refresh of the list is made under | no | kept |
Never copy or paste auth/chatgpt.json; craze writes it durably (synced to
disk) and never logs it. Signing out deletes it and keeps the rest, so the
next sign-in reuses craze's registration and the browser asks only which
account to use. A craze directory holds one ChatGPT account: a sign-in
with a different account is refused, since the registration is that
account's; use another CRAZE_HOME for a second account. The model list is
bound to the account too: a list that another account fetched — after a
change of account, say — is ignored until the next sign-in fetches the
account's own, and one craze cannot read is a warning at session start, with
no plan models until it is fetched again. So are the models it lists: a
session already running on one, or a sub-agent, sends nothing on another
account's sign-in — its next request, a summary's or a wake's too, ends with
native: model "chatgpt/…" is from another ChatGPT account's model list; a
new session offers the signed-in account's models — and goes on once the
first account is signed in again. Switching that session to another model
lets go of the first account's: /model then offers the signed-in account's
model of the same name, when its list has one.
Renewal. An access token lasts about an hour. craze renews it itself when a
request finds less than five minutes left, so the sign-in lasts until you sign
out. Every craze process — each native session's host, craze auth, the TUI —
shares the token file: one renews it under the lock, and the others take its
new tokens rather than renewing again. A renewal ChatGPT refuses for good (the
sign-in revoked, say) deletes the tokens, and the next turn ends with
native: ChatGPT sign-in is no longer valid; run /connect or craze auth login
chatgpt.
Requests go to OpenAI's Responses API at https://api.openai.com/v1 with
the access token, and nowhere else: no redirect is followed, and the token is
never put in any process's environment. Each carries the session's id, so
ChatGPT can reuse the cached start of a conversation. The
output ceiling is not sent: ChatGPT plan requests
refuse one, though the ceiling still sizes compaction.
The usage limit. When ChatGPT says the plan's usage limit is reached, the
turn ends with native: ChatGPT plan usage limit reached for craze. Manage
usage: https://chatgpt.com/settings/usage, and that craze process sends no
other request on the plan — a background sub-agent's, a wake's or a retry —
until you start the next turn. An account the plan cannot be used with in
craze, or a request it refuses, ends the turn with its own message.
Redaction. The token values are treated as keys are (Keys):
every native session, whatever its provider, learns the ones in
auth/chatgpt.json at the start of each turn and redacts them from tool
output, and a session that uses the plan learns renewed ones the moment it
renews them — its shell commands still running and its sub-agents included,
a sub-agent that is just starting too. The file tools refuse the whole auth/
directory, a hard link to any file in it included, and so does a search:
grep refuses to search there, and neither grep nor glob shows a file
there — or providers.toml — among what it finds in a directory above it.
A long line grep or read shortens is redacted, with every value the
session knows by then, before it is cut, so a value renewed while a search
runs leaves no piece of itself at the cut.
Anthropic¶
The catalog's anthropic provider, Anthropic, runs Claude models on
Anthropic's own API (the Messages API) with an API key, billed per token to
the key's account at Anthropic's prices. Connect it like any key provider:
export ANTHROPIC_API_KEY, or store a key with craze auth login
anthropic or /connect.
Its entry is locked. A providers.toml entry for anthropic may set
name, env_keys and api_key; a driver, base_url or source written
there is dropped with a warning naming the key, and craze's own driver and
endpoint are used, so the key goes to Anthropic and nowhere else. The
anthropic driver belongs to this provider alone: an entry of yours that
names it under another id is dropped with a warning (a load error in a
catalog = false directory). There, with no
catalog, a provider of your own named anthropic on another driver — a
gateway of yours, say — is yours. env_keys = [] is the opt-out from an
exported ANTHROPIC_API_KEY: the variable then funds nothing, and is still
kept out of the commands a native session runs, as every key variable is
(Keys).
Its models. craze ships one for each Claude model the API offered when
this version was made, each anthropic/<Anthropic's id> — Opus 5.5
(anthropic/claude-opus-5-5), 5, 4.8, 4.7, 4.6 and 4.5; Sonnet 5.5, 5 and 4.6;
Haiku 5.5 and 4.5; Fable 5.1 and 5 — named as Anthropic names them, with their
context windows, the efforts each takes (low to max; Haiku 4.5 has no
effort control) and Anthropic's prices. Each model with effort control starts
at medium (one rule for every Claude model; Anthropic's own default for some
is high), and every one takes images. Every request asks for adaptive
thinking, shown summarized, except on Opus 4.5 and Haiku 4.5, which take none.
The output ceiling is 64,000 tokens. Every model but Opus 4.5 and Haiku 4.5,
whose own maximum is 64,000, allows up to 128,000: set
max_output_tokens for one of those to raise it, never
past the model's own maximum, which Anthropic refuses.
The account's model list. craze also keeps the list of models the key's
account may use, as Anthropic's API gives it (GET /v1/models, which costs
nothing), in anthropic-models.json in the native directory. The list is
bound to the key that fetched it: a session uses it only when that key is
the one the provider is funded by when the session loads its models — the
first of its env_keys set to a usable key, else its stored key — so a list
another key fetched (before you changed keys, say, or another account's) is
ignored, and the shipped models stand alone until the current key's list is
fetched. The key is read when a session loads its models — when it starts,
and when it loads them again — so a key changed in between binds the list at
the next load.
When it is fetched. With the key that funds the provider — an exported
one included — the list is fetched when craze auth login
anthropic stores a key; when a native session
opens and the key's list is missing, over a day old or another key's; and
when the TUI asks a running session to look again — /connect after a save,
/model as it opens (the TUI) — so a key just connected is
the one fetched with. A session's fetch runs in the background, and sessions
opening at once on one craze directory make one request between them. A
session offers a list its own fetch wrote as soon as it is written (after the
turn, while one runs), and one another session or craze auth login wrote at
its next turn or /model. A fetch that fails is a line in the
session's journal and is tried again no sooner than 30 seconds later for the
same key; until a list is fetched the shipped models stand alone. A fetch
writes only a complete list naming at least one model, so a partial or empty
answer keeps the list there is.
Where the list is used, it is what /model offers:
- a shipped model it names keeps craze's settings and price;
- a shipped model it does not name is not offered (a partial entry of yours for one is ignored without a warning, and applies again when the account lists the model);
- a model it names that this craze does not ship is offered too, as
anthropic/<id>, named as Anthropic names it, with the window, efforts, image input and thinking the list gives and a ceiling of the model's own limit up to 64,000 — and no price (the status row shows its tokens alone) until a craze release adds it; - a model the list says is deprecated has
(deprecated)after its name; - an alias craze has retired is never offered again, whatever the list says.
A new session that reaches anthropic in the provider
order starts on the newest Opus the account lists — so
a new Opus is the start the day the account lists it — at medium. In
/model, Anthropic's models are listed together, placed by the provider's
name, newest first. The catalog's [anthropic_defaults] table holds these
rules — start_family (opus), default_effort (medium) and list
(all, every model the account lists; newest would offer each family's
newest alone) — and ships in the binary: it is not a models.toml setting,
and neither are a model's family, release date or thinking, which only craze's
catalog and the account's list say.
Its files are in the native directory, 0600:
| File | Holds | Secret |
|---|---|---|
anthropic-models.json |
The account's model list, when it was fetched, and the id of the key that fetched it: the first 12 hex digits of the key's SHA-256, never the key | no |
anthropic-models.json.lock |
The lock a fetch of the list is written under | no |
A list craze cannot read is a warning when a session loads its models, and the
shipped models stand alone until it is fetched again. craze auth logout
anthropic keeps the list: without a key it funds nothing.
Requests go to https://api.anthropic.com with the key, and nowhere else:
no redirect is followed. A key Anthropic rejects ends the turn with native:
Anthropic rejected the API key (HTTP 401) and how to replace it; a model the
key may not use, with native: Anthropic refused access to model "<alias>"
(HTTP 403): <Anthropic's reason>; and a model the account cannot use (a
listed model needing a setting the account lacks, say), with native: the
Anthropic account cannot use <id> (HTTP 404: <Anthropic's reason>).
An older Claude id of your own. A model you add on the anthropic
provider with an id craze knows — a shipped one, or one the account's list
names — takes what craze knows of it. One with an id craze has never seen is
sent adaptive thinking, as every current Claude model takes it; an older
model that takes none answers every request with an HTTP 400 about the
thinking field. Use a model /model offers instead: those are the ones the
account lists, and craze knows each.
The Responses API driver¶
The openai-responses driver speaks the OpenAI Responses API
(POST <base_url>/responses) with an API key, through craze's own client. It
is tested against Meta's Responses API, and the shipped meta provider uses
it. Any provider of yours may use it too: give it a base_url, and fund it as
any key provider, from env_keys or an inline api_key.
Where the key may go. The driver sends a static key, so its base_url
must be https, on any host, or plain http only to a loopback IP address
(127.0.0.1 or another 127.x.x.x, or [::1], never a name such as
localhost), with no user name or password, query or fragment. Anything else
stops the load, naming providers.toml, the provider and base_url; the
client checks the same rule again for every model it builds. No redirect is
followed, so the key reaches that endpoint and nowhere else. The rule holds
for Meta's provider too: a [providers.meta] entry that writes a base_url
and no driver, such as a gateway's, sends Responses requests there, and a
plain http one to anything but a loopback IP address is dropped with a
warning, Meta's own endpoint used instead. To send Meta's requests to a Chat
Completions endpoint of your own, write driver = "openai-compat" beside its
base_url.
What it sends. Every request is whole (store: false, so the provider
keeps nothing between requests) and streamed, with the system prompt as
instructions, images as input_image data URLs, every tool a plain
function, and:
- the model's effort as
reasoning.effort, and a request for the reasoning's encrypted content, which is replayed on the next request to the same provider and wire model, so a tool loop keeps its reasoning; - the output ceiling as
max_output_tokens; - the session's id as
prompt_cache_key, on every request a session sends — each step,/btw, the summarizer — and each sub-agent's own id on its own, since Meta caches nothing without a key.
Failures. A rejected key (HTTP 401 or 403) ends the turn with native:
<provider> rejected the API key and how to replace it, after one request; a
404 says the provider does not serve the model's wire_model; a 400 is not
retried. A 429 or a 5xx, and a rate limit or server error inside the stream,
are retried before any output, honouring the provider's Retry-After. An
answer that has begun is never retried. A response cut short by the output
ceiling ends the turn as on Chat Completions, stopped at max_tokens with the
answer so far, and a tool call cut off mid-arguments is not run; one stopped by
the provider's content filter ends as a refusal.
Going back to Chat Completions¶
To run Meta on Chat Completions, as craze did before, write its Chat Completions driver, endpoint and key variable together:
# providers.toml
[providers.meta]
driver = "openai-compat"
base_url = "https://api.meta.ai/v1"
env_keys = ["META_API_KEY"]
If your providers.toml already has a [providers.meta] entry, add these
keys to it. If that entry says source = "gx", also remove that line, or set
source = "manual". Otherwise craze still ignores the entry's driver and
base_url (see below).
Write driver and base_url together. A driver that differs from the
shipped one drops the shipped base_url (how the files
merge), so driver = "openai-compat" written alone is
dropped with a warning, and Meta stays on the Responses API.
Write env_keys too, unless the entry has an inline api_key (the one craze
auth login meta stores) to fund it. A different driver does not take the
shipped META_API_KEY either. An entry with neither still loads on Chat
Completions, but nothing funds Meta: each session start, and craze auth
list, warns [providers.meta] env_keys: missing and says to add env_keys =
["META_API_KEY"] or store a key. Before this release, such an entry took
META_API_KEY from the shipped provider, since its driver was the shipped
one. The same warning applies to any shipped provider whose driver you change
without giving it a key.
Delete the entry, or its driver, base_url and env_keys, to follow
craze's driver again. The same applies to a provider of yours: write its Chat
Completions driver and base_url instead.
A [providers.meta] entry an old craze's gx import wrote, marked source =
"gx", does not hold Meta on Chat Completions, even though it says driver =
"openai-compat". Such an entry yields to the catalog:
craze takes its api_key and any env_keys variable the catalog lacks, and
ignores its driver and base_url, so Meta moves to the Responses API with
the rest. A driver you wrote yourself, in an entry with no source or with
source = "manual", is kept.
A session recorded on either driver resumes on the other. The encrypted
reasoning never goes to Chat Completions: a step's reasoning goes there as an
empty reasoning_content, which Meta accepts. Chat Completions reasoning never
goes to the Responses API, and every tool call keeps its result. A running
session goes on with the client its model was built with: the new driver
reaches it when it next builds one, in a new or resumed session or at a switch
to another model.
Model memory: recent.json¶
A model or effort you pick in a native session's /model is remembered for
the next session, in recent.json beside the two files
(~/.craze/native/recent.json, or $CRAZE_HOME/native/):
{
"version": 1,
"recent": [
{
"model": "muse-spark-1.3-contributor",
"provider": "meta",
"wire_model": "muse-spark-1.3-contributor",
"effort": "xhigh",
"at": "2026-09-30T10:12:00Z"
}
]
}
The list is newest first, one entry per model, and keeps at most 10. Each
entry names the model by alias and by its provider and wire model, with the
effort last used on it ("" for a model with no effort control) and when it
was picked (at, in UTC, for people reading the file). The file is 0600.
Deleting it forgets everything.
Who writes it. Only a change made inside a running session — a model
switch (/model, the model dialog, or an attached client's switch) or an
effort change on the current model (which makes that model the newest, even if
/model was never used) — once it has taken, recording the model and the
effort the session is then on. --model, craze
prompt, a resume and a sub-agent's model never write it. A session's writes
land in the order its switches took, so the file's newest entry is its newest
choice; one an older switch would write after a newer one is skipped.
Who reads it. A session reads it when it starts — where it starts, and
the order its /model lists models in, the remembered ones right after the
current one, newest first — and every switch reads it again for the effort the
switch lands on. A switch to a model lands on the effort last used on it: the
one this session last set on that model (which the session holds whether or
not the file could be written, and judges as the file's entries are judged:
a model a release renames keeps it under its new alias, and an alias pointed
at another model does not inherit it), else the one this file remembers for it, else
the model's default_effort — never the effort the session was on. A
remembered effort the model no longer offers is its default. Asking for the
model you are already on keeps its current effort. Each switch or
effort change also ranks the running session's own list again, the model
switched to first, so its /model shows the new order and which effort each
model would land on; a choice another craze writes is seen by the next switch
to that model, but by the list only when the session next reloads its models
(after a key is saved or a sign-in, say). A reload ranks the list by the file
as it is then: when a write failed (its note says so), or was still pending
when the reload read the file, that model's place in the list can lag until
your next switch — the effort a switch to it lands on is still the one you
set in this session. /model X <effort> still picks a
level directly. See Effort on a switch.
Two sessions at once. Every write reads, changes and replaces the file
under a lock beside it, recent.json.lock, so two sessions switching at the
same moment both keep their choice. A switch waits at most 2 seconds for
another craze's lock. When the choice cannot be saved — the lock stayed busy,
the file cannot be read or written, or a newer craze wrote it — the session
says so in one note (not saving the model choice: …) and the switch itself
stands.
Reading is bounded. The read never waits on a FIFO and stops after 64 KiB,
though a stalled filesystem can still delay an ordinary read. A missing file, one craze cannot read, one
that is not a regular file (a FIFO, say) or is larger than 64 KiB, and one
that is not a recent.json (not JSON, or a version craze does not know) are
no memory at all. The next switch replaces a file that is not a
recent.json, and leaves alone one it cannot read, one that is not a regular file or is over
64 KiB, or one a newer craze wrote.
Where a new session starts. With no --model, a new native session starts
on the first of:
- the newest remembered model whose provider has a key, at its remembered
effort when the model still offers it (its
default_effortotherwise); - your
default_model, when you set one and its provider has a key (one that fails for another reason is still used, so the session says why); - for each provider in the provider order — your
provider_order, else the catalog's: Z.AI Coding Plan, ChatGPT plan, Fireworks, OpenRouter, Meta, Anthropic — that has a key, the provider's start model (glm-5.3; the first ofchatgpt/gpt-6.1-solandchatgpt/gpt-5.6-solthat the account lists;fireworks/ember-1;openrouter/gemini-flash-latest;muse-spark-1.3-contributor; the newest Opus the Anthropic account lists); - otherwise the first model, by alias, whose provider has a key — with a note saying so.
Steps 2 to 4 start at the model's default_effort. With no provider funded
at all the session does not start, and says how to connect one.
A remembered model is found by its alias while that alias still names the same provider and wire model, and otherwise by its provider and wire model, under whatever alias has them now: a release that renames a model keeps your choice, and an alias pointed at a different model does not inherit it. An entry no model matches, or whose provider has no key, is passed over and left in the file.
--model <alias>(the TUI's andcraze prompt's) starts on that model at the effort remembered for it, when the model still offers it, and writes nothing.craze promptwithout--modelstarts where a new TUI session would.- A resume (
craze -c,--resume) keeps its transcript's model and effort, and an explicit--modelon a resume picks the model with the transcript's effort carried over: the memory never applies to a resume. A resumed session that turns out to have no transcript at all opens as a new one, and starts as above.
Isolated setups: catalog = false¶
A models.toml with catalog = false at the top turns the catalog off for
its directory: the two files are then the whole table, as before the catalog
existed — both are needed, default_model is required, and every entry is
complete. There is no provider order there (start models come from the
catalog), so provider_order changes nothing. It is meant for sandboxes such
as the evaluation harness's homes, where no model beyond the ones written may
appear.
Files from an older craze¶
craze once imported its entries from gx, marking each source = "gx". Such
files still load and are never rewritten, but their entries yield to the
catalog: a model entry marked source = "gx" is ignored when craze ships (or
has retired) its alias, and a provider entry marked source = "gx" for a
provider craze ships contributes only its api_key and any env_keys
variable the catalog lacks. Any other entry — source = "manual", or none —
is your own, as above. A hand-copied entry that repeats the shipped one is a
no-op.
Retired models¶
When a release retires a model (or renames one, retiring the old alias), the
catalog records the alias and every dead wire model id it pointed at; an
alias whose wire id still answers is retired by alias alone, with no wire ids
listed, so an entry of yours pointing at that id is not dropped. A retired
alias disappears from an untouched directory with no warning: a source =
"gx" entry for it is ignored, a default_model naming it is no pin (a new
session starts by the provider order), and a [subagents] model or tier
naming it falls back to its default (the parent's model). Any model entry of
yours, under any alias, whose provider and wire model are a retired pair is
dropped silently too — a
dead wire id is dead whoever wrote it. An entry of yours for a retired alias
that points at a live wire id is your own model and stays.
Sub-agent settings¶
[subagents] (see Sub-agent models) never stops a
session for what the catalog changed: a model or tier target that is not a
model is dropped — silently for a retired alias, with a warning otherwise —
and falls through to the defaults, and an effort the kept model does not
offer is dropped with a warning.
Native output ceiling¶
On the native provider (--provider native), every request to a model names
an output-token ceiling: the most tokens one response may generate, reasoning
included. A model's optional max_output_tokens in ~/.craze/native/models.toml
(or $CRAZE_HOME/native/) sets it:
[models."fireworks/deepseek-v4p1-flash"]
max_output_tokens = 65536 # optional, >= 0; absent or 0 = the default below
A model with no max_output_tokens gets 32,000 tokens, the same default
opencode uses — or a quarter of the model's context_window, rounded down and
never under one token, when that is smaller (a window under 128,000 tokens), so
a small model keeps room for its prompt. The default exists so a response that starts repeating itself stops
there instead of running to the provider's own cap. An explicit value always
wins, even above 32,000: set one for a model whose answers or reasoning need
more room, or lower for a provider that refuses 32,000. A negative value is
refused at load.
On the wire the ceiling is max_tokens, except for a wire model id that
contains gpt-5 (among a few OpenAI reasoning-model names), which gets
max_completion_tokens instead. The same ceiling sizes
compaction: the automatic trigger never passes the window
less the ceiling. On a model with no max_output_tokens and a window under
about 213,000 tokens, the default therefore moves that trigger below 85% of the
window (to 75% under 128,000 tokens), where before it sat at 85%; a session
resumed after upgrading that is already past the new trigger compacts before
its next turn. A smaller max_output_tokens moves the trigger back up.
The value is your own entry in models.toml; craze
never rewrites it.
The ChatGPT plan's models are the exception: no ceiling
is sent to them, since ChatGPT plan requests refuse one, but the ceiling
(32,000, or the quarter of the window) still sizes their compaction. On the
openai-responses driver, Meta's among them, the
ceiling is max_output_tokens.
Native compaction¶
On the native provider (--provider native), ~/.craze/native/models.toml
(or $CRAZE_HOME/native/ when the home is relocated) may carry an optional
[compaction] section — see Compaction for what it
controls:
[compaction]
auto = true # optional; compact on its own at all
threshold_percent = 85 # optional, 1-99: % of the model's context window
tail_tokens = 20000 # optional, >= 0: the most, in tokens, of a
# verbatim tail a compaction keeps (whole steps
# only), also capped at a quarter of the threshold
Every key is optional and applies to every model — a per-model override is a
follow-up. The section is your own entry in models.toml: craze reads it and
never rewrites the file. If a tool of yours does re-encode the file, a section
left with none of these keys is dropped rather than written out empty.
Native cost¶
On the native provider, ~/.craze/native/models.toml (or $CRAZE_HOME/native/
when the home is relocated) may carry an optional per-model cost table —
what the status row and the usage
section price a session's spend from:
[models."fireworks/kimi-k3".cost]
input = 0.60 # $ per 1,000,000 uncached input tokens
output = 2.50 # $ per 1,000,000 output tokens (reasoning included)
cache_read = 0.15
cache_write = 0.0
Every key is optional and independent; a model with no [cost] table saves
exactly as it did before this section existed. Shipped models may carry a
cost of their own; a rate you write for one replaces that rate alone (see
How the files merge). The Z.AI Coding Plan's models
carry none, like the ChatGPT plan's: both are flat-rate plans. Anthropic's
shipped models carry Anthropic's prices — cache_read its cache hit,
cache_write its five-minute cache write, the only one craze asks for — and a
model only the Anthropic account's list names carries none until a release
adds it (Anthropic). Haiku 5.5 also has a long-context tier,
below. Each rate is dollars per 1,000,000 tokens, from 0 up to $10,000 — a
rate outside that range, or one that is not a finite number (nan, inf), is
refused at load.
craze prices a usage record by identity — the model's (provider, wire
model) pair — never by the alias it happened to run under, since a resumed
session's alias can stop existing while the identity it named still does.
On every price lookup craze rebuilds the canonical identity → rates map from
the table's models: for every alias that shares an identity, the first in
sorted order that has a cost prices that identity, and each rate is
rounded to an exact number of picodollars per token, half up, on that same
lookup — not built once and cached at load. Two priced aliases of the same
identity that set a different cost still load, with a warning naming
both and saying which one's price is used. An identity with no priced alias
is simply unpriced — its usage is still counted in tokens, but adds no cost.
The cost table is your own entry in models.toml, the same as
[compaction] above: craze reads it and never rewrites
it.
Long-context pricing¶
A model's cost may have one price tier for long prompts: a request whose
prompt is over long_context_above tokens is priced at the
[models."<alias>".cost.long_context] rates instead, every token of it.
[models."anthropic/claude-haiku-5-5".cost]
input = 0.1
output = 0.5
cache_read = 0.01
cache_write = 0.125
long_context_above = 100000 # a prompt over this many tokens is long
[models."anthropic/claude-haiku-5-5".cost.long_context]
input = 0.5 # the rates a long request pays, $ per 1,000,000 tokens
output = 2.5
cache_read = 0.05
cache_write = 0.625
- The prompt is everything the request sent: its input, cache reads and
cache writes together. Exactly
long_context_abovetokens is not long; one more is. - Each request is priced on its own, as Anthropic bills it: one step of a turn, one attempt of a compaction, one request of a sub-agent or of a side question. A long request pays the long rates even when part of its prompt is a cache hit, and the requests before and after it keep their own prices. craze classifies each request as its usage arrives, so a sub-agent's spend and a compaction's are priced request by request though they are reported as one sum.
- The keys.
long_context_aboveis a whole number of tokens; above 0 it needs thelong_contexttable withinputandoutputat least, and the table needs it. It is at most the model'scontext_windowwhen the model declares one. Eachlong_contextrate is optional and in the same range as the base rates. One the merged table leaves out — left out by your entry and, on a shipped model, by the shipped tier — falls back to the model's base rate for it, never to 0: a tier withinputandoutputalone prices a long request's cache reads and writes at the basecache_readandcache_write. A problem names the model, the table and the key. - On a shipped model the keys are an overlay like the rates: a
long_contextrate you write replaces that rate alone, andlong_context_above = 0turns the tier off, so every request is priced at the base rates.
The shipped catalog gives a tier to Haiku 5.5 alone — the rates above, which are Anthropic's for prompts over 100,000 tokens. Every other shipped Anthropic model takes its whole context window at its standard price.
A session's transcript keeps what it needs to price the tier again on a resume: the part of a sub-agent's usage row, and of a compaction's usage, that came from long requests. A resumed session prices it with the table it loads, so its spend matches the live session's while the prices are unchanged. A transcript from before the tier existed has no such parts, so its sub-agent rows and compactions price as they did; its own steps are priced one by one like any other, so a long Haiku 5.5 step there now reads at the tier. If that part is ever larger than the usage it belongs to, or below 0, craze does not trust it and prices that usage at the base rates.
Native vision¶
A native model's vision key says whether craze may send it images: the
pictures pasted into the composer (see Images) and the ones
the Read tool returns for an image file. The shipped catalog sets it
true on the models seen to describe a screenshot, and leaves it off on GLM
5.3 — glm-5.3, Z.AI's start model, fireworks/glm-5p3 and
openrouter/glm-latest. Every shipped Anthropic model takes images, and a
model only the Anthropic account's list names takes them when the list says it
does. A model you add defaults to false.
[models."local/qwen"]
provider = "local"
wire_model = "qwen3-vl"
vision = true # this one takes images
[models."glm-5.3"]
vision = true # turn it on for a shipped model, or false to turn it off
On a shipped model the key is an overlay like any other (see How the files
merge): the one you write replaces the catalog's for
that model alone. For a model without vision, the composer warns at paste
time and the model receives [Image omitted: <model> does not accept images.
File: <path>] where the image was; the
Images section of the TUI reference has the
rest.
Native tool profile¶
A native session offers its model a set of tools and a system prompt written
for them: a tool profile. craze ships one, opencode (the file tools
read, write and edit, then bash, grep, glob and the others), and
it is the default for every model. A model entry in models.toml may name a
profile with tool_profile:
[models."local/qwen"]
provider = "local"
wire_model = "qwen3-coder"
tool_profile = "opencode" # the only profile there is today; leaving it out is the same
A profile name craze does not know makes the load of models.toml fail, with a
message listing the ones it has (unknown tool profile "…": want one of "opencode", or
leave it out for the default). The profile is fixed when a session opens,
because the system prompt is frozen for the session, and is recorded in the
transcript's header: switching with /model to a model whose profile differs
from the session's is refused, and a resumed session keeps the profile it
started with. With one profile today, the key changes nothing yet.
Cached results of waiting commands¶
When a message or plan is longer than --max-message allows, craze
wait, craze new --wait and craze send --wait print the
head and write the whole result to
~/.cache/craze/results/<short>-<inc>-<turn>.md: <short> is the session's
short id, <inc> the last 8 characters of the incarnation the turn ran in
(turn ids restart with each incarnation) and <turn> the turn's id. The files
are 0600 in a 0700 directory under the process's own $HOME, not
CRAZE_HOME, like the rest of ~/.cache/craze. They are rewritten when the
same turn is waited on again, and craze does not read them back.
Environment¶
| Variable | Purpose |
|---|---|
CRAZE_HOME |
The craze directory (default ~/.craze): config.toml and the session index live directly inside it. Skills and plugin caches still follow HOME |
CRAZE_AGENT_BIN |
The launch's own provider's agent binary when --agent-bin is unset; never a session of another provider's (Agent binaries) |
CRAZE_PROVIDER |
Provider id when --provider is unset (cursor, grok, gx, or native) |
CRAZE_JOURNAL |
Turns the session journal off for this run when it reads as false. It cannot turn one on against journal = false, and a value craze cannot read as a bool turns it off with one line saying so. Empty or unset leaves the decision to the config file |
CRAZE_CONTROL_SOCKET |
Turns the control socket off for this run when it reads as false. It cannot turn one on against control_socket = false, and a value craze cannot read as a bool turns it off with one line saying so. Empty or unset leaves the decision to the config file |
CRAZE_DETACH |
Turns detached hosts off for this run when it reads as false: the session runs inside the TUI's own process and ends with the terminal. It cannot turn detaching on against detach = false, and a value craze cannot read as a bool turns it off with one line saying so. Empty or unset leaves the decision to the config file |
CRAZE_HUB_IDLE |
How long the hub waits, with no client and no live host, before it exits: a Go duration ("30s", "5m"). Default 60 seconds; a value that is not a positive duration gives the default. Mostly of use in tests |
CRAZE_RUNTIME_DIR |
Overrides the control socket's runtime base (default: $XDG_RUNTIME_DIR/craze, then /run/user/<uid>/craze on Linux, then /tmp/craze-<uid>): an absolute path, short enough to leave room for <ns>/<hostId>.sock under sun_path's limit. Meant for tests and unusual hosts; see Protocol reference |
ANTHROPIC_API_KEY, FIREWORKS_API_KEY, META_API_KEY, OPENROUTER_API_KEY, ZHIPU_API_KEY / ZAI_API_KEY |
API keys for the native provider's shipped providers, tried before any inline api_key; a value under 8 bytes is skipped with a warning |
XAI_API_KEY |
Grok API key; used when initialize advertises xai.api_key |
GROK_CODE_XAI_API_KEY |
Legacy alias for XAI_API_KEY |
HERDR_ENV, HERDR_SOCKET_PATH, HERDR_PANE_ID |
Read, never set. HERDR_ENV=1 with the other two set means craze is in a herdr pane and reports host status to it; HERDR_ENV is then removed from the agent's environment |
ROOST_SOCKET, ROOST_TAB_ID |
Read, never set. ROOST_SOCKET set with a positive integer ROOST_TAB_ID means craze is in a roost tab and reports host status to it |
ROOST_AGENT_HOOK |
Never read, never set by craze; plays no part in detecting roost. While craze reports host status to roost, it is removed from the agent child's environment, so roost's own agent hooks stay inert inside craze's agent |
If none of --agent-bin, CRAZE_AGENT_BIN (each for the launch's own
provider only) and [agents] names one, Cursor looks for
cursor-agent, then agent, on PATH. Grok looks for grok only. gx looks
for gx only.