CLI & Scriptability API
Updated August 16, 2026
The crystl CLI lets you control crystl from any terminal. List gems, approve permissions, read terminal output, and script multi-agent workflows, all without touching the GUI.
Free vs Guild. The rule is simple: any read is free, any mutation needs Guild (filing feedback,
copy, andcardare exceptions, free on every tier). Free on any crystl:status,gems,shards,screen,screen --command,commands,history,fs,pending,events,wait,docs,token,doctor,render,copy,card,settings/settings get,formation list,markdown list/show,hero list,party list,resurrect list,workbench list,schedule list,sidequest status,quest levels,quest templates list,court status,agent profile show, thelistforms offacetandaskuser, andreport.agent profile setis free too, deliberately: it’s local machine config with no abuse surface. Guild-only control commands includeopen,close,gem select,shard select,shard create,shard close,shard rename,watch,abort,settings set,formation save/apply,gauntlet,resurrect,hero summon,merge,send,approve,deny,quest start/master/propose/templates save/clear/end,sidequest start/end,party create/delete, workbench edits and panel controls (aliasbacklog),facet add/slot/remove,ssh bridge-address,askuser answer,anoint, andcourtconvene/add/dismiss/end. Without a membership they return403 guild_required. This is why/fanout, which spawns and merges workers, is Guild-only. Scheduling is the exception to the read/mutation split:schedule add/schedule cancelare free up to 3 active schedules on every tier. Past the cap,schedule addreturns403 limit_reached(notguild_required), and Guild lifts the cap.
Setup
There usually isn’t one. The CLI ships inside Crystl.app, and crystl installs it for you.
Every launch, crystl checks /usr/local/bin/crystl and does one of three things:
- Installs it silently. If it can write there without asking, it symlinks
/usr/local/bin/crystlto the binary inside the running app, atContents/Helpers/crystl, and says nothing. The same check repairs a link left pointing at a copy of crystl that moved or was replaced by an update. - Offers to install it. If the write needs your admin password, crystl doesn’t ambush you at launch. It offers instead: a small card, one time, with an install crystl button. Click it and you get the one password prompt. Close it with the ✕ and crystl remembers, so the card never comes back. The card waits until first-launch onboarding is finished (so on a brand new install it turns up on your second launch), and it doesn’t appear at all if you’ve turned notifications off. Settings is the manual path either way.
- Leaves it alone. If something else is already installed as
crystl, crystl won’t touch it. See anothercrystlis already installed below.
Once it’s linked, open a new terminal and crystl is there.
Prefer to do it by hand? Point the link at the binary inside your own copy of the app, at Contents/Helpers/crystl. If you installed to /Applications, that’s:
sudo ln -sfh /Applications/Crystl.app/Contents/Helpers/crystl /usr/local/bin/crystl
Help → install command line tool… runs the same install on demand, as does the button in Settings.
The CLI automatically reads the auth token from ~/.crystl-bridge-token. No configuration needed.
Requires Crystl.app to be running. The CLI talks to crystl’s local HTTP bridge.
Settings → general → terminal → CRYSTL CLI
The manual controls live in one section, on the terminal tab of Settings → general:
- Status, in plain language:
installed at /usr/local/bin/crystl, or what’s wrong and what still works. Anything other than a healthy install tints the line yellow. - One button, retitled for the situation: install, uninstall, repair (for a link pointing at an app that isn’t there any more), or how to fix (for someone else’s
crystl). - cli docs →, which opens this page.
When the command isn’t available, the general page also carries a banner across the top so you see it from any of its tabs. It’s the only place crystl flags this. There’s no badge in the sidebar and no dot following you around; the command is optional, and an optional thing that keeps waving at you is nagging.
Another crystl is already installed
crystl never overwrites a crystl it can’t prove it created. If /usr/local/bin/crystl points somewhere else, or is a binary or script of your own, crystl leaves it exactly as it is: no card, no silent replacement, and the install button turns into how to fix, which explains what’s there and hands you the command to remove it.
sudo rm /usr/local/bin/crystl
Then install again. The same rule covers a broken link that was never crystl’s: removing it is your call, so crystl says what it found and stops.
Either way, agents in your local shards already have the command. This is only about terminals outside crystl.
For agents running inside crystl
Agents in local shards always have crystl, installed or not. Every shard gets the bundled binary’s directory prepended to its PATH, so an agent can run crystl copy, crystl notify, or crystl workbench on a machine where nobody ever installed the symlink. No admin prompt, working on first launch, and it beats a stale /usr/local/bin link from an older build.
The exception is a shard on a remote host. The bundle is a path on your Mac, so exporting it to a shell somewhere else would name a directory that doesn’t exist. Remote agents reach crystl through the reverse-tunnelled bridge and crystl ssh bridge-address instead. See remote SSH sessions.
When the tell agents about the crystl CLI toggle is on (default), crystl upserts a Crystl CLI primer into each gem’s CLAUDE.md and AGENTS.md as the project is opened, so any agent in a crystl shard discovers it can coordinate with sibling gems and shards. It isn’t a stub: the primer runs about 11 KB, roughly 130 lines, and it lands in every gem you open. That’s context your agent reads on every session, so it’s worth knowing the size before you leave it on everywhere.
The section is bracketed with <!-- crystl-cli:begin --> / <!-- crystl-cli:end --> so re-running is idempotent and removal is one delete. If a project has no agent instruction file yet, crystl can seed AGENTS.md and a tiny CLAUDE.md import entrypoint unless that creation toggle is disabled.
Turning the toggle off stops new primers, it doesn’t retract the old ones. Blocks already written stay in the repos that have them, because those files are yours and crystl won’t edit your git history on a settings change. To take them back out, run crystl uninstall --primers, or delete the bracketed section by hand.
Tab completion
Inside a shard, crystl completes itself. Type crystl and press tab for the subcommand list, keep going for sub-subcommands, or start a flag with -- and tab through what’s valid right there. The completion script is generated from the CLI binary every time crystl launches, so it always matches the commands your build actually has: a new command is completable the day it ships.
One requirement: your own zsh setup has to have completion switched on (an autoload -Uz compinit && compinit somewhere in your config, which most completion setups already include). If you’ve never enabled it, crystl stays out of the way rather than switching the completion system on for you, and tab falls back to zsh’s normal file matching.
Terminals outside crystl don’t get this automatically. Generate the script once and drop it in your fpath:
mkdir -p ~/.zsh/completions
crystl --generate-completion-script zsh > ~/.zsh/completions/_crystl
Then make sure ~/.zsh/completions is in your fpath before compinit runs, open a new terminal, and tab away.
Commands
status
Overview of all gems, shards, and running agents. This is the default command. A worker parked on an in-terminal approval prompt shows ⏸ awaiting input next to its gem, so a stalled fan-out is visible at a glance instead of looking idle.
Each running agent is annotated with how long it has been in its current state and when it last produced output (e.g. idle 5s · last output 2s ago). crystl computes these durations itself so the numbers are consistent whether you read them here, on a paired phone, or off the agent_status_changed event. Agents themselves are weak at time math, so the app owns the clock rather than asking the agent.
status also reports memory telemetry: a memory: app N MB · pressure normal header (the crystl app’s resident memory plus the system memory pressure level: normal, warn, or critical), and each shard’s resident memory in both crystl status and crystl shards. An orchestrating agent can check this before fanning out workers and rein things in when pressure isn’t normal: fewer or lighter shards, a --scrollback cap per worker, or scrollback clear after a noisy phase. The same numbers ride on GET /api/v1/gems as memory.app_rss_bytes, memory.pressure, and per-shard memory_rss_bytes.
Beside the memory line, status reports whether each configured open-model connection is answering: local model: <label> <url> reachable (loaded: qwen3-coder · pulled: +5), or unreachable, or checking… while the first probe runs. Ollama slots split loaded (in VRAM now) from pulled (on disk, cold). An auth challenge counts as reachable, since something answered. The probe is cached and time-limited, so it never blocks status.
crystl status
crystl status --json
gems
List all open gems with their shards and agent status.
crystl gems
crystl gems --json
Targeting a gem by name
Almost every command takes --gem <name|id>. Names come from the folder you opened, so two gems can genuinely carry the same name: ~/work/api and ~/clients/acme/api are both api.
When a name matches more than one open gem, the command errors instead of picking one. It prints every match with its name, id, and full path, so you can see which one you meant:
error: gem name "api" matches 2 open gems
api 9f3c1a2e ~/work/api
api 4b71d008 ~/clients/acme/api
retry with --gem <id>
Retry with the id and it resolves exactly. Ids are stable for the life of the gem, so a script that has to survive a duplicate name should target ids from crystl gems --json rather than names.
This used to silently take the first match. That was a quiet way to send work into the wrong repo, so an ambiguous name is now a hard error. A name that matches exactly one gem behaves as it always did.
gem select / shard select
Bring an already-open gem or shard forward in the desktop app. These commands deliberately move your view through the same selection path as clicking the gem or shard. They are Guild control commands.
crystl gem select myapp
crystl shard select opal
crystl shard select auth-refactor --gem myapp
gem select accepts a gem name or ID. shard select accepts a crystal name, ID, or unique display label. If you omit --gem, it uses the selected gem; selecting a shard also brings its owning gem forward. Selecting the item that’s already active succeeds and tells you it was already selected.
| Flag | Purpose |
|---|---|
--gem <name|id> | Gem that owns the shard (defaults to the selected gem; shard select only) |
open
Open a project directory as a new gem.
crystl open ~/Projects/myapp
crystl open .
close
Close a gem by name or UUID. Quote names that contain spaces. Reopening a path that is already a gem focuses the existing one instead of stacking a duplicate.
crystl close myapp
crystl close "Competitor research"
formation
List, save, and apply named window arrangements. formation list is a free read. formation save and formation apply change the app, so they need Guild.
crystl formation list
crystl formation list --json
crystl formation save "client work"
crystl formation save "client work" --force
crystl formation apply "client work"
Saving captures the current window’s open gems and shards. An exact name collision is refused unless you pass --force. Applying replaces the current window arrangement with the saved formation. See formations for what gets captured and restored.
| Command / flag | Purpose |
|---|---|
list [--json] | List saved formations with gem/shard counts and the default marker (free) |
save <name> | Capture the current gem and shard arrangement (Guild) |
save --force | Replace an existing formation with the same name |
apply <name> | Replace the current window with a saved formation (Guild) |
--json | Return the list or action result as JSON |
fs
Browse directories so you know what paths you can open. Defaults to your projects directory. Read-only.
crystl fs # list your projects directory
crystl fs ~/Code # list a specific path
crystl fs --json
shards
List shards in a gem. A shard parked on an in-terminal approval prompt (the agent’s own “Do you want to proceed?” menu, which is not a pending request) is flagged ⏸ awaiting input, so you can spot a fanned-out worker that is silently waiting on you. The same flag streams over crystl events as agent_status_changed.awaitingInput.
crystl shards
crystl shards --gem myapp
Whether a shard actually has an agent
A shard restored after a crystl restart comes back with its name, cwd, and role marker before its agent is running again, and a role marker sitting on an empty shell is worse than an empty slot: nothing tells you that dispatches are going nowhere. So a row says which of those it is.
| Annotation | What it means |
|---|---|
orchestrator / worker of <lead> | The role marker, with an agent running. |
orchestrator (unmanned) | The role marker survived the restart, but no agent is running in the shard yet. |
⏸ no agent — relaunches on dispatch | The shard is holding a deferred relaunch. Send to it and its agent starts on its own conversation first, then takes the message. |
⚠ no agent — relaunch failed: <error> | The relaunch was attempted and no agent came back. The agent’s own error text is quoted when it’s available (Session ID … is already in use tells you what to do about it in a way no generic flag can). |
⚠ no agent — stale role marker | A role marker with no agent and no pending relaunch. |
Annotations join with ·, so a restored lead reads orchestrator (unmanned) · ⏸ no agent — relaunches on dispatch. Treat a ⚠ row as a shard you cannot dispatch into until you deal with it: an orchestrator that keeps sending into a failed relaunch is talking to a bare shell.
send
Send text to a terminal shard. Like typing into the terminal.
crystl send "git status"
crystl send --gem myapp "claude 'fix the tests'"
crystl send --gem myapp --shard opal "ls -la"
crystl send --gem myapp --shard opal "run the migration" --wait
--wait blocks until the dispatch is confirmed delivered, so an orchestrator knows its instruction arrived instead of assuming it did. Delivery counts either way it can land: as a user turn in the target’s transcript, or as an entry in the agent’s own input queue when the message arrives while the agent is mid-turn. That second case is the common one, since a lead dispatching follow-up work is usually talking to a shard that’s already working.
The exit code carries the outcome, and it’s what a script should branch on:
| Exit | Meaning |
|---|---|
0 | Delivered. A transcript user turn or a queue entry matched the sent text. |
3 | No receipt, but the shard was working the whole time. The text is queued behind its current turn. Don’t re-send or the agent gets it twice. |
1 | No receipt from an idle shard. Nothing landed, so re-sending is safe. |
Queue evidence depends on the agent: an agent whose transcript format exposes no queue state simply gives --wait nothing to match, and for those the busy-versus-idle split between 3 and 1 is what carries the information.
abort
Stop the active agent turn without closing its terminal. This is an agent-neutral Guild control command.
crystl abort --shard opal
crystl abort --gem myapp --shard opal
If the shard has a held tool approval for its current agent session, crystl resolves it through that agent’s abort path. Claude reports aborted via pending approval. Codex can only deny at that layer, so crystl denies the tool, sends Escape followed by Ctrl-C, and reports both actions. A stale approval from an older agent session is left alone. With no current held approval, crystl sends Escape followed by Ctrl-C and reports sent interrupt.
There is no --force flag and no confirmation prompt. The shard stays open.
| Flag | Purpose |
|---|---|
--shard <name|id> | Shard to interrupt (required; also accepts a display label) |
--gem <name|id> | Gem that owns the shard (defaults to the selected gem) |
notify
Notify an orchestrator shard with an attributed worker ping. Unlike send (which injects raw text into the target’s input line as if the human typed it), notify carries the sending shard’s identity, so the lead’s chat dock attributes the message to the worker while still waking the orchestrator agent. This is the push half of a fan-out: workers can stay silent and report only when they finish or get blocked, instead of the lead polling them.
crystl notify --done --gem myapp --shard diamond "refactor complete"
crystl notify --blocked --shard diamond "needs the API key"
crystl notify --progress --shard diamond "3 of 5 files migrated"
crystl notify --shard diamond --from opal "FYI: tests green" # untyped ping
The --done / --blocked / --progress flags attach a typed kind to the ping: a machine-checkable completion signal the orchestrator can block on with wait done, instead of parsing “DONE” out of prose. Text is optional when a flag is set. Every notify also broadcasts a worker_notify event on the SSE stream carrying the kind as data.
The sender is auto-detected from the CRYSTL_SHARD env a Crystl shell exports; pass --from to override it. Like send, it’s a control command (Guild-gated over the desktop CLI, free over a direct-LAN phone connection).
| Flag | Purpose |
|---|---|
--shard <name> | Target lead/orchestrator shard (defaults to the gem’s first shard) |
--done / --blocked / --progress | Typed completion signal (waitable via crystl wait done; at most one) |
--gem <name> | Gem name or ID (defaults to the selected gem) |
--from <name> | Sending shard name (defaults to $CRYSTL_SHARD) |
copy
Hand the user something to copy. crystl copy surfaces text in the copy bar, an editable strip docked at the bottom of the shard with a one-click Copy button, so they don’t have to drag-select it out of wrapped terminal lines. Reach for it whenever your output contains something the user needs out of the conversation (a token, a URL, a generated snippet), and especially for commands they’ll run elsewhere, where a hand-retyped flag or path is easy to get wrong. Free on every tier.
crystl copy "npx some-cli --flag value" # text goes to the bar
some-command | crystl copy # or pipe it in
crystl copy "deploy step 1" --label "deploy" # name the tab
crystl copy "second snippet" # appends a 2nd tab (up to 10)
crystl copy "fixed" --tab 1 # overwrite tab 1 instead of appending
crystl copy --gem myapp --shard opal "..." # target a specific shard
The bar holds up to 10 numbered tabs, so an agent emitting several snippets can stage them all: a plain copy appends a tab, --tab <N> overwrites one. The user can edit the text before clicking Copy. The window grows taller to make room, so the terminal keeps its size.
| Flag | Purpose |
|---|---|
--label <title> | Name the tab’s chip |
--tab <N> | Overwrite tab N instead of appending |
--gem <name> / --shard <name> | Target a specific shard (defaults to the selected one) |
card
Show an attributed floating notification card on the desktop and add it to the phone’s notification list. This is the attention-grabber for an agent whose host has no notification-hook integration. Free on every tier.
crystl card "the migration is ready to review"
crystl card "tests passed on all three workers" --title "release check"
card must run inside a crystl shard. The bridge takes the gem and shard identity from that live session, so a caller cannot impersonate a sibling by passing names in the command. The normal notification rules still apply, including the master notifications toggle, desktop auto-dismiss, watched-shard suppression, and deduplication.
A shard may create up to three cards in ten seconds. The fourth returns HTTP 429. Success is silent because the card itself is the confirmation.
| Flag | Purpose |
|---|---|
--title <text> | Add a short title above the card text |
screen
Dump the terminal screen content. See what an agent is doing without switching tabs.
crystl screen
crystl screen --gem myapp
crystl screen --gem myapp --shard opal
crystl screen --gem myapp --shard opal --scrollback 100 # include scrollback lines
An empty result says why instead of handing back a blank screen you have to interpret. There are two cases and no third:
- The shard’s terminal engine reported no readable rows at all, in which case the note points you at that shard’s saved scrollback in
.crystl/history/<shard>-active.log. - The visible screen is genuinely blank while scrollback sits above it, in which case the note gives the line count and tells you to refetch with
--scrollback Nto read it.
Stale saved scrollback is never served as though it were live.
watch
Watch new terminal output for a regular expression without polling screen. Registration is a Guild control action.
crystl watch --gem myapp --shard opal --pattern 'permission denied'
crystl watch --gem myapp --shard opal --pattern 'ready on port [0-9]+' --once
crystl watch --gem myapp --shard opal --pattern 'rate limit' --timeout 60
The bridge matches only newly observed output lines after the watch starts. Continuous mode prints every new match and also emits output_pattern_matched on crystl events. Add --once to deregister and exit after the first match. Killing the CLI removes its held watch immediately.
The timeout defaults to 300 seconds and is capped at 600 seconds.
| Flag | Purpose |
|---|---|
--gem <name|id> | Gem to watch (required) |
--shard <name|id> | Shard to watch (required; also accepts a display label) |
--pattern <regex> | Regular expression matched against newly observed output lines (required) |
--timeout <seconds> | Stop after this many seconds (default 300, maximum 600) |
--once | Exit after the first match instead of streaming matches until timeout |
| Exit | Meaning |
|---|---|
0 | At least one matching line was observed. |
2 | The timeout elapsed without a match. |
1 | The shard closed while the watch was active. |
screenshots
List your recent macOS screenshots, the same ones the screenshot bar shows, as absolute paths an agent can open with its image-read tool. This lets you say “view the screenshot I just took,” “the 5 I just took,” or “the app screenshot from yesterday” and have the agent resolve it to files. Read-only and local: it queries Spotlight directly, so it needs no bridge and works even with Crystl closed. Free on every tier.
crystl screenshots # recent screenshots, newest first
crystl screenshots --last 1 # just the most recent
crystl screenshots --last 5 # the 5 most recent
crystl screenshots --since yesterday --before today # everything from yesterday
crystl screenshots --since 2h # the last two hours
crystl screenshots --type window --last 3 --json # recent window captures, as JSON
The command only reads screenshot metadata (path, time, capture kind, dimensions), never image bytes, and stops scanning as soon as your filter is satisfied, so --last 1 is cheap no matter how many screenshots you have. The agent then reads only the paths it needs.
| Flag | Purpose |
|---|---|
--last <N> | Only the N most recent (after any time/type filter) |
--since <when> | Only shots at/after <when> (inclusive) |
--before <when> | Only shots before <when> (exclusive) |
--type <kind> | Only window, display, or selection captures |
--json | Machine-readable output (path, ISO timestamp, type, size) |
<when> accepts now, today, yesterday, a relative age (30m, 2h, 3d, 1w), or an ISO date (2026-07-19). macOS stores no app name on a screenshot, only the capture kind, so for “the app screenshot,” --type window is the closest pre-filter, and the agent picks the right one by viewing the candidates.
commands and screen —command
List the shell commands indexed by the Prism terminal, or print the complete retained output for one finished command. These are free reads on every tier.
crystl commands
crystl commands --gem myapp --shard opal --last 20
crystl commands --output 17
crystl commands --output last
crystl screen --gem myapp --shard opal --command 17
crystl screen --gem myapp --shard opal --command last
Each row has a monotonic occurrence number, command line, completed or running state, exit status when known, and scroll-invariant output rows. commands --output and screen --command return the same complete output for one finished occurrence. Use an index from commands, or last for the newest command.
Output extraction returns the real output or it fails:
- A running target returns
command_still_running. It never presents output-so-far as final or silently falls back to the previous command. - Output that has fallen beyond the shard’s scrollback cap returns
command_output_evicted. It never returns a retained suffix as though it were complete.
The command index currently requires the Prism terminal surface. An agent process is normally one long shell command, so use history for the agent’s individual turns and tool calls.
| Flag | Purpose |
|---|---|
--gem <name|id> | Gem to inspect (defaults to the selected gem) |
--shard <name|id> | Shard to inspect (defaults to its active shard) |
--last <N>, --limit <N> | List only the last N indexed commands |
--output <index|last> | Print only one completed command’s retained output |
--json | Return the command list or selected output envelope as JSON |
screen --command <index|last> | Return the same selected command output through screen |
files, gitstate, and search
Prism also captures structured file changes, Git state, and exact screen tokens. These free reads default to the selected gem and active shard, accept --gem / --shard, and support --json.
crystl files # file-modification records for the shard
crystl gitstate # Git and worktree state (branch, changes)
crystl search "TODO" # find an exact token in the screen index
| Command | Extra flags |
|---|---|
files | --agent <id> |
gitstate | --agent <id> |
search <token> | --limit <n> (takes an exact <token> argument) |
--agent filters by terminal-core agent ID when a shard has run more than one. These pair well with crystl screen and crystl history for programmatic worker supervision.
history
Print a shard’s structured Claude transcript (past turns and tool calls) so an agent can recover context after a compaction or in a fresh shard. Claude Code shards only; see the history navigator for the GUI.
A session summary (total tokens, tool calls, models used, wall-clock active time) prints on top of every projection, useful for judging a worker’s cost and context before acting on its output.
Bare crystl history prints a navigation index: that summary, a session and generation header, one line per row (sequence, time, kind, tool and status, and a one-line preview), then runnable hints for pulling a row. No bodies, so a long session costs you a screen instead of a scrollback. Skim the index, then fetch only the rows you actually want. If the index isn’t available for that shard, crystl says so on stderr and shows the full transcript instead.
Per-turn model, token usage, and duration aren’t on the index lines. They come back with the rows you fetch (--row / --ref), with --all, and in --json.
crystl history # navigation index for the active shard
crystl history --gem myapp --shard opal
crystl history --row 47 # fetch row 47 in full
crystl history --rows 47,51 # several sequences at once
crystl history --ref r1_ab3f... # fetch by durable rowRef
crystl history --all # the whole transcript, every body
crystl history --json # unchanged: today's full response
--row N is a convenience lookup in the shard’s current session. A rowRef from the index is the durable form: it stays correct after a session switch, and a stale one fails with row_not_found rather than resolving to whichever row happens to reuse the number. Either way, fetching a tool call brings its paired result along, since that’s the useful unit to read.
Index filters narrow the listing. A tool call and its result count as one unit, so a filter or a --limit never splits a pair. --tool and --status are index-only, so ask for the index explicitly when you use them:
crystl history --index --tool Bash # only Bash calls, with their results
crystl history --index --status error # ok | error | pending
crystl history --tools-only # only tool calls and results
crystl history --limit 20 # last 20 units
--all means the whole transcript, so it refuses to be narrowed: pass --tool, --status, --tools-only, or --limit alongside it and crystl tells you to pick one instead of quietly returning less than everything. Filter with --index, then read a row with --row <seq>.
--json still returns the full response (per-turn usage, model, stopReason, cwd, gitBranch), so nothing machine-readable changed shape. Ask for --index --json when you want the index envelope.
crystl history also fronts the timeline, crystl’s own searchable record of what every shard’s agent said and every command that ran, across gems, sessions, and SSH, past and present (see history navigator for what’s stored and how to delete it):
crystl history search "auth timeout" # full-text search across ALL shards, open or closed
crystl history search "migrate" --gem myapp # scope to a gem (name, id, or directory)
crystl history search "npm test" --shard opal # scope to a single shard
crystl history metrics # per-model token rollups + totals
crystl history metrics --gem myapp --days 7
crystl history clear --older-than 90 # scoped deletion (index only — never transcripts or files)
crystl history clear --gem myapp
crystl history compact # checkpoint + compact the database, reports before/after size
crystl history show "flux fix" --shard opal # AGENTS: open the search palette in the user's window,
# pre-filled — show them a moment instead of quoting it
SQLite doesn’t shrink a file when rows are deleted, so a clear alone frees nothing on disk. crystl history compact does the reclaiming: it checkpoints and vacuums the timeline database and prints the before and after sizes. It changes no identity, so every rowRef you’re holding still works. The same button lives in Settings → Storage, next to the database’s current size.
search and metrics are free read commands. clear, compact, and show are control commands (Guild over the desktop CLI); clear refuses to run without a scope, and compact runs only from the local app or CLI. The same surface is on the bridge API under /api/v1/timeline/*. Users get the same search in-app with Cmd+Shift+F.
pending
List pending permission requests.
crystl pending
approve / deny
Approve or deny a pending permission request by ID.
crystl pending # see request IDs
crystl approve 1 # approve request #1
crystl deny 2 # deny request #2
askuser
List and answer the questions agents raise through AskUserQuestion, the same prompts the floating question panel shows, answerable from any shell. Listing is free; answer resolves the prompt, so it’s Guild-gated.
crystl askuser # list open questions (default subcommand)
crystl askuser list --json
crystl askuser answer <id> "2 weeks" # answer by text, or by an option label
For a multi-question prompt, the answer applies to every question in the series.
shard create
Create a new shard inside an existing gem. Useful for orchestrator agents that want to fan work out into parallel shards.
crystl shard create --gem myapp # shared cwd
crystl shard create --gem myapp --isolated # git worktree
crystl shard create --gem myapp --isolated --base release/2.0 # fork the worktree from a branch/tag/SHA
crystl shard create --gem myapp --crystal-name opal # pick the name
crystl shard create --gem myapp --prompt "add tests" # launch an agent on a task
crystl shard create --gem myapp --agent codex --prompt "add tests" # spawn a different agent
crystl shard create --gem myapp -c "npm test" # run a raw shell command
crystl shard create --gem myapp --hero wizard --isolated # summon a solo hero
crystl shard create --gem myapp --hero seeker --prompt "audit auth" # hero with an opening task
crystl shard create --gem myapp --prompt "add tests" --scrollback 500 # cap this shard's scrollback
To spawn a worker on a task, use --prompt (and optionally --agent/--model). crystl launches the agent and sends the task as its first message. This is agent-neutral: a Claude orchestrator can spawn a Codex worker, or vice versa. Reach for -c only when you want a raw shell command rather than an agent; the raw command path is mutually exclusive with agent and hero launches.
| Flag | Purpose |
|---|---|
--isolated | Back the shard with a git worktree so live in-repo edits use a separate working directory |
--base <ref> | Fork the isolated shard’s worktree from a specific branch, tag, or SHA instead of the gem’s integration target. Requires --isolated. The CLI equivalent of the worktree menu’s new isolated shard from branch… |
--crystal-name <name> | Pick the shard’s crystal name (auto-assigned otherwise) |
--prompt, --task <text> | Task sent to the launched agent as its first message |
--agent <name> | Agent to launch: claude, codex, or antigravity (the sunset gemini still resolves for back-compat). Omit to auto-resolve (see below) |
--model <id> | Pin the agent’s model/version (e.g. gpt-5.1-codex-max) |
-c, --command <cmd> | Run a raw shell command once the shard starts (mutually exclusive with --agent, --prompt, and --hero) |
--approval <mode> | Pre-set the shard’s approval mode: manual, smart, all, or default |
--quest | Install the fanout comms channel so a worker can report back over quest_msg (see Fanout) |
--hero <name> | Summon a solo hero shard by catalog name (e.g. wizard). Applies its agent, model, and persona. May pair with --prompt/--task for the hero’s opening task and --model for a per-summon model override; mutually exclusive with --agent and -c. Requires quest enabled. See hero below for discovery |
--scrollback <N> | Cap this shard’s terminal scrollback at N rows (clamped to 200–10,000). Omit to follow the global scrollback setting (Settings → general → terminal → TERMINAL RENDERING); quest party members follow their own quest scrollback default instead, and an explicit --scrollback wins over both. Not preserved across shard restore or resurrect; a restored shard uses the global setting |
--json | Emit the created shard as JSON |
Spawned agents inherit the shard environment, so a worker launched with --agent aider, --agent opencode, or --agent goose finds your open-model connection without any extra flags. Add --local <label> to name an endpoint explicitly: it resolves the right flags per agent (--oss --local-provider for codex, the ANTHROPIC_BASE_URL env for claude) and errors before any shard opens if the slot is missing, disabled, or the wrong kind.
For big fan-outs, a small --scrollback cap (e.g. 500) keeps workers light on memory. Agent messages and commands stay in history search regardless of scrollback.
Agent resolution. When --agent is omitted, crystl resolves the spawned agent in order: the gem’s per-gem Spawn Agent override (Gem Settings → general) → the agent already detected running in the gem → the global Default Spawn Agent (Settings → agents → defaults). That field names an AGENT only; what each size launches for it is set separately, on the agents tab. If none of those resolve, the command errors rather than assuming an agent: pass --agent, run an agent in the gem first, or set a default.
shard close
Close a shard in a gem by name. This is a mutation, so it’s Guild-gated like shard create, merge, and send.
crystl shard close --gem myapp --shard opal
crystl shard close --shard opal # --gem defaults to the selected gem
crystl shard close --gem myapp --shard opal --json
Closing an isolated shard is non-destructive to committed work: crystl removes the git worktree but preserves any commits or uncommitted changes on the shard’s branch, so you can reopen the branch later from the branches button. It refuses to close the last remaining shard in a gem, since a gem always needs at least one. Eligible local shards are also recorded in the resurrect list before they close; remote shards and quest party members are skipped.
shard rename
Give a shard a short display label: a friendly name shown on its pill, in the window title, and in crystl shards / status (and on the phone). Useful for labelling the workers you fan out (parser, tests, api). This is a mutation, so it’s Guild-gated like shard create.
crystl shard rename --gem myapp --shard opal --name "auth-refactor" # set a label
crystl shard rename --shard auth-refactor --name "backend" # address by the current label
crystl shard rename --gem myapp --shard opal --name "" # clear it → back to the crystal name
The label is display-only: it never changes the shard’s crystal name, git branch, or worktree, so a rename can’t break identity or addressing. It’s normalized to a short canonical form (lowercased, ≤14 characters, control characters stripped) so it reads the same everywhere it’s shown or typed. After renaming, you can address the shard by its new label, its crystal name, or its id. All three are case-insensitive; --shard matches identity (name/id) first, then a label, and errors if a label is ambiguous. An empty --name clears the label.
Rename is refused for shards whose name isn’t yours to change: Quest party members (their name comes from the hero/role) and side-quest participants (the name is the live message routing key). Rename a solo hero instead, or edit the hero in Settings → Heroes.
scrollback clear
Clear a shard’s screen and retained scrollback, the same as pressing Cmd+K in that shard. This actually frees the buffer’s memory, so it’s a RAM lever alongside shard create --scrollback <N> and shard close: after a worker’s long, noisy phase (a huge build log, a test flood), clear its scrollback and its resident memory drops. The shard’s agent turns and commands stay in history search regardless, so nothing you’d want to find again is lost.
crystl scrollback clear # the selected shard
crystl scrollback clear --gem myapp --shard opal # a specific shard
Free on every tier, so agents can use it freely (the HTTP route is POST /api/v1/gems/:id/shards/:sid/clear-scrollback).
resurrect
Undo-close for recently closed local shards: bring back a shard you just closed in a gem. resurrect list is read-only and free; the resurrect action itself is a mutation, so it’s Guild-gated like shard create, merge, and send.
crystl resurrect list --gem myapp # what can be brought back
crystl resurrect list --gem myapp --json
crystl resurrect --gem myapp # bring back the most recently closed shard
crystl resurrect --gem myapp --shard opal # bring back a specific one by name
crystl resurrect --gem myapp --shard opal --json
Without --shard, resurrect restores the most recent entry. It restores the shard’s name and color, and makes a best-effort attempt to restore its isolation (git worktree) and agent session. Quest shards are excluded: they aren’t tracked for resurrection.
| Command | Free? | Purpose |
|---|---|---|
resurrect list [--gem <name|id>] | Free | List recently closed shards that can be brought back |
resurrect [--gem <name|id>] [--shard <name>] | Guild | Bring back a closed shard (most recent if --shard omitted) |
merge
Merge an isolated shard’s git worktree branch back into the gem’s integration target (main by default, configurable per gem). This is the integration step /fanout runs when a worker finishes. It routes through crystl’s own merge machinery, so worker agents never merge their own branches.
crystl merge --gem myapp --shard opal
crystl merge --gem myapp --shard opal --json
Returns a structured result: success (with a commit count), nothingToMerge, dirty, conflicts, failed, or notIsolated. The result includes the branch it merged into. Conflicts and dirty trees are left for you to resolve from the shard tab’s merge badge; merge never forces.
worktree target
View or set a gem’s integration target: the branch its isolated shards and sealed quests merge back into. By default that’s the repository’s default branch (main), but you can point a gem at a release or feature branch so a fleet of workers integrates there instead. A shard also forks its worktree from the target, so its commits stay relative to where they will merge. Both crystl merge and sealed quests honor it.
crystl worktree target # show the resolved target for the selected gem
crystl worktree target --gem myapp
crystl worktree target set branch:release/2.0 --gem myapp # per-gem override to a specific branch
crystl worktree target set current --gem myapp # the branch checked out when a shard starts
crystl worktree target set repo-default --gem myapp # back to the repo default branch
crystl worktree target set branch:release/2.0 --global # the global default new gems inherit
crystl worktree target reset --gem myapp # clear the override, inherit the global default
crystl worktree target quest-branch on --gem myapp # a fresh branch per sealed quest
show reads the resolved target (the per-gem override if set, otherwise the global default, otherwise the repo default) and is free. The set, reset, and quest-branch mutations are Guild-gated.
| Argument | Purpose |
|---|---|
repo-default | The repository’s default branch (main, master, and so on) |
current | Whatever branch is checked out when the shard starts |
branch:NAME | A specific branch |
--gem <name> | Target gem (defaults to the selected one) |
--global | Set the global default that new gems inherit, not a per-gem override |
You can also set this in the app: Settings → Worktrees for the global default, or a gem’s settings worktrees tab for a per-gem override. See isolated sessions for the concept.
hero
Discover and summon solo hero shards, a single catalog specialist (its own name, role, persona, agent, and model) started outside a quest party. list is free; summon is a mutation, so it’s Guild-gated, and (like the desktop swords button) it requires quest enabled.
crystl hero list # built-in catalog + this gem's local heroes
crystl hero list --gem myapp # local heroes for a specific gem
crystl hero list --json
crystl hero summon wizard # summon into the selected gem
crystl hero summon wizard --gem myapp --isolated # in a git worktree
crystl hero summon seeker --gem myapp --prompt "review the auth diff"
crystl hero summon ranger --gem myapp --model gpt-5.1-codex-max
hero list shows the names you can pass to summon (built-ins like wizard, warrior, ranger, plus any project-local heroes in .crystl/heroes/). Party-only roles such as healer are excluded from solo summon. crystl hero summon <name> is a focused alias for crystl shard create --hero <name>, with the same behaviour and the same gating. Add --prompt / --task to give the hero its opening assignment immediately, and --model to override the hero’s configured model for that summon only.
If you’ve customized a built-in hero in Settings (model, persona, stats, agent), solo summon uses your edited version, the same customizations a quest party would apply. Project-local heroes are taken as-is from their .crystl/heroes/ definition.
| Command | Free? | Purpose |
|---|---|---|
hero list [--gem <name>] | Free | List summonable heroes (name · role · agent) |
hero summon <name> [--gem <name>] [--isolated] [--prompt <task>] [--model <id>] | Guild | Summon a solo hero shard |
gauntlet
Launch a release-readiness crew as solo hero shards: two Seekers, one Monk, and one Scribe. The default mode is review-only and isolated, so each hero gets its own git worktree and reports findings without editing files. Pass --fix when you want the isolated crew to make focused fixes.
crystl gauntlet "final release readiness"
crystl gauntlet --check "swift test" --check "npm test"
crystl gauntlet --fix --prompt "stabilize the 2.0 release"
crystl gauntlet --profile release --dry-run --json
The Scribe is instructed to watch the other gauntlet shards, consolidate a PASS/WARN/FAIL/BLOCKED verdict, and add concrete follow-ups to the Workbench when useful.
| Flag | Purpose |
|---|---|
--gem <name> | Target gem (defaults to the selected one) |
--prompt, --task <text> | Goal or scope for this gauntlet |
--check <cmd> | Check command to ask the Monk to run; repeat for multiple checks |
--fix | Allow isolated shards to make focused fixes instead of only reporting findings |
--shared | Use shared shards instead of isolated worktrees |
--profile <name> | Load defaults from .crystl/gauntlet.json |
--dry-run | Print the planned crew and prompts without creating shards |
Optional project profiles live in .crystl/gauntlet.json:
{
"profiles": {
"release": {
"prompt": "Final release readiness pass",
"checks": ["swift test", "npm test"],
"party": ["seeker", "seeker", "monk", "scribe"]
}
}
}
anoint
Confer the orchestrator role on a shard. The anointed shard gets the castle marker in the shard bar, the workers tab in its chat dock, isOrchestrator over the bridge, and the orchestrate briefing: instructions to coordinate other agents rather than write code itself. This is the CLI parity for the desktop anoint facet, and both land in the same policy, so the click path and the CLI can never disagree about who the lead is. It’s a mutation, so it’s Guild-gated.
crystl anoint # the selected shard in the selected gem
crystl anoint --gem myapp --shard opal
crystl anoint --gem myapp --shard opal --no-deliver
crystl anoint --json
By default the briefing is delivered to the agent running in that shard. Pass --no-deliver to mark the role without sending it, for an agent that is already briefed. Anointing a shard that is already the orchestrator is a no-op, reported as already.
Two refusals are worth knowing, both 409:
no_agent_runningmeans the shard has no agent, so the briefing would land in the shell instead. Start the agent, then anoint again, or use--no-deliverto mark the role only.agent_launchingmeans the shard was created moments ago and its agent is still starting. Retry shortly.courthandles this one for you when it summons a hand.
| Flag | Purpose |
|---|---|
--gem <name> | Gem name or ID (defaults to the selected gem) |
--shard <name> | Shard name, id, or display label (defaults to the selected shard) |
--no-deliver | Mark the role without sending the orchestrate briefing |
--json | Output as JSON |
court
Convene a standing court: a hierarchy you talk to through one front door.
- crown is the shard you talk to. It owns the conversation and owns no work, so it is always free to answer. A crystl shard is single-threaded from your side, so anything you type while the agent is mid-turn queues behind it. “Always available” therefore means “always idle”, and the only way to stay idle is to hold no tasks.
- hand is the crown’s orchestrator, an anointed shard. The crown hands work to it; it spawns workers, keeps them busy, and merges their branches.
- officers are standing seats under the hand, each with a
--charge: a remit that persists, where a worker’s task would end.
Nothing but you may send to a crown. The crown has exactly one inbound edge, and it’s the user. An agent’s send or notify into a crowned shard is refused, and the refusal points it at the hand instead. That’s the whole reason the crown works: the moment a finished worker can wake it, your conversation gets interrupted and the crown is just another busy agent. The crown learns everything else by reading live state, never by being reported to. Your own input is untouched on every surface you actually use: typing in the terminal, the chat dock, the phone, and crystl send from an ordinary shell all reach the crown normally.
Court is deliberately distinct from its neighbours: party is a saved roster, quest is peers in a shared chat, gauntlet is a one-shot audit crew. Court is the only one that persists and the only one with a single front door.
crystl court "ship the 3.0 release" # convene (the default subcommand)
crystl court convene "ship the 3.0 release" --with scribe --with sentinel
crystl court --dry-run # print the plan, seat nobody
crystl court status # who is seated, and what each is doing
crystl court add scribe --charge "keep the docs current"
crystl court dismiss scribe
crystl court end
crystl court end --keep-staff # take the crown off, leave the staff running
Convening prefers to crown the shard you are already in, because crowning in place transfers no context: you are already talking to it. If no shard can be crowned in place, a crown hero is summoned instead, and your goal rides along as its opening task. An existing hand is reused rather than duplicated, since a second one would give the crown two places to dispatch to. Each officer is briefed to report to the hand by its real shard name.
crystl court status is composed entirely from a read, so it’s free on every tier. Convene, add, dismiss, and end create or close shards, so they’re Guild-gated.
court end dismisses the officers, then the hand (in that order, so officers are never left addressing a dead shard), and finally uncrowns the crown. The crown is usually your own shard, so it is never closed, only uncrowned: you get your ordinary shard back. At a terminal it names the shards it is about to close and asks first. A non-interactive caller (no tty, --json, or --force) proceeds without asking.
| Command | Free? | Purpose |
|---|---|---|
court [convene] "<goal>" [--with <hero>] [--profile <name>] [--dry-run] | Guild | Crown a lead and staff it with a hand and officers |
court status [--gem <name>] | Free | Show the seated crown, hand, officers, and worker count |
court add <hero> [--charge <remit>] | Guild | Seat an officer that reports to the hand |
court dismiss <hero|shard> | Guild | Close a seated officer |
court end [--keep-staff] [--force] | Guild | Dissolve the court |
Every subcommand takes --gem <name> (defaulting to the selected gem) and --json.
Optional project profiles live in .crystl/court.json, mirroring .crystl/gauntlet.json. A --with flag is added on top of the profile’s officers rather than replacing them: the profile is your standing court, the flags are what you want for this convene.
{
"profiles": {
"default": {
"goal": "keep the release train moving",
"officers": [
{ "hero": "scribe", "charge": "keep the docs current" }
]
}
}
}
agent profile
Show or teach the map from a model size (small, standard, large) to the complete start command your agent should run, or a bare model id. Heroes ask for a size rather than a model id so that one hero catalog works on any agent, and crystl ships no size-to-model table: ids churn, there are more agent CLIs every month, and a baked-in table would be stale on arrival. So the map is learned, from you or from the agent itself.
Both show and set are free on every tier. It’s local machine config with no abuse surface, and gating it would leave a free account’s heroes on the wrong model with no way to say otherwise.
crystl agent profile # defaults to `show`
crystl agent profile show --agent codex
crystl agent profile show --json
crystl agent profile set --agent codex \
--small gpt-5.6-luna --standard gpt-5.6-sol --large gpt-5.6-terra
crystl agent profile set --agent claude \
--large 'claude --model opus --effort high'
crystl agent profile set --agent 'npx my-agent --fast' \
--small model-mini --standard model-pro --large model-max \
--model-via '--model {model}'
Each size accepts a bare model-id shorthand or a complete exact start command. The command is the primary form: reach for it whenever a size also needs effort, a path, leading environment assignments, or other launch arguments. Paths, environment, and arguments are preserved exactly. A bare id keeps working, composed into a command through that agent’s known convention and rewritten as the full command on the profile’s next edit. Any model value may name an open-model connection with model@endpoint, including inside a command’s own flag: 'claude --model glm-4.6@glm'. Complex shell grammar such as pipelines, redirects, substitutions, and command lists must live in a wrapper script. Pass at least one size or --model-via. Only the values you pass are changed, so you can fill one in now and the rest later. An empty string clears that size or template.
Profiles use the normalized launch command as their identity. A bare seeded command resolves to its native profile, so recognized aliases such as agy and antigravity share one row. A user-owned path, arguments, leading environment assignments, wrapper, or full custom command gets a distinct exact profile. Exact profiles can inherit untouched native settings when crystl recognizes the executable, but edits remain scoped to that full command.
crystl knows how its native integrations receive a model. For any other command using bare model ids, set --model-via to a template containing exactly one {model}: '-m {model}', '--model {model}', or 'MODEL={model}'. Argument templates are appended to the full command; an environment assignment is prefixed. A complete command in a size field needs no template because it already says exactly what to run. crystl never guesses an unknown CLI’s convention. An unmapped size is not an error: crystl passes no model selection and the agent launches on its configured default. The same profiles are editable in Settings → agents → agents, and the three agent-neutral fallback commands live one tab over under defaults.
| Command | Free? | Purpose |
|---|---|---|
agent profile [show] [--agent <command>] | Free | Show learned size launch values and delivery profiles |
agent profile set --agent <command> [--small <value>] [--standard <value>] [--large <value>] [--model-via <template>] | Free | Set a complete start command, or a bare model id, for each size |
--model-via <template> | Free | Set how a custom command receives a model; use exactly one {model} |
endpoint
Configure the model endpoints your agents run against: a local server on your own hardware, or a hosted provider that speaks one of three API formats. This is the whole Settings → providers & keys form from the terminal, so your agent can do the research part — which API format a provider speaks, and what its models are called — instead of leaving it to you.
An endpoint that names a CLI becomes an agent you can type. Bind claude to an endpoint called glm and claude@glm runs in any new shard, alongside hosted claude rather than instead of it. Its three sizes are the same record crystl agent list shows — one set of sizes, editable from either place.
All three subcommands are free on every tier, for the same reason agent profile is: describing a provider you already pay for is local configuration, not control.
crystl endpoint list
crystl endpoint list --json
crystl endpoint set --label glm --kind anthropic \
--url https://api.z.ai/api/anthropic \
--agent claude --standard glm-4.6
crystl endpoint set --label glm --small glm-4.5-air --large glm-5-turbo
crystl endpoint set --label box --kind ollama --url http://100.64.0.3:11434 \
--on-host-url http://localhost:11434 --context 64000 --agent codex
pbpaste | crystl endpoint set --label glm --key-stdin
crystl endpoint remove glm
--kind is the API format the provider speaks, not where it runs: ollama for Ollama’s own API, openai for anything OpenAI-compatible (LM Studio, vLLM, llama.cpp), and anthropic for anything speaking the Anthropic Messages API — including hosted providers such as GLM, Kimi and DeepSeek. It is required when creating an endpoint; on an update, absent means unchanged, so setting one field never blanks another.
Keys never go on the command line. There is no --key flag: a command line lands in scrollback and in your shell history, where a credential outlives the moment it was needed. --key-stdin reads it from a pipe and stores it in the macOS Keychain, and no route reads it back — endpoint list reports only whether a key is stored. --clear-key removes one.
--context is the endpoint’s real context window in tokens. It is worth setting: Ollama defaults to 4096, which is smaller than an agent’s standing prompt, and nothing can discover the number from a cold server. crystl uses it to warn you before you start work an endpoint is too small for.
--enable makes the endpoint the ambient default for its format’s agents, so plain claude (or codex, or aider/opencode/goose) runs there in every new shard. It is off by default and does not gate calling the endpoint by name — claude@glm works either way. --disable turns it back off.
Enabling one is worth a moment’s thought on an anthropic endpoint, because plain claude then takes both its address and its key: the session can use only the models that endpoint serves, and the work bills against that key rather than a signed-in subscription. Ask it for a hosted model name the provider does not have and the session fails rather than falling back. That bites hardest when the endpoint runs on your own machine, where none of the hosted names exist, so crystl endpoint set tells you when you enable that combination. Calling the endpoint by name instead — claude@glm — leaves plain claude alone.
remove deletes the endpoint, its stored key, and its callable agent together, so no half of the pair is left behind.
| Command or flag | Free? | Purpose |
|---|---|---|
endpoint [list] [--json] | Free | List endpoints, their agents, sizes, and whether a key is stored |
endpoint set --label <name> [--kind <format>] [--url <url>] | Free | Create an endpoint, or merge changes into one |
--agent <command> | Free | Bind the CLI that calls it; empty string unbinds |
--small / --standard / --large <model> | Free | Set the bound agent’s sizes |
--on-host-url <url> | Free | Address as seen from a shard running on that machine |
--context <tokens> | Free | The endpoint’s real context window |
--key-stdin / --clear-key | Free | Store a credential from stdin, or remove the stored one |
--enable / --disable | Free | Make it the ambient default for its format’s agents, or stop |
endpoint remove <label> | Free | Remove the endpoint, its key, and its agent |
schedule
Schedule a fresh agent shard to start later. A schedule targets a specific gem and carries the same launch choices as shard create: task prompt, agent, model, approval mode, and optional isolated worktree. schedule without a subcommand defaults to schedule list.
crystl schedule add --gem myapp --at "2026-07-18 09:00" --prompt "review the overnight test failures"
crystl schedule add --gem myapp --at "09:00" --every daily --prompt "triage the morning inbox"
crystl schedule add --gem myapp --at "9:00 AM" --repeat weekdays --timezone America/Los_Angeles --prompt "check release health"
crystl schedule add --gem myapp --at "2026-07-18T16:00:00Z" --agent codex --model gpt-5.1-codex-max --approval smart --isolated --prompt "prepare the release branch"
crystl schedule list
crystl schedule cancel 7A4F2C91
One-time --at values accept a local date and time (YYYY-MM-DD HH:mm) or an ISO-8601 timestamp. Recurring schedules use a clock time (09:00 or 9:00 AM) with --repeat or its alias --every; valid recurrence values are once, daily, and weekdays. The time zone defaults to the Mac’s current zone and accepts an IANA identifier such as America/Los_Angeles. Recurring schedules preserve that local wall-clock time across daylight-saving changes.
Crystl owns and persists schedules as personal machine state. Crystl.app must be running and the target gem must be open when a task fires. A due task is checked again after wake or app restart, so an overdue occurrence catches up once rather than launching duplicates. If the target gem is closed, that occurrence is recorded as failed; a one-time schedule stays disabled, while a recurring schedule advances to its next occurrence. Completed and failed history remains visible in schedule list until you cancel it.
| Command or flag | Purpose |
|---|---|
schedule [list] [--json] | List active schedules and retained run history (free) |
schedule add --gem <name|id> --at <time> --prompt <task> | Create a schedule for the named gem (free up to 3; Guild lifts the cap) |
--repeat, --every once|daily|weekdays | Set recurrence (defaults to once) |
--timezone <IANA> | Interpret local times in this time zone |
--agent claude|codex|gemini | Choose the agent; omitted uses gem detection/settings |
--model <id> | Pin a model/version for the new shard |
--approval manual|smart|all|default | Set the scheduled shard’s approval mode |
--isolated | Create the shard in an isolated git worktree |
schedule cancel <id> | Cancel and delete by full ID or unique ID prefix (Guild; aliases delete, remove) |
party
Manage Quest parties: a named roster of heroes you launch together. Build a party here, then launch it with crystl quest start --party <name>. list is free; create and delete are mutations, so they’re Guild-gated.
crystl party list # parties available to the selected gem
crystl party list --gem myapp --json
crystl party create "Design & Dev" --heroes wizard,ranger,warrior
crystl party create "Ship It" --heroes wizard,ranger --gem myapp --local
crystl party delete "Design & Dev" --gem myapp
party create seats the named heroes by identity (precedence project-local > custom > built-in, the same resolution a quest party uses). Pass --local to commit the party to <gem>/.crystl/parties/*.json so it ships with the repo. Everyone who clones the gem gets it. Without --local the party is global to your machine.
| Command | Free? | Purpose |
|---|---|---|
party list [--gem <name>] | Free | List parties available to a gem |
party create <name> --heroes <a,b,c> [--gem <name>] [--local] | Guild | Seat a new party (--local commits it to the repo) |
party delete <name> [--gem <name>] | Guild | Remove a party |
quest
Launch, plan, clear, or end a crystl quest from the command line. quest start is the CLI parity for the desktop “begin quest” panel: it summons the gem’s party so a fleet of role-played agents start working in a shared chat room. quest clear archives the chat. quest end stops the active quest and turns heroes back into normal solo shards. The planning verbs (master, propose, templates save) drive the quest master flow. These are mutations, so they’re Guild-gated; quest levels and quest templates list are read-only and free.
crystl quest start # launch the gem's active party
crystl quest start --gem myapp --party "Design & Dev"
crystl quest start --sealed # each hero gets its own git worktree
crystl quest start --sealed --target-branch release/2.0 # sealed, merging into a chosen branch
crystl quest start --fresh # archive prior chat, keep an authored plan
crystl quest start --fresh --emergent # fresh start with no plan at all
crystl quest start --template release-arc # launch on a saved questline template
crystl quest start --size small # launch every hero small, to explore cheaply
crystl quest start --size large # launch every hero large, for a hard problem
crystl quest clear # archive the gem's quest chat history
crystl quest clear --gem myapp
crystl quest end --gem myapp
| Flag | Purpose |
|---|---|
--gem <name> | Target gem (defaults to the selected one) |
--party <name> | Which party to summon (defaults to the gem’s active/first party) |
--sealed | Sealed quest, where each hero runs in its own git worktree |
--target-branch <name> | For a sealed quest, the branch its worktrees merge into (defaults to the gem’s integration target) |
--fresh | Archive any prior quest chat before launching. A plan the quest master has already authored carries into the new arc |
--emergent | Start with no plan. The party finds its own levels. Pair with --fresh to drop an authored plan as well. Can’t be combined with --template |
--template <slug> | Launch on a saved questline template. Implies a fresh arc: any in-progress quest is archived first, and the template file is left untouched |
--size <small|standard|large> | The model size every hero launches at, overriding each hero’s own size. Omit it and each hero keeps its own. A hero with a model pinned launches with that pin either way (no built-in carries one), and a hero with no size runs your agent’s own default |
--size is the CLI twin of the begin-quest panel’s model size dropdown, so one flag runs the whole party small or large without editing a hero. See model sizes for what each size means for the agent you run, and starting a quest for how it resolves per member.
quest clear archives the chat aside (recoverably) and leaves the root quest docs (QUEST-LOG.md, DECISIONS.md, HANDOFF.md) untouched. quest end ends the active quest without deleting the shards.
quest master
Summon the gem’s quest master headlessly: a hidden level-authoring runtime you plan a quest with. It’s idempotent (one per gem), and summonable before a quest even exists. After summoning, open plan quest in the app to talk to it. Guild-gated.
crystl quest master # summon into the selected gem
crystl quest master --gem myapp
crystl quest master --model opus # override its model for this summon
Level authoring is reasoning-heavy and low-volume, so --model is a good place to trade up to a higher-reasoning model.
quest propose
The Quest Master’s prepare-then-commit step: pre-fill the begin-quest panel with a suggested roster and open-or-sealed choice, without launching. The panel’s own begin quest button stays the human’s explicit commit, so this never spawns agents. Guild-gated.
crystl quest propose --heroes wizard,ranger,rogue # pre-fill, open mode
crystl quest propose --heroes wizard,rogue --isolation sealed
| Flag | Purpose |
|---|---|
--gem <name> | Target gem (defaults to the selected one) |
--heroes <a,b,c> | Comma-separated hero names to field (from the merged catalog). Required |
--isolation <open|sealed> | Isolation for the proposed quest (defaults to open) |
crystl resolves the names against the gem’s full merged hero catalog and silently drops any that aren’t summonable adventurers, so the Quest Master can’t propose itself. If nothing resolves, the command errors rather than opening an empty panel.
quest templates
Save the gem’s authored quest plan as a committable questline template, or list the templates a gem can launch. list is read-only and free; save is a mutation, so it’s Guild-gated.
crystl quest templates list # launchable questlines for the gem
crystl quest templates list --gem myapp
crystl quest templates save "release arc" # save the authored plan as a template
crystl quest templates save "release arc" --force # overwrite an existing slug
list shows every launchable questline source, kind-marked: committable markdown templates (.crystl/questlines/<slug>.md), local questline JSON, and shipped questlines. save copies the gem’s authored quest-plan.md verbatim to .crystl/questlines/<slug>.md (the name is slugified for the filename) and carves the folder out of the .crystl gitignore so it commits with the repo. Launch a saved template with crystl quest start --template <slug> or from the begin-quest panel’s questline dropdown.
quest levels
Show the gem’s questline: its levels and where the party is right now. Read-only and free.
crystl quest levels # the current questline and progress
crystl quest levels --gem myapp
crystl quest levels --briefs # include each level's full brief, not just the title
sidequest
CLI parity for a side quest: bridge two shards you already have open into an ad-hoc two-agent chat, without summoning a party. start pairs two shards by name; status shows the active pairing; end tears it down. The two shards can live in different gems and even on different hosts. start/end are mutations (Guild-gated over the desktop CLI); status is free.
crystl sidequest status # show the active pairing (or "none")
crystl sidequest start --gem myapp --a diamond --b jade # same-gem
crystl sidequest start --a crystl-web/garnet --b crystl/citrine # fully cross-gem
crystl sidequest start --gem crystl-web --a garnet --b crystl/diamond # --gem as default scope
crystl sidequest end # end the active side quest
crystl sidequest end --gem myapp
--gem is optional. It sets a default scope for bare shard names, while --a and --b each accept a qualified <gem>/<shard> form so an endpoint can name its own gem, that is how a side quest pairs shards across gems. A bare shard name with no --gem resolves globally across every open gem, and must be unique or crystl returns an ambiguous; pass gem error.
| Flag | Purpose |
|---|---|
--gem <name> | Default gem scope (name or ID) for bare shard names. Optional; endpoints can override it with a qualified <gem>/<shard> |
--a <shard> | First shard to pair, bare or <gem>/<shard> (start only) |
--b <shard> | Second shard to pair, bare or <gem>/<shard> (start only) |
events
Stream live bridge events as JSON lines to stdout. One event per line, pipe-friendly:
crystl events # every type
crystl events --type pending_changed # filter
crystl events --type pending_changed,askuser_changed,notification,agent_status_changed,backlog_changed --limit 5
Frame envelope: {"type": "<event>", "data": <payload>}. Common event types include pending_changed, askuser_changed, notification, agent_status_changed, backlog_changed, quest_changed, and shard_update. Ctrl-C exits cleanly.
wait
Block until a condition is met, then exit. Built on events, so agents can coordinate without polling.
crystl wait pending # block until a permission request exists
crystl wait pending --timeout 30 # give up after 30s (exits with status 1)
crystl wait pending --json # emit the matching request as JSON
crystl wait askuser --timeout 60 # block until an AskUserQuestion opens
crystl wait notification --type Stop # block until a matching hook notification arrives
crystl wait awaiting --gem myapp # block until a worker parks on an in-terminal prompt
crystl wait idle --shard opal # block until a worker finishes a turn
crystl wait done --shard opal # block until that worker reports done (or blocked) via notify --done
crystl wait done --shard opal --timeout 120 --idle-fallback # succeed on timeout if opal sits silently idle
wait pending and wait askuser have a fast path: if a matching item already exists when the command starts, it prints and exits without subscribing. wait notification listens for fresh notifications. wait awaiting catches in-terminal prompts that are not structured permission requests; wait idle exits on a working-to-idle edge.
wait done is the machine-checkable “worker finished” wait: it blocks on the typed worker_notify signal from crystl notify --done (a --blocked report also releases it; a stuck worker needs the lead awake). Idle alone only means a turn ended, so --idle-fallback (requires --timeout and --shard) covers the worker that finished but forgot to ping: on timeout expiry it succeeds if the watched shard is sitting idle.
token
Print the current bearer token for debugging.
crystl token
facet
Manage facet inserts, the snippet library behind the facet bar, from the CLI. list is free; add, slot, and remove change the library, so they’re Guild-gated.
crystl facet list # show the library (ids, labels, slots)
crystl facet list --json
crystl facet add "review" "review this branch for bugs"
crystl facet add "deploy" "npm run deploy" --kind command --slot 1
crystl facet slot <id> 2 # bind an existing insert to bar slot 2
crystl facet remove <id>
facet add flag | Purpose |
|---|---|
--scope <all|shared|isolated> | Which shards the insert is offered in (default: all) |
--action <insert|submit> | Insert the text, or insert-and-submit (default: submit) |
--kind <prompt|command> | Categorize the insert (default: prompt) |
--slot <1|2|3> | Also bind it to a facet-bar slot |
--json | Emit the created insert as JSON |
workbench
Show or hide the Workbench panel, or read and edit the WORKBENCH.md / BACKLOG.md task list from the CLI. Panel controls and edits are mutations, so they’re Guild-gated. workbench list is read-only and free.
crystl workbench # slide the panel into view (default subcommand)
crystl workbench open --gem myapp
crystl workbench open --column progress # open on backlog | progress | done
crystl workbench close
crystl workbench toggle # flip open/closed
crystl workbench list --gem myapp
crystl workbench add "fix login redirect" --section "Next"
crystl workbench start 12 # mark task 12 [~]
crystl workbench check 12 # mark task 12 [x]
crystl workbench uncheck 12 # mark task 12 [ ]
crystl workbench cycle 12 # todo -> in progress -> done -> todo
crystl workbench delete 12
crystl workbench section "Later"
crystl backlog … is kept as an alias (the panel was renamed from “backlog”), so older scripts and habits keep working. Task indexes come from crystl workbench list.
State changes report what the server did
check, uncheck, and start report the task’s actual state on the server, not the state you asked for. Several agents can share one workbench, so the task you’re checking may have moved since you read the list.
- The success line names the state the task ended in.
- A conflict, such as checking a task another agent already checked, exits non-zero. Nothing is silently overwritten.
- In
--jsonmode a failure prints nothing on stdout. There’s no error object to parse.
That last point matters if you’re scripting: check the exit code, don’t test for empty output. A consumer that only reads stdout sees the same nothing on a conflict as on a crash, so branch on the status code.
if crystl workbench check 12 --json > result.json; then
echo "checked"
else
echo "someone got there first (exit $?)"
fi
Read the list again after a non-zero exit and you’ll see who moved it.
markdown
Discover and surface the markdown files in the calling gem. Two subcommands: list finds them, show opens one in the workbench editor. Both are free on every tier, deliberate surfacing hooks like crystl copy: they read or open gem-owned files without mutating them.
markdown list [dir] lists the gem’s markdown files, one path per line, so an agent can find the doc it wants to surface. Pass an optional gem-relative subdirectory to scope the listing.
crystl markdown list # every .md file in the gem
crystl markdown list docs # scope to a subdirectory
markdown show <path> opens one of the gem’s markdown files in the workbench editor, the CLI version of “show me that doc.” Useful for an agent that wants to surface a file for you to read or edit (“here’s the runbook I just updated”) without you hunting for it. The path is gem-relative or absolute and must be a .md file inside the calling gem; it opens in the gem the command is run from.
crystl markdown show README.md
crystl markdown show docs/guide.md
crystl edit CHANGELOG.md # short root-level alias for `markdown show`
docs
Search and read crystl’s documentation from the terminal. Handy for an agent answering “how do I X?” or “is this a bug?” without bloating its context. Pulls the docs index live from crystl.dev (cached locally for 6h). Read-only.
crystl docs # list every doc page
crystl docs notifications # exact page id → prints the full page
crystl docs "isolated shards" # otherwise searches; prints ranked matches
crystl docs isolated --full # print full bodies of all matches
crystl docs --fresh # bypass the cache and refetch
doctor
Check local CLI, bridge, and hook wiring. Useful before debugging approval/notification issues.
crystl doctor
crystl doctor --full # add a health snapshot
crystl doctor --json
--full appends a health section: crystl’s version and uptime, memory use and system memory pressure, how many gems and shards are open, transcript parse-cache stats, timeline row counts, and the size of each store on disk (timeline database, docs and hero caches, side-quest logs, app logs). It’s numbers only, never terminal content, and it’s the quickest way to answer “why does this feel slow?” before digging further.
render
Render terminal bytes offline through crystl’s terminal grid. This does not require a running bridge; it’s for debugging terminal parser/rendering behavior.
crystl render --feed transcript.bin --cols 120 --rows 40
crystl render --feed transcript.bin --dump-cells
crystl render --feed transcript.bin --json
ssh
Configure the direct bridge address used by SSH sessions when the automatic tunnel is not enough.
crystl ssh bridge-address 100.64.0.1:19280
crystl ssh bridge-address "" # clear override
report
Draft a bug report, idea, or praise for the crystl team. Free on every tier. Write the report in plain language and never include terminal output, file paths, or secrets.
crystl report bug "approval panel hangs after Ctrl-C"
crystl report bug "everything feels sluggish" --diagnostics
crystl report idea "let me pin a shard to the top"
crystl report praise "the rail is gorgeous"
crystl report other "..." --email [email protected] # optional reply address
Nothing is sent until you press send. The report opens as an editable draft on your desktop showing exactly what would leave your machine: the message, your reply email, and a toggle for sharing the report in the crystl Slack so other users can see it. Edit anything, then send or discard. Because your agent can’t send on your own behalf, it’s safe to ask one to write the report for you; you still get the last word.
Add --diagnostics (on report bug and report other) to attach a health snapshot, the same numbers doctor --full prints: memory, cache counters, store sizes. It appears in the draft as its own section with a toggle, so you can detach it before sending. It carries no terminal content, transcripts, or file contents. Worth attaching for anything performance-related; it usually saves a round of back-and-forth.
settings
Read crystl’s current app settings, inspect one whitelisted setting, or live-apply a narrow setting change. The full snapshot and settings get are free. settings set is a Guild control command.
crystl settings # grouped, human-readable snapshot
crystl settings --json # raw snapshot object
crystl settings get terminal_surface # one whitelisted value, free
crystl settings set terminal_surface prism # live-apply a value, Guild
crystl settings set rail right
crystl settings set notificationsEnabled off
This is deliberately not a generic preferences writer. Unknown keys are rejected with the complete allowlist, and API-key values are never exposed. set validates the value and uses the same live-apply path as Settings, so visible controls such as the rail and notification panels update immediately.
One key is read-only. terminal_surface is what you asked for; terminal_surface_active is what shards actually got. Ask for it with crystl settings get terminal_surface_active, and settings set refuses it, since there’s no preference to write. It reads unknown until a shard has been built, because until then no renderer has been chosen. The same pair is on GET /health as terminalSurface.requested and terminalSurface.active.
| Key | Accepted values |
|---|---|
terminal_surface | prism (the default), cavrn (alias for Prism), or xterm (fallback) |
terminal_surface_active | Read-only. The renderer shards were actually built with: prism, xterm, or unknown before any shard exists |
rail | on, off, left, right, or top |
crystalRailEnabled | Boolean: on / off, true / false, yes / no, or 1 / 0 |
crystalRailPosition | left, right, or top |
notificationsEnabled | Boolean |
approvalPanelsEnabled | Boolean |
denialPanelsEnabled | Boolean |
askUserPanelsEnabled | Boolean |
receiveExternalNotifications | Boolean |
Stop | Boolean |
PostToolUse | Boolean |
SubagentStop | Boolean |
See Settings for the snapshot fields and the matching app controls.
uninstall
Remove crystl’s hooks, CLI symlink, and support files from this machine. Free on every tier.
crystl uninstall --dry-run # print exactly what would change, touch nothing
crystl uninstall # asks before it does anything
crystl uninstall --yes # skip the confirmation
crystl uninstall --primers # also strip the CLI primer from open gems' agent docs
Worth knowing why this exists: dragging Crystl.app to the trash runs no code. The Claude and Codex hooks it registered keep firing on every tool call of every agent session afterwards, quietly failing. This command is the explicit reversal.
It reports in three parts: what it removed, anything not done with the reason, and what it left in place, which is your own data and never deleted. Start with --dry-run if you want the inventory first.
--primers goes further and edits files inside your git repos, stripping the crystl CLI primer section from each open gem’s agent docs. It’s opt-in for that reason. It’s also the way to clear primers you no longer want without uninstalling for good: turning the tell agents about the crystl CLI setting off stops new ones, but the blocks already written stay put until something deletes them.
Agent sessions that are already running keep calling the now-inert hook script until they restart. It works with crystl not running; the only bridge call is a free read to list open gems for --primers.
Scripting examples
Monitor an agent
# What is the agent doing right now?
crystl screen --gem myapp
# Wait for new output without polling
crystl watch --gem myapp --shard opal --pattern 'ready|failed' --once
Approve from another terminal
crystl pending # see what's waiting
crystl approve 1 # approve it
Launch parallel agents
crystl open ~/Projects/frontend
crystl open ~/Projects/backend
crystl send --gem frontend "claude 'update the API client'"
crystl send --gem backend "claude 'add the new endpoint'"
JSON for scripting
crystl gems --json | jq '.gems[].name'
crystl pending --json | jq '.pending | length'
HTTP API
The CLI wraps a local HTTP API at http://127.0.0.1:19280/api/v1/. You can call it directly with curl:
TOKEN=$(head -1 ~/.crystl-bridge-token)
# List gems
curl -s -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:19280/api/v1/gems | jq
# Send a command
GEM_ID="..." # from gems response
SHARD_ID="..." # from gems response
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"text":"ls\r"}' \
"http://127.0.0.1:19280/api/v1/gems/$GEM_ID/shards/$SHARD_ID/send"
# Subscribe to real-time events (SSE)
curl -N -H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:19280/api/v1/events
Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/api/v1/gems | GET | List open gems (includes memory telemetry: memory.app_rss_bytes, memory.pressure, per-shard memory_rss_bytes) |
/api/v1/gems | POST | Open a gem |
/api/v1/gems/:id | GET | Get gem details |
/api/v1/gems/:id | DELETE | Close a gem |
/api/v1/gems/:id/select | POST | Bring an open gem forward |
/api/v1/gems/:id/shards | POST | Create a shard (incl. hero) |
/api/v1/gems/:id/shards/:sid | GET | Get shard details |
/api/v1/gems/:id/shards/:sid | DELETE | Close a shard |
/api/v1/gems/:id/shards/:sid/select | POST | Bring a shard and its gem forward |
/api/v1/heroes | GET | List summonable heroes (?gem= for local) |
/api/v1/gems/:id/shards/:sid/send | POST | Send text to terminal |
/api/v1/gems/:id/shards/:sid/abort | POST | Stop the active agent turn without closing the shard |
/api/v1/gems/:id/shards/:sid/notify | POST | Send an attributed worker notification |
/api/v1/gems/:id/shards/:sid/rename | POST | Set/clear the shard’s display label |
/api/v1/gems/:id/shards/:sid/copy | POST | Push text to the copy bar (free) |
/api/v1/gems/:id/shards/:sid/screen | GET | Get screen content |
/api/v1/gems/:id/shards/:sid/screen?command=… | GET | Get one completed shell command’s retained output (free) |
/api/v1/gems/:id/shards/:sid/commands | GET | List indexed shell commands or get selected output (free) |
/api/v1/gems/:id/shards/:sid/watch | POST | Hold a bounded regex watch against new output |
/api/v1/gems/:id/shards/:sid/clear-scrollback | POST | Clear screen + retained scrollback (free) |
/api/v1/gems/:id/shards/:sid/anoint | POST | Confer the orchestrator role (body {"deliver": true|false}) |
/api/v1/gems/:id/shards/:sid/crown | POST | Crown a shard (body {"deliver": true, "goal": "…"}) |
/api/v1/gems/:id/shards/:sid/crown | DELETE | Uncrown a shard |
/api/v1/agent-profiles | GET | Learned size-to-model and delivery profiles by exact command (free) |
/api/v1/agent-profiles | POST | Teach an exact command’s model profile (free on every tier) |
/api/v1/gems/:id/shards/:sid/merge | POST | Merge an isolated shard’s branch |
/api/v1/gems/:id/shards/:sid/transcript | GET | Get structured shard history |
/api/v1/gems/:id/resurrect | GET | List resurrectable closed shards → { gem_id, entries } (free) |
/api/v1/gems/:id/resurrect | POST | Resurrect a closed shard (optional body { "crystalName": "opal" }) → ShardResponse |
/api/v1/schedules | GET | List scheduled agent tasks and retained run history (free) |
/api/v1/schedules | POST | Schedule a fresh agent shard |
/api/v1/schedules/:id | DELETE | Cancel and delete a schedule by ID or unique prefix |
/api/v1/pending | GET | List pending permissions |
/api/v1/pending/:id/decide | POST | Approve or deny |
/api/v1/history | GET | Recent decisions |
/api/v1/notifications | GET | Recent notifications |
/api/v1/events | GET | SSE event stream |
/api/v1/askuser | GET | List open agent questions |
/api/v1/askuser/:id/answer | POST | Answer an agent question |
/api/v1/gems/:id/quest/start | POST | Launch a quest party (optional template slug) |
/api/v1/gems/:id/quest/master | POST | Summon the gem’s Quest Master |
/api/v1/gems/:id/quest/propose | POST | Pre-fill the begin-quest panel (never launches) |
/api/v1/gems/:id/quest/templates | GET/POST | List launchable questlines, or save the plan as a template |
/api/v1/gems/:id/quest/levels | GET | The gem’s questline and where the party is |
/api/v1/gems/:id/quest/clear | POST | Archive the quest chat |
/api/v1/gems/:id/quest/end | POST | End the active quest |
/api/v1/gems/:id/quest | GET | Quest state (active, messages, heroes) |
/api/v1/gems/:id/quest/message | POST | Post a message into the quest bus |
/api/v1/parties | GET/POST | List or create parties |
/api/v1/parties/:id | DELETE | Delete a party |
/api/v1/gems/:id/backlog | GET | Read the workbench/backlog |
/api/v1/gems/:id/backlog | POST | Open, close, or toggle the workbench panel |
/api/v1/gems/:id/backlog/toggle | POST | Toggle a task checkbox |
/api/v1/gems/:id/backlog/cycle | POST | Cycle a task through todo / in progress / done |
/api/v1/gems/:id/backlog/task | POST | Add a task |
/api/v1/gems/:id/backlog/task/:index | POST/DELETE | Edit or delete a task |
/api/v1/gems/:id/backlog/section | POST | Add a section |
/api/v1/gems/:id/backlog/section/:index | POST/DELETE | Rename or delete a section |
/api/v1/facets | GET/POST | List or add facet inserts |
/api/v1/formations | GET | List saved formations (free) |
/api/v1/formations/save | POST | Save the current window arrangement |
/api/v1/formations/apply | POST | Apply a saved window arrangement |
/api/v1/settings | GET/POST | Read the settings snapshot, or live-apply a whitelisted setting |
/api/v1/settings/:key | GET | Read one whitelisted setting (free) |
/api/v1/settings/ssh | POST | Set or clear the SSH bridge address |
/api/v1/feedback | POST | File a bug / idea (free on every tier) |
/api/v1/card | POST | Show an honestly attributed notification card (free) |
All endpoints require Authorization: Bearer <token> header.
Error responses
When the routing layer rejects a request before it reaches a handler, it answers with a structured envelope rather than a bare status line or a wall of text:
{
"error": {
"code": "unauthorized",
"message": "bearer token is not valid for this bridge",
"status": 401
}
}
code is the stable, machine-readable identifier. Branch on it. message is written for a human reading a log and can change between releases. status repeats the HTTP status so a client that only kept the body still knows what happened.
Three routing-layer statuses use this shape:
| Status | Meaning |
|---|---|
401 | The bearer token is missing or wrong |
403 | Authenticated, but not allowed: a Guild-only control command on a free tier, or a non-loopback caller hitting a mutation with remote access off |
503 | The bridge is up but can’t serve the request yet, for example before the app has finished starting |
The two-line token file
~/.crystl-bridge-token has two lines, and only the first one is the token. Paste the whole file into an Authorization header and every request fails.
crystl now catches that specific mistake. When a 401 credential looks like the entire file rather than a single token, the message says so and tells you to send line 1 on its own. If you’re staring at a 401 that “should” work, read the message before re-checking anything else; it’s usually this.
That’s what the head -1 in the curl examples above is for:
TOKEN=$(head -1 ~/.crystl-bridge-token)
Health check
GET /health reports whether the bridge is answering, and which terminal renderer is actually in use:
{
"terminalSurface": { "requested": "prism", "active": "prism" }
}
requested is the terminal surface you picked in settings. active is the one the app really built a shard with. They differ when a requested renderer couldn’t start and crystl fell back, so comparing the two is how you confirm a renderer preference actually took effect. Before any shard exists there is nothing to measure, so active reads unknown.
Security
- Auth: Every request requires a bearer token read from
~/.crystl-bridge-token(0600 permissions, regenerated each launch) - Remote mutation gate: Local loopback callers can use the CLI directly. Non-loopback callers can mutate terminals only when remote access is enabled; otherwise control routes return 403.
- Rate limited: 100 requests per second per client IP
- Structured rejections: routing-layer
401,403, and503answers carry an{ "error": { "code", "message", "status" } }body, so a client can branch oncode. See error responses.