# The rystic CLI

Installs, runs and health-checks every simulator — natively or as a container.

[Installation](https://www.rystic.ai/docs/installation.md) is the walkthrough. [CLI flag reference](https://www.rystic.ai/docs/cli-reference.md) lists every command and flag, generated from command help. [Docker](https://www.rystic.ai/docs/docker.md) covers running without a local CLI: image defaults, Compose overrides and environment variables.

## Install

```bash
curl -fsSL https://www.rystic.ai/install.sh | sh
rystic login rys_…                        # an API key from /account/api-keys (+ docker login)
rystic login --token-stdin < key.txt      # the same, kept out of shell history
rystic login ./rystic-license.json        # Enterprise: activate a license file (+ docker login)
```

1. The CLI is the downloader, not the product — anyone can install it. [Simulator](https://www.rystic.ai/docs/concepts/glossary.md#simulator) binaries and images need an API key or, on Enterprise, a license.
2. macOS and Linux; Windows zip on request. `RYSTIC_BIN` overrides where it lands.
3. `login rys_…` stores the key at `$RYSTIC_HOME/token`, readable by you only, and never prints it. `login <file>` verifies a license offline and parks it in `$RYSTIC_HOME`. Either runs `docker login registry.rystic.ai` when a runtime answers; `--no-docker` skips that half. [Plans and API keys](https://www.rystic.ai/docs/licensing.md) covers both.
4. After a license login, on a terminal and with `$CI` unset, `login` offers a numbered menu of simulators to start, each exactly as `rystic run <product> -d --scenario <its quickstart scenario>`; a busy port retries once on a free one. `--no-start` skips the offer, and so does any non-terminal or CI run.

## Commands

| Command | What it does |
|---|---|
| `login <rys_key \| file>` | store an API key, or install an Enterprise license file, and log Docker in to the registry with it |
| `docs [topic] \| search <terms>` | Rystic's docs, offline: the site's pages as of this CLI's release, plus each installed simulator's `HOWTO.md` and `SCENARIOS.md` |
| `run [product]` | run a simulator (pulls it if missing); `-d` detaches, args after `--` go to the simulator |
| `ui [product]` | run a simulator's [verified UI](https://www.rystic.ai/docs/concepts/glossary.md#verified-ui) against a running instance: the `<product>-ui` image through the registry when Docker answers, else its archive on your Node; Pro and Enterprise |
| `ps` | list running simulators — status from a live probe, not the record |
| `stop <name>… \| --all` | stop instances (SIGTERM, then SIGKILL after `-timeout`) |
| `restart <name>… \| --all` | stop and start again from the recorded spec |
| `rm <name>…` | remove a stopped instance's record and log |
| `logs [-f] <name>` | print a detached simulator's log; `-f` follows |
| `tail [-addr url] [-events …]` | stream a running simulator's state events to this terminal |
| `up [-d] [-f rystic.yaml] [svc…]` | start every service of a stack file — each exactly one `run -d` |
| `down [-f rystic.yaml \| --project p]` | stop every instance of a stack file's project |
| `export compose\|antithesis [-f] [-o dir]` | a digest-pinned `docker-compose.yaml` (+ `.env.example`), or the Antithesis scaffold |
| `scenario save <instance> <file>` | a running instance's state as a [scenario file](https://www.rystic.ai/docs/concepts/glossary.md#scenario-file) that `run --seed` and `POST /_rystic/seed` take back; `--force` replaces an existing file |
| `status` | API key, license, runtime, registry, installed, running, updates — one screen |
| `list` | released simulators — latest, installed, running (reads a license file) |
| `pull [product]` | download and install a simulator release without running it |
| `update` | install a newer simulator release, or just ask whether one exists |
| `check` | boot an installed simulator and confirm it serves and [resets](https://www.rystic.ai/docs/concepts/glossary.md#reset) |
| `doctor [product]` | check this machine end to end without running anything |
| `features` | list what the installed simulator can do |
| `config [set k v]` | show every setting with its source, or set one |
| `license [activate <file>]` | Enterprise: show the active license, or install one |
| `mcp [install --client c]` | serve MCP tools on stdio so a coding agent drives simulators, or write the server into a client's config ([guide](https://www.rystic.ai/docs/mcp-server.md)) |
| `feedback [message]` | send Rystic a bug, friction or feature request, filed under your account |
| `self-update` | replace this CLI with the latest release |
| `version` | this CLI's version, then every running simulator's — warning on any drift |

`rystic help <command>` prints a command's flags on stdout. An older CLI prints them on stderr; `rystic help <command> 2>&1` works on either.

`--json` prints `{schema_version, command, status, data, warnings, error}`. Only `run`, `ps`, `restart`, `up`, `status`, `list`, `version`, `config`, `report`, `docs` and `scenario save` take it; the rest refuse it (exit 2).

### Simulator-specific commands

These only apply to a simulator whose manifest declares the feature — today the [Kalshi simulator](https://www.rystic.ai/docs/simulators/kalshi.md). `rystic features` lists what the installed build supports.

| Command | What it does |
|---|---|
| `run --live` / `feed <product>` | feed a simulator from the exchange as it moves — [Live mode](https://www.rystic.ai/docs/simulators/kalshi.md#live-mode) |
| `logs --feed <name>` | a detached instance's feeder log |
| `batch [--runner docker --image <ref>]` | run a bot against every [tape](https://www.rystic.ai/docs/concepts/glossary.md#tape) in a library — [Backtesting](https://www.rystic.ai/docs/kalshi-backtesting.md) |
| `report <batch-out-dir> [-sort col] [-csv]` | the batch P&L table — exact totals, settled vs left open |
| `grid [product] [-- <bot command>]` | the bot × [scenario](https://www.rystic.ai/docs/concepts/glossary.md#scenario) matrix — bundled example bots plus yours |

### run

```bash
rystic run stripe-twin                                # serves :8080, streams events
rystic run stripe-twin -d --name a --port 0           # detach on a free port
rystic run stripe-twin --scenario design_studio       # boot into a named scenario
rystic run stripe-twin --seed ./scenario.json         # seed file, applied before the first request
rystic run stripe-twin --runner docker -d             # as a container, from registry.rystic.ai
rystic run stripe-twin -- --events=all                # args after -- go to the simulator
```

1. CLI flags can appear before or after the product name; anything after `--` goes to the simulator.
2. Default port 8080; a busy port is exit 9; `--port 0` picks a free one.
3. `--runner docker` needs a container runtime. The image comes from the registry, or is built locally from a pulled Linux archive when `image_source=archive` or the registry is unreachable. See [Docker](https://www.rystic.ai/docs/docker.md).
4. Replay (`--tapes`, `--tape`, `--copies`, `--speed`) and `--live` are Kalshi features: [Replay](https://www.rystic.ai/docs/simulators/kalshi.md#replay-a-captured-market-day), [Live mode](https://www.rystic.ai/docs/simulators/kalshi.md#live-mode).
5. On Free and Pro, `run` hands the simulator your API key and the simulator checks in with Rystic while it serves. `-v` prints one line per check-in. On Free, `--network` exits naming the `faults` feature the plan lacks: [Boot inputs](https://www.rystic.ai/docs/control-plane.md#boot-inputs).

### up, down, export — the stack file

```yaml
# rystic.yaml — every key is a run flag; the service name is the instance name
version: 1
project: demo
services:
  payments: {product: stripe-twin, port: 0, scenario: design_studio}
  reviews:  {product: github-pulls-twin, port: 0, seed: ./seeds/repo.json}
  exchange: {product: kalshi-twin, runner: docker, port: 0, scenario: thin_book}
```

File paths are relative to the stack file. On Free, a service may set no `network`. Kalshi-only keys (`tapes`, `tape`, `copies`, `live` and the `live_*` family) are listed on the [Kalshi page](https://www.rystic.ai/docs/simulators/kalshi.md#live-mode). The [Docker builder](https://www.rystic.ai/docs/docker-builder.md) writes this file for you.

```bash
rystic up -d                       # one identity line per service
rystic ps --project demo
rystic down
rystic export compose -o ./deploy  # digest-pinned compose + .env.example
rystic export antithesis -o ./anti --tenant <t> --gcp-project <p>
```

A stack file never carries a key or license. A malformed one is refused naming the file, service and key. `export` warns when a `tape:` cannot be expressed in Compose.

### ps, stop, logs, status, tail

```bash
rystic ps                    # NAME PRODUCT VERSION RUNNER URL MODEL LICENSEE UPTIME STATUS
rystic stop a b              # or --all
rystic logs -f a
rystic status                # --offline skips the network
```

`status` prints the registry line — `registry.rystic.ai · logged in · reachable`. A `token:` line shows the API key masked (`rys_AAAA…`), where it came from, and the organization and plan the site reports; `--offline` prints `not checked` instead. A license file adds a `license:` line. With neither, it prints `credential: none` and the `rystic login` line to run. `--json` carries both as `token` and `license`, never the key itself.

```bash
rystic tail -addr http://localhost:53124            # a detached instance on another port
rystic tail -events all                             # public market data too
```

`tail` reads the simulator's `GET /_rystic/events` stream. `-addr` defaults to `http://localhost:8080`.

### doctor

```bash
rystic doctor stripe-twin
```

The pre-demo check, no boot: the CLI and its home, configuration, the install, signing-key agreement, simulator data, container runtime, registry login — and, for a simulator with a live feed, the exchange key and where it came from. Exits 0 when no line shows `✗`.

`doctor` checks the credential the installed build runs on. On an API key:

```
✓ api key                rys_AAAA… · from /Users/you/.rystic/token · org <org> · <plan> plan · www.rystic.ai answered
```

`✗ api key plan` means the plan does not include that product; a refused key is `✗ api key` with the exit-11 sentence, and `doctor` exits 11. If the site does not answer, the line is a `!` warning and the simulator checks the key when it starts. With a license file, `doctor` checks the license instead and reports any stored key as `api key (not in use)`, a warning at most. `rystic check <product>` boots the installed build once to prove it.

### docs

```bash
rystic docs                          # numbered index: the site's pages, then each installed simulator's HOWTO and SCENARIOS
rystic docs agent-quick-start        # one page as Markdown
rystic docs cli#exit-codes           # one section
rystic docs stripe-twin              # the installed HOWTO.md, plus the live routes of each running instance
rystic docs kalshi-twin scenarios    # the installed SCENARIOS.md
rystic docs search rate limit        # every section holding all the terms, as <topic>#<anchor>
rystic docs -online cli              # read the live site instead of the built-in snapshot
```

The site's pages are a snapshot built into the CLI when it was released, so `rystic docs` works offline and needs no key; a page added since needs `-online`. A product's short name works too (`rystic docs stripe`); with nothing installed it falls back to the site's page for that simulator. `-json` prints the envelope, `-n` caps search results. An unknown topic, section or `SCENARIOS.md` exits 5 and says what to try instead. Stripe ships no `SCENARIOS.md`.

### list, pull, update

```bash
rystic list                                # PRODUCT LATEST INSTALLED RUNNING SUMMARY (on an API key or a license file)
rystic pull -version <x.y.z> stripe-twin   # omit for latest
rystic update                              # the CLI and every installed simulator
rystic update -check                       # report only; exit 10 if newer ones exist
```

`pull` installs into `$RYSTIC_HOME/twins/<product>/<version>`, checksum-verified, marked current. `-os linux -arch amd64` fetches another platform's build (the air-gap Docker path).

### config

```bash
rystic config                       # every setting, its value and where it came from
rystic config set runner docker     # flag > env > config.json > built-in
```

| Key | Env | Built-in |
|---|---|---|
| `runner` | `RYSTIC_RUNNER` | `native` |
| `port` | `RYSTIC_PORT` | `8080` |
| `tapes` | `RYSTIC_TAPES` | — (Kalshi replay library) |
| `image_source` | `RYSTIC_IMAGE_SOURCE` | `registry` (`archive` builds locally) |
| `registry` | `RYSTIC_REGISTRY` | `registry.rystic.ai` |
| `docker.binary` | `RYSTIC_DOCKER` | `docker`, else `podman` |
| `docker.host` | `DOCKER_HOST` | — |
| `release_endpoint` | `RYSTIC_RELEASE_ENDPOINT` | `https://www.rystic.ai/api/twin-release` |
| `catalog_endpoint` | `RYSTIC_CATALOG_ENDPOINT` | `https://www.rystic.ai/api/catalog` |
| `docs_endpoint` | `RYSTIC_DOCS_ENDPOINT` | `https://www.rystic.ai` (what `docs -online` reads) |
| `token` | `RYSTIC_TOKEN` | — (an API key; `rystic config` shows only `(set)` and where it came from) |

`config set token` is refused: edit `config.json` or use `rystic login`.

## Files and environment

Everything lives under `$RYSTIC_HOME` (default `~/.rystic`): installed simulators, the `current` marker per product, `token` (the API key `login` stored) or the active license, `config.json` (once you `config set` something), and `run/` — one record per instance.

| Variable | Meaning |
|---|---|
| `RYSTIC_HOME` | install root |
| `RYSTIC_PRODUCT` | product to use when no product argument is given; set by the image |
| `RYSTIC_TOKEN` | an API key; checked before `config.json` and the stored `token`, and how containers get it |
| `RYSTIC_LICENSE` | Enterprise: a license file path — or the license itself (JSON or base64), which is how containers get it |
| `RYSTIC_REGISTER_WAIT` | how long a simulator retries reaching Rystic at start, default `10m`; `0` tries once |
| `RYSTIC_LEASE_URL` | the [lease](https://www.rystic.ai/docs/concepts/glossary.md#lease) server a Free or Pro simulator checks in with, default `https://lease.rystic.ai` |
| `RYSTIC_LEASE_CACHE` | where a simulator keeps its last lease for an [offline start](https://www.rystic.ai/docs/licensing.md#offline-and-outages), default `rystic/leases` under your config directory |
| `RYSTIC_LEASE_VERBOSE` | `true` prints one line per check-in; `run -v` sets it |
| `RYSTIC_DOCS_ENDPOINT` | the site `docs -online` reads |
| `RYSTIC_CLI_ENDPOINT` | where `update` and `self-update` look for a newer CLI, default `https://www.rystic.ai/api/cli-release` |
| `RYSTIC_LICENSE_PUBKEY` | a license public key to verify against instead of the built-in one (`-pub` on the command line) |
| `DOCKER_CONFIG` | where `status` and `doctor` look for your Docker login, as Docker does; only the entry names are read |
| `CI` | any value suppresses `login`'s offer to start a simulator |
| `RYSTIC_BIN` | installer: where it puts `rystic` |
| `RYSTIC_VERSION` | installer: which CLI version to install, default `latest` |

`RYSTIC_TOKEN`, `RYSTIC_LEASE_URL`, `RYSTIC_REGISTER_WAIT`, `RYSTIC_LEASE_CACHE` and `RYSTIC_LEASE_VERBOSE` reach the simulator, including a `--runner docker` container, when they are set.

The simulator binary also reads these variables. With the default image command, `rystic run` inherits them or sets them from the corresponding CLI flags:

| Variable | Meaning |
|---|---|
| `RYSTIC_ADDR` | direct simulator listen address (`--addr`); `rystic run` sets it from `--port` / `RYSTIC_PORT` |
| `RYSTIC_SCENARIO` | boot into a [named scenario](https://www.rystic.ai/docs/concepts/glossary.md#named-scenario) (`--scenario`) |
| `RYSTIC_SEED` | boot [seed](https://www.rystic.ai/docs/concepts/glossary.md#seed) file (`--seed`) |
| `RYSTIC_NETWORK` | `off`, `on`, or a profile file (`run --network`); Pro and Enterprise |
| `RYSTIC_EGRESS_RETAIN` | [egress](https://www.rystic.ai/docs/concepts/glossary.md#egress-log) frames kept, default `100000` (`run --egress-retain`) |
| `RYSTIC_EVENTS` | event feed scope (`--events`) |
| `RYSTIC_SEED_GATE` | `reject` refuses an incomplete seed instead of serving it with a warning |
| `RYSTIC_GATEWAY_TOKEN` | for a simulator behind your own proxy: the bare binary, or a container with a health check of its own. Every request, `/_rystic/version` included, must carry `X-Rystic-Gateway-Token` with this value or gets `403`. `rystic run`'s readiness probe and the image's `HEALTHCHECK` send no header, so `run -d` times out (exit 7) and the stock image never reports healthy |

[Control plane and plans](https://www.rystic.ai/docs/control-plane.md#environment-variables) lists every simulator variable, its default and which ones a plan gates.

Kalshi-only variables — `RYSTIC_LIVE*`, `KALSHI_KEY_ID`, `KALSHI_PRIVATE_KEY_*`, `KALSHI_CAPTURE_DIR` — are on the [Kalshi page](https://www.rystic.ai/docs/simulators/kalshi.md#live-mode).

## Exit codes

Stable — script against them.

| Code | Meaning |
|---|---|
| 0 | ok |
| 1 | error |
| 2 | usage |
| 3 | license (Enterprise): missing, unreadable, unsigned, expired or for another product. Also no license and no usable API key (none, or not shaped `rys_…`) when `pull` or `update` asks for a build, or `run` does because the simulator is not installed yet |
| 4 | network |
| 5 | not found: not installed, no matching release asset, or no such doc, section or `SCENARIOS.md` (`docs`) |
| 6 | integrity |
| 7 | simulator unhealthy |
| 8 | runtime unavailable |
| 9 | conflict (port or name in use, or the file `scenario save` would write exists) |
| 10 | update available (`-check`) |
| 11 | API key: missing or malformed when `run` starts an installed Free or Pro build, or refused by the release endpoint: `invalid_token`, `revoked`, `org_deleted`, `key_expired`, `product_not_in_plan`. A new key or a bigger plan fixes it; a retry does not |

A simulator stopped by its lease (monthly limit, spend cap, revoked key) exits 1 with the reason on stderr, and a foreground `run` hands that back.

## Next

<!-- simulator-list -->
1. [Docker](https://www.rystic.ai/docs/docker.md) — pull, compose, CI.
2. [Simulators](https://www.rystic.ai/docs.md#simulators) — one page per API: Anthropic, Clerk, Dropbox, GitHub, Gmail, Google Drive, Kalshi, Linear, Lob, Notion, Okta, PayPal, Resend, S3, Shopify Storefront, Slack, Stripe and Word.
3. [Control plane and plans](https://www.rystic.ai/docs/control-plane.md) — every `/_rystic` route and the plan that serves it.

## What leaves your machine

On Free and Pro, the simulator checks in with `https://lease.rystic.ai` while it runs: at start, every 30 seconds, and on exit, carrying your API key and the 30-second windows in which it was in use. Never your requests, their contents, your data or your hostname. [Plans and API keys](https://www.rystic.ai/docs/licensing.md#what-a-running-simulator-sends) lists every field. An Enterprise simulator makes no outbound calls. (One exception, Enterprise included: the Kalshi simulator's replayed-market metadata fetch, [documented on its page](https://www.rystic.ai/docs/simulators/kalshi.md#the-one-outbound-call).)

The CLI itself calls Rystic only in these cases:

| When | Where | What it sends |
|---|---|---|
| `pull`, `update`, `status` (its update check; `--offline` skips it), `run` or `up` when a simulator is not installed yet (for a Docker service, only to learn the latest version), and `ui` when its verified UI is not installed yet | `https://www.rystic.ai/api/twin-release` | your API key or license, the product, version, OS, architecture, which command asked and, for `ui`, `artifact: "ui"` |
| `list`, and the start offer after a license `login` | `https://www.rystic.ai/api/catalog` | the license |
| `update` and `self-update` | `https://www.rystic.ai/api/cli-release` | OS, architecture and the version asked for; no credential |
| `update`, `self-update` and `install.sh`, after `cli-release` answers | `https://release-assets.githubusercontent.com` | nothing: a download of the short-lived URL `cli-release` returned |
| `login` with no key (which `install.sh` runs) | `https://www.rystic.ai/api/device/status` | the API key the CLI generated, polled until someone approves its `/device` link; the link you open carries only the key's hash, its prefix and a name for the machine |
| `doctor` and `status` (not with `--offline`) | `https://registry.rystic.ai/v2/` | nothing: an anonymous request to see whether the registry answers |
| `login` | `registry.rystic.ai`, through your local `docker login` | the key or license as the registry password |
| `run --runner docker`, `up` with Docker services, and `ui` when Docker answers | `registry.rystic.ai` | an image pull, with the key or license as the credential |
| `batch --runner docker`, when its `--image` is not on the machine yet | the registry that image names | an image pull, with whatever your `docker login` holds for that registry |
| `export compose` | `registry.rystic.ai` | the license, to look up each image's digest (`--no-digest` skips it) |
| `feedback` | `https://www.rystic.ai/api/feedback` | your API key, the report as typed, the CLI's version, OS, architecture and container engine, and each running simulator's product, version, name and status; never an env file, a request body or a key's value |
| `docs -online` | `https://www.rystic.ai` (`RYSTIC_DOCS_ENDPOINT`) | nothing: it reads `llms.txt` and the pages' `.md` |

`rystic docs` without `-online`, `ps`, `logs`, `stop`, `features`, `config` and `help` stay on the machine.

A live feed adds the source API itself: the CLI dials it from your machine — see [Kalshi → Live mode](https://www.rystic.ai/docs/simulators/kalshi.md#live-mode).

---

Source: https://www.rystic.ai/docs/cli · Markdown: https://www.rystic.ai/docs/cli.md
