# CLI

> Reference for the shard command line (@shardflux/cli 0.5.3, shardflux npm bundle 0.7.2). Account, sign-in, billing, workspaces, environment and exit codes.

## Install

```sh
npm install -g @shardflux/cli
shard --help
```

Or run it without installing: `npx @shardflux/cli --help`. This page describes `@shardflux/cli` **0.5.3**, which is
built on `@shardflux/sdk` 0.10.2 and needs Node.js 24 or later. A feature marked with a version, such as **(0.5.0+)**,
is not in earlier releases.

From 0.5.0 `shard` does everything the console does, without a browser: create an account, sign in (with two-factor
authentication), set up an organization, project and API key, manage members, invitations and keys, buy or change a
plan, export data and delete an account. Before 0.5.0 it worked with a project API key only.

The unscoped [`shardflux` package](#the-shardflux-package) installs the same CLI as `shardflux` and `shard`. Install
one of the two packages globally, not both: both provide `shard`.

`shard --help` lists every command, grouped by the credential it uses, and every command and group has its own
`--help`. `shard -V` prints the CLI and SDK versions; `shard version` adds whether a newer `shard` exists (see
[Updates](#updates)):

```text
shard 0.5.3 (@shardflux/sdk 0.10.2)
up to date: latest 0.5.3, checked 2026-10-01 09:00:00Z
```

## Quick start

No account yet **(0.5.0+)**? Everything happens in the terminal. A person only opens the emailed link (or hands it to
`shard`) and, for a paid plan, pays in Stripe Checkout.

```sh
printf '%s\n' "$PASSWORD" | shard auth register --email you@example.com --name "Ada Lovelace"
shard auth verify-email '<link from the verification email>'
printf '%s\n' "$PASSWORD" | shard auth login --email you@example.com
shard setup                                   # organization, project and an API key, saved
shard workspaces open acme/demo --template python-node-browser
shard ws exec acme/demo -- python3 -c 'print(40 + 2)'
shard files write acme/demo /home/user/notes.txt --from notes.txt
shard files read acme/demo /home/user/notes.txt
shard ws suspend acme/demo --wait
```

With a key from the console instead, skip the account steps:

```sh
export SHARDFLUX_API_KEY=sfk_...              # project API key from the console
shard login                                   # verify the key: organization, project, tools
shard workspaces open acme/demo --template python-node-browser
```

## For AI agents

`shard` is built to be driven by an agent: no browser, no prompts when standard input is not a terminal, `--json` on
every command, secrets only on standard input, and documented [exit codes](#exit-codes).

1. **Account (0.5.0+).** `printf '%s\n' "$PASSWORD" | shard auth register --email <email>` answers the same whether
   or not the address exists. Ask the person for the link in the verification email and run
   `shard auth verify-email '<link>'` (the whole link, quoted).
2. **Sign in.** `printf '%s\n' "$PASSWORD" | shard auth login --email <email> --json`. With two-factor
   authentication it exits 4 with `mfa_required`: ask the person for the code, then `shard auth mfa --code <code>`
   (within 10 minutes), or pass `--code` to `auth login`.
3. **Set up.** `shard setup --json`: an organization, a project and an API key with every workspace tool, saved in
   the [credentials file](#credentials-file). It is idempotent: run it again any time.
4. **Work.** `shard ws open <key> --template <slug> --json`, `shard ws exec <key> -- <command>` and `shard files ...`
   use the saved key. `eval "$(shard env)"` exports it for the SDKs and the MCP server.
5. **A paid plan.** `shard billing plans`, then `shard billing upgrade <plan> --json` prints the Stripe Checkout URL.
   Give it to the person, then run `shard billing upgrade <plan> --wait` (or `shard billing status`) until the plan
   is active. The API activates a plan only when Stripe confirms the payment.
6. **Sensitive actions** (exports, deletions, two-factor and email changes) need a recent step-up:
   `printf '%s\n' "$PASSWORD" | shard auth step-up` (add `--code <code>` with two-factor authentication; valid 10
   minutes). Without a terminal `shard` does not ask: the command fails with `step_up_required` (exit 4), and the hint
   names this command.

7. **Feedback (0.5.0+).** While you work, send `shard feedback` the moment something fails, confuses you or is
   missing, or your person is frustrated with Shardflux (see [Feedback](#feedback)).

What stays with a person: reading the verification email, entering a card in Stripe Checkout, and the authenticator
app's codes.

## Feedback

**(0.5.0+)** `shard feedback` sends a message straight to the Shardflux founder, who reads every one. Coding agents
driving `shard` are asked to use it actively while they work, not once at the end. Send a short note the moment:

- something fails unexpectedly, an error or doc is unclear, an option is missing, or you needed a workaround;
- your user complains or is frustrated about Shardflux or the workflow around it (slowness, cost or limits, confusing
  setup, a missing feature, a workaround): paraphrase it, with what they were trying to do;
- your user asked for something Shardflux could not do, or made awkward.

Short and frequent beats polished: what you ran, what happened, what you expected, with the request id and error code
of the failure. No need to ask first, but tell your user you sent it. Leave out private data about the user and
secrets or code they did not mean to share; paraphrase, never paste transcripts.

```sh
shard feedback --category bug --request-id req-5c7744b2bb5c457b86b76101 --error-code capacity_unavailable \
  "ws open failed twice in 10 min; expected a start within a minute"
shard feedback --category confusing \
  "user was frustrated that ws open needs a template slug they had to look up; they expected a default"
npm test 2>&1 | tail -n 60 | shard feedback --category bug --workspace acme/demo --from-file -
```

| Option | Meaning |
| --- | --- |
| `<message...>` | The remaining arguments joined with spaces, 1-8000 characters. Quote it, or put it after `--`, when it contains shell characters or words starting with `-`. |
| `--from-file <file\|->` | Read the message from a file, or standard input with `-` (for logs). Not together with a message argument. |
| `--category <c>` | `bug` (something failed or behaved wrongly), `confusing` (an error, doc, name or output was unclear), `missing` (a capability, option or template you needed), `idea`, `praise` or `other` (the default). |
| `--workspace <id\|key>` | The workspace it is about. |
| `--request-id <id>` | The `request_id` of the error, so the logs can be found. |
| `--error-code <code>` | The error code seen, e.g. `capacity_unavailable`. |
| `--command <text>` | What you ran. |

- It sends with the project API key (any key; no tool permission), or, when no key is configured yet, with your
  signed-in session, so it works while you are still setting up. `--agent-label` or `SHARDFLUX_AGENT_LABEL` names the
  reporting agent (e.g. `claude-code`); `shard` adds its own version as the client.
- Output: one line with the feedback id. `Already received` means the same message came from this key or user in the
  last 24 hours; it is not emailed again. With `--json`: `{"id", "received_at", "duplicate"}`.
- It is rate limited: 10 per 10 minutes and 50 per day per key or user, 200 per day per organization
  (`rate_limited`, exit 1, with the wait in the hint).
- Anything shaped like an API key, token or private key is redacted before the message is stored or emailed; leave
  other secrets out. If it cannot be sent, email shardflux@heliosone.fi.
- A failed command ends with a ready-made `feedback:` line to fill in (see [Output](#output)).

## Environment

| Variable | Meaning |
| --- | --- |
| `SHARDFLUX_API_KEY` | Project API key (`sfk_<key id>_<secret>`) for the workspace commands. Default: the key `shard setup` saved (0.5.0+). |
| `SHARDFLUX_SESSION_TOKEN` | (0.5.0+) A person's CLI session (`sfu_...`) for the account commands. Default: the session `shard auth login` saved. |
| `SHARDFLUX_API_URL` | API base URL. Default `https://api.shardflux.dev`; `--api-url` overrides it. Plain `http://` is allowed only for loopback hosts. Credentials are saved per API URL. |
| `SHARDFLUX_CONFIG_DIR` | (0.5.0+) Directory of `credentials.json` and `update-check.json`. Default `$XDG_CONFIG_HOME/shardflux`, else `~/.config/shardflux` (`%APPDATA%\shardflux` on Windows). |
| `SHARDFLUX_NO_UPDATE_CHECK=1` | (0.5.0+) Never ask whether a newer `shard` exists (also `true`, `yes`, `on`). `NO_UPDATE_NOTIFIER=1` does the same. |
| `SHARDFLUX_AGENT_LABEL` | Attribution label for the tool tokens `shard` obtains. Default `cli`; `--agent-label` overrides it. Each label is one agent session, listed by `shard ws sessions`. |
| `SHARDFLUX_NO_WAKE=1` | Do not resume a suspended workspace on use (same as `--no-wake`; `--wake` overrides it). |
| `SHARDFLUX_WAKE_TIMEOUT_MS` | Longest wait, in milliseconds, for a workspace to wake or finish a transition. Default `120000`; `--wake-timeout` overrides it. |
| `SHARDFLUX_HTTP_KEEPALIVE=1` | Reuse HTTP connections (see [HTTP connections](#http-connections)). |
| `BROWSER` | (0.5.0+) Program that `--open` runs with a URL. Default `xdg-open`, `open` (macOS) or `rundll32` (Windows). |

`shard` never accepts a key, session or password as an argument (`--api-key`, `--token`, `--password` and similar
flags are refused with exit 2). It never prints the key or the session, except `shard env`, which prints the key on
purpose, and it redacts anything shaped like a key (`sfk_...`) or a session token (`sfu_...`) from error output.

## Credentials file

**Changed in 0.5.0: `shard` now stores credentials.** Before 0.5.0 it stored nothing and read the key from
`SHARDFLUX_API_KEY` on every run. That still works and still wins. The file is written only by the commands that sign
in or save something: `auth login`, `auth mfa`, `setup`, `api-keys create --save`, `orgs use`, `projects use`,
`invitations accept --use`, and the session renewals of step-up, password and two-factor changes. `shard login`, the
key check, still stores nothing.

- **Where:** `$SHARDFLUX_CONFIG_DIR/credentials.json`, else `$XDG_CONFIG_HOME/shardflux/`, else
  `~/.config/shardflux/` (Windows: `%APPDATA%\shardflux\`).
- **Protection:** the directory is created with mode 0700 and the file 0600, written atomically (a temporary file,
  then a rename). `shard` warns when the file is readable by others. A file that is not valid JSON, or not this format,
  is never overwritten silently: commands that read it stop with exit 2 naming it, and a command that writes
  credentials first moves it to `credentials.json.corrupt-<time>`.
- **Profiles:** one per API URL, so production, staging and a local API never mix. Each holds the session, the
  default organization and project, and the saved API key:

  ```json
  {"version": 1, "profiles": {"https://api.shardflux.dev": {
    "session": {"token": "sfu_...", "expires_at": "...", "user": {"id": "...", "email": "..."}, "state": "active"},
    "organization_id": "...", "project_id": "...",
    "api_key": {"token": "sfk_...", "id": "...", "key_id": "...", "project_id": "...", "name": "shard-cli laptop"}}}}
  ```

- **Precedence:** the project API key is `SHARDFLUX_API_KEY`, else the saved `api_key`. The session is
  `SHARDFLUX_SESSION_TOKEN`, else the saved `session`. `shard auth logout` removes the session (the key stays); delete
  the file to forget everything.

## Global options

| Option | Meaning |
| --- | --- |
| `--api-url <url>` | API base URL (default `$SHARDFLUX_API_URL`, else `https://api.shardflux.dev`). |
| `--json` | Machine-readable JSON on stdout; errors as JSON on stderr. |
| `--agent-label <label>` | Attribution label for workspace tool tokens (default `$SHARDFLUX_AGENT_LABEL`, else `cli`). |
| `--wake`, `--no-wake` | Resume a suspended workspace when `exec`, `files` or `changes` use it (default), or fail with `workspace_not_running`. |
| `--wake-timeout <ms>` | Longest wait for a workspace to wake or finish a transition (default `120000`); then exit 5. |
| `-h`, `--help` | Show help. |
| `-V`, `--version` | Print the version. |

Aliases: `ws` for `workspaces`, `ops` for `operations`, `secret` for `secrets`, `template` for `templates`, and
(0.5.0+) `org`/`organizations` for `orgs`, `project` for `projects`, `keys`/`api-key` for `api-keys`, `member` for
`members`, `invite`/`invitation` for `invitations`, `volume` for `volumes`. `shard workspaces files ...` and
`shard workspaces operations ...` also work.

**Naming a workspace.** `<id|key>` is tried as an id when it is a UUID. Otherwise, or if no workspace has that id, it
is matched as an exact workspace key across every lifetime and purpose, so sessions, drafts and test instances resolve
too. The live workspace wins over tombstones that ended sessions left with the same key.

**Durations.** `--timeout` takes a duration such as `90s`, `5m` or `30m`.

## Accounts and sign-in

(0.5.0+) These commands use a person's CLI session (`sfu_...`), which `shard auth login` saves.

```sh
printf '%s\n' "$PASSWORD" | shard auth register --email you@example.com [--name "Ada Lovelace"]
shard auth verify-email '<link from the email>'        # or just the token in it
shard auth resend-verification                         # signed in, email not verified yet
printf '%s\n' "$PASSWORD" | shard auth login --email you@example.com [--code 123456 | --recovery-code X]
shard auth mfa --code 123456                           # completes a sign-in that waits for its second factor
shard auth status                                      # session and saved key, without secrets
shard auth sessions [--revoke <session-id>]            # browser and CLI sessions
shard auth logout [--all]
printf '%s\n' "$PASSWORD" | shard auth step-up [--code 123456]
printf '%s\n%s\n' "$CURRENT" "$NEW" | shard auth password change
shard auth password reset --email you@example.com
printf '%s\n' "$NEW" | shard auth password reset-confirm '<link from the email>'
shard auth email change --email new@example.com        # then: shard auth email confirm '<link>'
shard auth totp enroll                                 # secret and otpauth:// URI for the authenticator app
shard auth totp confirm --code 123456                  # prints recovery codes once
shard auth totp disable --code 123456
shard auth totp recovery-codes
```

| Command | Options | Does |
| --- | --- | --- |
| `shard auth register` | `--email <email>` (required), `--name <name>` | Creates an account and emails a verification link. It answers the same whether or not the address exists. |
| `shard auth verify-email <link\|token>` | | Verifies the email address with the emailed link, or the token in it. |
| `shard auth resend-verification` | | Emails a new verification link (signed in, email not verified yet). That link also sets a new password: `auth verify-email` reads it from standard input (or asks), else prints the `auth password reset-confirm` command to run. |
| `shard auth login` | `--email <email>` (required), `--code <code>`, `--recovery-code <code>` | Signs in and saves the session for this API URL. Then prints who you are, whether the email is verified, your organizations and the next step (`shard setup`, or `shard auth verify-email`). |
| `shard auth mfa` | `--code <code>`, `--recovery-code <code>` | Completes a pending two-factor sign-in. |
| `shard auth status` | | The session (user, verification, two-factor, expiry, step-up) and the saved API key, without secrets. |
| `shard auth sessions` | `--revoke <session-id>` | Your live sessions, browser and CLI, or revokes one. |
| `shard auth logout` | `--all` | Ends the session and removes it from the credentials file; `--all` ends every session of the account. A saved API key stays. |
| `shard auth step-up` | `--code <code>`, `--recovery-code <code>` | Confirms the password (and the code, with two-factor authentication) for sensitive actions, for 10 minutes. |
| `shard auth password change` | | Current, then new password, from standard input lines or prompts. Other sessions end. |
| `shard auth password reset` | `--email <email>` | Emails a password reset link. |
| `shard auth password reset-confirm <link\|token>` | | Sets a new password (standard input or a prompt) with the emailed reset link. |
| `shard auth email change` | `--email <email>` | Sends a confirmation link to the new address (step-up). |
| `shard auth email confirm <link\|token>` | | Confirms the email change. |
| `shard auth totp enroll` | | Starts two-factor authentication: prints the secret and the `otpauth://` URI for the authenticator app. |
| `shard auth totp confirm` | `--code <code>` | Turns two-factor authentication on; prints the recovery codes once. |
| `shard auth totp disable` | `--code <code>`, `--recovery-code <code>` | Turns two-factor authentication off (step-up first; the password from standard input or a prompt). |
| `shard auth totp recovery-codes` | | Replaces the recovery codes; prints the new ones once. |

- **Passwords never come from arguments** (`--password` is refused). On a terminal `shard` asks without echo (a new
  password twice); otherwise it reads standard input, one line per secret. Emailed links and tokens are single-use
  arguments; quote a link. Two-factor codes and recovery codes may be arguments.
- **Sessions.** `auth login` saves the session in the [credentials file](#credentials-file). It lasts 30 days from its
  last use and at most 90 days. Step-up, password changes and two-factor changes renew it, and `shard` saves the new
  one at once. A saved session the API no longer accepts is removed, and the error says to sign in again. The person
  sees every CLI session in the console (Settings > Account & security) and can sign it out there.
- **Two-factor sign-in.** `--code` or `--recovery-code`, or the code typed at the prompt. Without either (and without
  a terminal) the pending sign-in is saved and `auth login` exits 4 with `mfa_required`; `shard auth mfa --code`
  completes it within 10 minutes.
- **Step-up.** Exports, deletions, email changes and two-factor changes need the password again within the last 10
  minutes. On a terminal `shard` asks (and for a code with two-factor authentication) and retries once. Otherwise the
  command exits 4 with `step_up_required`: run `printf '%s\n' "$PASSWORD" | shard auth step-up` first (with two-factor
  authentication add `--code`, or pipe the code as the second line).

## Setup

(0.5.0+) `shard setup` makes a signed-in account ready for the workspace commands:

1. The organization: `--org`, the saved default, or your only one. With none it creates "&lt;your name&gt;'s
   organization" (`--org-name` to choose); with several it lists them.
2. The project: `--project`, the saved default, or the organization's only one. With none it creates "Default"
   (`--project-name`).
3. An API key with every workspace tool (`--tool`, repeatable, narrows it), named `shard-cli <hostname>`
   (`--key-name`), saved in the credentials file and checked against the API.

It is idempotent: a saved key that still works is kept (`--new-key` makes a new one). It prints what it found or
created and the next command. The key is never printed; `shard env` prints it for tools that read `SHARDFLUX_API_KEY`
(the Python SDK, the MCP server, your own scripts):

```sh
eval "$(shard env)"                       # sh, bash, zsh
shard env --shell fish | source
shard env --shell powershell | Invoke-Expression
```

`shard env` prints the saved key and the API URL as `export` lines (`set -gx` for fish, `$env:` for PowerShell).

## Organizations, projects, keys and members

(0.5.0+) Account commands work on the default organization and project: `--org`/`--project` (an id or a slug), else
the one saved by `orgs use`, `projects use` or `setup`, else the only one you have. With several and no default, the
usage error lists them.

```sh
shard orgs list | create <name> [--slug S] [--use] | get [org] | use <org>
shard orgs export [org] [--out file]              # JSON, written 0600 (step-up)
shard orgs workspaces [org] [--project P] [--state S] [--all]   # every project's workspaces, as the console lists them
shard orgs deletion [org]                         # what deleting would do, and what blocks it
shard orgs delete <org> --confirm <slug> --yes    # irreversible (step-up)
shard projects list | create <name> [--slug S] [--use] | get <project> | use <project>
shard api-keys list | create <name> [--tool T]... [--read-only] [--expires 30d|90d|365d|never] [--save] | revoke <id|key_id> --yes
shard members list | set-role <user|email> <owner|admin|member|billing> | remove <user|email> --yes
shard invitations list | create <email> --role <role> | revoke <id|email> | accept '<link>' [--use]
shard templates publish <slug>@<version> | archive <slug>@<version>
```

| Command | Does |
| --- | --- |
| `shard orgs list`, `get`, `use` | Your organizations and your role in each; one with its plan; the default for account commands. |
| `shard orgs create <name>` | Creates an organization; you become its owner. `--use` makes it the default. |
| `shard orgs workspaces [org]` | The organization's workspaces in every project, as the console lists them (`--project`, `--state`, `--prefix`, `--lifetime`, `--purpose`, `--include-deleted`, `--all`). |
| `shard orgs export [org]` | The organization's data as JSON, to a file (owners; step-up). |
| `shard orgs deletion [org]`, `shard orgs delete <org>` | What deleting would do and what blocks it; the deletion itself (owners; step-up; `--confirm <slug> --yes`). An active subscription or an open checkout blocks it. |
| `shard projects list`, `create`, `get`, `use` | The organization's projects; creating one needs an owner or admin. |
| `shard api-keys list`, `create`, `revoke` | The project's API keys. `create` prints the new key once, alone on stdout (`KEY=$(shard api-keys create ci)`), with what it is on stderr; `--save` stores it in the credentials file instead of printing it. Revoking the saved key also removes it from the file. |
| `shard members list`, `set-role`, `remove` | Members and their roles: `owner`, `admin`, `member` or `billing`. `remove` yourself to leave. |
| `shard invitations list`, `create`, `revoke`, `accept` | Invitations by email (owners and admins). `accept` takes the emailed link; your verified email must be the invited one. |
| `shard templates publish`, `archive <slug>@<version>` | Publishes or archives a version of an organization template (owners and admins, signed in). The other template commands use the API key. |

With `--json`, `api-keys create` prints `{api_key, secret, saved: false}`, or `{api_key, saved: true,
credentials_file}` with `--save`.

## Billing

(0.5.0+)

```sh
shard billing plans                                  # public: no sign-in needed
shard billing status                                 # plan, billing state, period end, pending checkout
shard billing upgrade <plan> [--open] [--wait] [--timeout 15m]
shard billing portal [--open]                        # plan changes, payment methods, invoices, cancellation
shard billing invoices [--all]
shard billing alerts [--set 50,80,100 | --clear]     # usage alert emails, percent of each allowance
shard billing overage [status | on --cap <dollars> | cap <dollars> | off]   # (0.5.1+) opt-in overage and its spend cap
```

`billing overage` is described in [Usage and overage](#usage-and-overage).

`billing upgrade` checks the plan against the public catalog (an unknown one lists the plans you can buy), starts
Stripe Checkout and prints its URL: a person pays there. `--open` also opens it in the default browser (`$BROWSER`,
else `xdg-open`, `open` or `start`; best effort). The API activates the plan only when Stripe confirms the payment, so
`--wait` polls until the subscription is active (exit 0), the checkout expires or is canceled (exit 6,
`checkout_ended`), or `--timeout` runs out (exit 5; the checkout stays open). An organization that already has a
subscription changes plans in the billing portal: the command prints the portal URL instead (exit 0). Buying a plan
needs an owner or billing member.

## Audit, exports and account deletion

(0.5.0+)

```sh
shard audit list [--action api_key.] [--from 7d] [--to <time>] [--source app|cell_tool] [--outcome denied] [--all]
shard audit export [--format ndjson|csv] [--out file] [filters]     # at most 90 days, 50 000 rows
shard account status                                                # profile, memberships, deletion state
shard account export [--out file]                                   # your data as JSON (step-up)
shard account delete --confirm you@example.com --yes                # after a grace period (step-up)
shard account cancel-deletion
```

`--from` and `--to` take an RFC 3339 time or a duration ago (`90m`, `24h`, `7d`, `2w`). Files that `shard` writes for
exports are created with mode 0600 and never overwritten without `--force`. The audit log belongs to owners and
admins. Deleting the account is refused while you are the only owner of an organization with other members, or the
only member of one: transfer or delete those first (`shard account status` lists them).

## Key, usage and version commands

These use the project API key: `SHARDFLUX_API_KEY`, else the key `shard setup` saved.

| Command | Does |
| --- | --- |
| `shard login` | Verifies the key and shows its organization, project and tool permissions. It stores nothing. |
| `shard whoami` | Shows the principal behind the key. |
| `shard usage [--org <organization-id>]` | Usage summary for the current period: plan, meters and allowances, and the [overage](#usage-and-overage) block (0.5.1+). |
| `shard usage --estimate` | (0.5.0+) The bill so far and the projected use of each allowance, with the API's notes. |
| `shard usage --series` | (0.5.0+) Usage over time: `--from`, `--to`, `--granularity hour\|day`, `--meter <meter>`. |
| `shard usage --workspace <id\|key>` | (0.5.0+) One workspace's usage over time, with the same range options. |
| `shard version [--check]` | The CLI and SDK versions and the update status; `--check` asks the API now (see [Updates](#updates)). |

## Workspaces

### shard workspaces open

```sh
shard workspaces open <key> --template <slug> [options]
```

Opens a workspace by key: creates it on first use, reconnects to or resumes it afterwards, never resets it. Waits
until the workspace is ready, the template's start commands and services included, unless `--no-wait`.

| Option | Meaning |
| --- | --- |
| `--template <slug>` | Template slug (required), for example `python-node-browser`. |
| `--secret <NAME>` | Bind this secret name (repeatable). Replaces the binding of an existing workspace; omit to leave it unchanged. |
| `--input <NAME=value>` | (0.4.0+) Text input of the template version (repeatable). Replaces the inputs of an existing workspace. Not for credentials: those are secret inputs. |
| `--input-file <file>` | (0.4.0+) `NAME=value` lines (`#` comments) or a JSON object. `--input` wins for a name in both. |
| `--lifetime <persistent\|session>` | `session`: the workspace is discarded when the session ends (`shard ws close` or the idle timeout), and the key then opens a new one. Default: the template's default, else `persistent`. |
| `--cpu-millis <n>`, `--memory-mib <n>`, `--disk-gib <n>` | Caps in millicores, MiB and GiB. |
| `--mode <processful\|file-first>` | (0.5.0+) `file-first` opens a [file-first workspace](#file-first-workspaces) (`file_first` works too). Default: `processful` for a new key, the stored mode for an existing one. With `--lifetime session` it is a usage error. |
| `--wait`, `--no-wait` | Wait until ready (default), or return at once. |
| `--timeout <duration>` | Give up waiting after this long (default `5m`). The start continues server side. |
| `--timing` | Print where the time went (see [Timing](#timing)). |

### Other workspace commands

| Command | Options | Does |
| --- | --- | --- |
| `shard workspaces list` | `--state <state>`, `--prefix <key-prefix>`, `--lifetime <persistent\|session\|any>`, `--purpose <standard\|template_draft\|template_test\|any>`, `--include-deleted`, `--all`, `--limit <n>` (1-200, default 50), `--cursor <cursor>` | One page, or every page with `--all`. The next cursor is printed on stderr. By default only persistent standard workspaces. A file-first workspace reads `running (file-first)` (0.5.0+). |
| `shard workspaces get <id\|key>` | | One workspace (deleted ones included), with the startup of its template's start commands and services (0.4.0+), its mode and a file-first workspace's tree revision (0.5.0+), and a pending suspend when idle (0.5.1+). |
| `shard workspaces inputs <id\|key>` | | (0.4.0+) The workspace's text inputs. |
| `shard workspaces exec <id\|key> -- <command> [args...]` | `--cwd <dir>` (an absolute path, default `/home/user`; a relative one is refused with `invalid_cwd`, exit 2), `--env <K=V>` (repeatable), `--timeout <duration>` (exit 124), `--stdin <file\|->` (max 1 MiB), `-v` / `--verbose` (0.5.0+), `--execution-id <id>` and `--output-limit <bytes>` (0.5.0+, file-first only), `--timing` | Runs argv with no shell (use `-- bash -lc "..."` for shell syntax). Output is streamed and resumes from byte offsets after dropped connections. Exits with the command's exit code; Ctrl-C cancels the command. `-v` then prints the session id and exit status on stderr. On a file-first workspace the command runs as an execution instead (see [File-first workspaces](#file-first-workspaces)). (0.5.1+) A command that could not start (a `--cwd` that is not a directory, a program not on `PATH`) exits 6 with the workspace's reason and a hint; before 0.5.1 it exited 1 with no output. |
| `shard executions get <id\|key> <execution-id>` | `--wait <duration>` | (0.5.0+) A file-first workspace's execution (also `shard workspaces executions get`): its output, then what ran. Exits like `exec`; 5 while it still runs. |
| `shard workspaces suspend <id\|key>` | `--wait`, `--timeout <duration>` (default `5m`), `--timing`; `--when-idle <duration>` (30s to 1h) or `--cancel-when-idle` (0.5.1+) | Suspends a running workspace (durable checkpoint). Returns at once unless `--wait`. `--when-idle` (0.5.1+) [suspends it when idle](https://docs.shardflux.dev/concepts/lifecycle.md#suspend-when-idle) instead: once it has been idle that long, counted from the later of its last work and the request; the next tool call or a resume cancels it, and a running command, attached stream or keepalive postpones it. `--cancel-when-idle` cancels that. Neither combines with `--wait` or `--timeout` (exit 2). `--json` prints `{workspace, operation, suspend_request}`. |
| `shard workspaces resume <id\|key>` | `--wait`, `--timeout`, `--timing` | Resumes a suspended workspace. |
| `shard workspaces fork <id\|key> <new-key>` | `--cpu-millis`, `--memory-mib`, `--disk-gib`, `--wait`, `--timeout`, `--timing` | Forks into a new key. |
| `shard workspaces delete <id\|key>` | `--yes` (required), `--wait`, `--timeout`, `--timing` | Deletes the workspace. Tool access ends at once; storage is cleaned up asynchronously; the key of a persistent workspace is never reused. |
| `shard workspaces close <id\|key>` | `--wait`, `--timeout`, `--timing` | Ends a session workspace now (it is deleted; the key then opens a new workspace). A persistent workspace is refused (`not_session`). |
| `shard workspaces reset <id\|key>` | `--yes` (required), `--wait`, `--timeout`, `--timing` | Layered workspaces: wipes every change and restarts on the template. Keeps key, id, template version, caps, secret bindings and volume attachments. The previous state stays restorable for 7 days. |
| `shard workspaces save-as-template <id\|key>` | `--template <slug>` (required), `--display-name <name>`, `--description <text>`, `--checkpoint <checkpoint-id>`, `--default-lifetime <persistent\|session>`, `--default-idle-timeout <duration>` (60s-24h), `--acknowledge <path>` (repeatable, up to 200), `--auto-publish` / `--no-auto-publish`, `--wait`, `--timeout` (default `30m`) | Saves a layered workspace as the next version of an organization template. `--wait` follows the build. |
| `shard workspaces changes <id\|key>` | `--path <prefix>`, `--hash`, `--summary`, `--all`, `--limit <n>` (1-1000), `--cursor <cursor>`, `--timing` | What a layered, running workspace changed against its template: `added`, `modified`, `metadata` (with `--hash`), `deleted`, `replaced`. |
| `shard workspaces sessions <id\|key>` | | Attributed agent sessions: label, principal and tokens issued. |
| `shard workspaces secrets <id\|key>` | `--set <NAME>` (repeatable), `--clear` | Shows the secret names bound to the workspace and their status, or replaces them. Values are never shown. |

## Files

| Command | Options | Does |
| --- | --- | --- |
| `shard files read <id\|key> <path>` | `--out <file>`, `--timing` | Raw bytes to stdout, or to a local file. |
| `shard files write <id\|key> <path>` | `--from <file\|->` (default `-`, standard input), `--append`, `--parents` / `--no-parents`, `--if-revision <n>` (0.5.0+, file-first only), `--timing` | Atomic write, durable once acknowledged. Missing parent directories are created unless `--no-parents`. |
| `shard files ls <id\|key> <path>` | `--limit <n>` (default 1000), `--timing` | Directory listing. |
| `shard files stat <id\|key> <path>` | `--revision`, `--timing` | (0.5.0+) One path's metadata. `--revision` adds the content's SHA-256, the value `--expected-revision` takes. |
| `shard files search <id\|key> <path> <pattern>` | `--regex`, `-i` / `--ignore-case`, `--include <glob>` and `--exclude <glob>` (repeatable), `--max <n>` (1-5000, default 200), `--timing` | (0.5.0+) Searches file contents under a directory (or in one file): `path:line:column: text` per match. A search stopped early says why on stderr. `--exclude` replaces the default `.git` and `node_modules` exclusions. |
| `shard files patch <id\|key> <path>` | `--old <text> --new <text>` (repeatable pairs), `--replace-all`, or `--content-file <file\|->`; `--expected-revision <sha256\|absent>`, `--if-revision <n>` (file-first only), `--timing` | (0.5.0+) Replaces exact text (each `--old` must occur once unless `--replace-all`; in order, all or none), or the whole content, atomically. Prints the new revision. |

See [Search and edit files](https://docs.shardflux.dev/guides/files.md) for search, patches and revisions. `files read`, `ls`, `stat` and `search`
of a suspended workspace are answered from its disk without waking it (0.5.0+; `files search` notes it on stderr).
While the fleet is upgraded, a workspace on a host without search or patches gets `host_feature_unavailable` (exit 1,
not retried), with a hint naming another way.

## File-first workspaces

(0.5.0+) A [file-first workspace](https://docs.shardflux.dev/concepts/file-first.md) has no VM between commands; its state is a versioned file tree
under `/home/user`. Each `shard ws exec` runs the command as an execution: a fresh VM on the latest tree revision,
whose changed files become the next revision.

```sh
shard ws open acme/agent-7 --template python-node-browser --mode file-first
shard files write acme/agent-7 /home/user/app/main.py --from main.py     # prints the new tree revision
shard ws exec acme/agent-7 -v -- python3 /home/user/app/main.py
shard ws exec acme/agent-7 --execution-id build-42 --timeout 10m -- bash -lc "make -C /home/user/app"
shard executions get acme/agent-7 build-42                              # the recorded result, again
```

- **Output** is printed when the command has ended, not streamed: stdout and stderr byte for byte.
  `--output-limit <bytes>` keeps at most that much of each (default 1 MiB, at most 16 MiB).
- **Exit code:** the command's own (124 timed out, 128+N killed by signal N). An execution that ended `failed` or
  `lost` published nothing and exits 6, naming its reason and whether a new execution may succeed.
- **`-v`** prints on stderr, after the output, the execution id, its state and exit status, the tree revision before
  and after (`1 -> 2`), `replayed`, the timings and the changed paths (`A`, `M`, `D`; the first 100).
- **`--execution-id <id>`** is the execution's idempotency key (default a fresh `ex-<uuid>`): the same id with the same
  command returns the recorded result instead of running it again, so a script can repeat the call safely.
  `shard` sends the request again with the same id after a dropped connection and after `no_execution_host`, and waits
  within `--wake-timeout` while another execution holds the workspace.
- **Ctrl-C** stops waiting (exit 130). An execution cannot be canceled; `shard` prints the `shard executions get`
  command that fetches its result later. Results are kept for 7 days.
- **Tree revisions:** `shard ws get` shows the current one, and `files write` and `files patch` print the revision
  after their change. `--if-revision <n>` applies the change only while the tree is at revision n; otherwise nothing
  changes, `shard` exits 1 and the hint names the current revision.
- **Not in file-first mode:** `workspaces suspend`, `resume`, `fork`, `reset`, `save-as-template` and `changes` are
  refused without a request (`not_supported_for_mode`, exit 1). `--execution-id`, `--output-limit` and
  `--if-revision` on a processful workspace are usage errors (exit 2).
- **Opening:** the other mode for an existing key is `mode_mismatch` (exit 1), a deployment without file-first
  workspaces answers `mode_not_available` (exit 2), and a template version without layered disks
  `layout_unsupported` (exit 1). Each comes with a hint.

## Operations

| Command | Options | Does |
| --- | --- | --- |
| `shard operations list <id\|key>` | `--state <state>`, `--kind <kind>`, `--limit <n>` (1-200, default 50) | A workspace's operations, newest first. |
| `shard operations get <operation-id>` | | One operation. |
| `shard operations wait <operation-id>` | `--timeout <duration>` (default `5m`), `--timing` | Waits for an operation to finish: exit 0 succeeded, 6 failed or canceled, 5 timeout. |

## Secrets

```sh
printf %s "$OPENAI_API_KEY" | shard secrets create OPENAI_API_KEY     # value from standard input
shard secrets create DATABASE_URL --from-file ./database-url.txt      # or from a file
shard ws open acme/demo --template python-node-browser --secret OPENAI_API_KEY --secret DATABASE_URL
shard ws exec acme/demo -- python3 agent.py                           # sees both as environment variables
shard ws secrets acme/demo                                            # names and status, never values
printf %s "$NEW_KEY" | shard secrets rotate OPENAI_API_KEY
```

| Command | Options | Does |
| --- | --- | --- |
| `shard secrets create <name>` | `--from-file <file>`, `--description <text>`, `--tool <tool>` (repeatable; default `exec` and `pty`), `--allow-workspace <workspace-id>` (repeatable), `--org <organization-id>`, `--allow-project <project-id>` (repeatable), `--all-projects` | Creates a secret. The value (UTF-8, up to 64 KiB) comes from standard input or `--from-file`, never from an argument. |
| `shard secrets list` | `--org`, `--include-deleted`, `--all`, `--limit <n>`, `--cursor <cursor>` | Secret metadata; values are never shown. |
| `shard secrets get <id\|name>` | `--org` | One secret's metadata. |
| `shard secrets update <id\|name>` | `--org`, `--description`, `--tool`, `--allow-workspace`, `--any-workspace`, `--allow-project`, `--all-projects`, `--clear-projects` | Changes the description or usage permissions, from the next session start. |
| `shard secrets rotate <id\|name>` | `--org`, `--from-file <file>` | Stores a new value as the next version; older values are erased. |
| `shard secrets versions <id\|name>` | `--org`, `--limit <n>` | Versions, newest first. |
| `shard secrets delete <id\|name>` | `--org`, `--yes` (required) | Erases the values and removes the name from every workspace binding. |
| `shard secrets access-log <id\|name>` | `--org`, `--outcome <granted\|denied\|failed>`, `--workspace <workspace-id>`, `--limit <n>` | Which sessions resolved a secret. |

- Values are read only from standard input or `--from-file`, with one trailing newline removed. `--value`,
  `--password` and similar flags are refused.
- Secrets are project secrets of the API key's project by default. `--org` addresses organization-wide secrets; those,
  and `secrets access-log`, belong to organization owners and admins, so a project API key gets 403 (exit 4).
- `<id|name>` is tried as an id when it is a UUID, otherwise matched by name in the project (or the organization with
  `--org`).
- A secret name that is unknown, or one the workspace may not use, is refused on open (exit 2,
  `secret_not_available` with the names).

## Egress and volumes

(0.5.0+) These use the project API key, like the other workspace commands.

```sh
shard egress get [--project <id> | --workspace <ws>]
shard egress set --file policy.json [--project <id> | --workspace <ws>] [--if-version N]
shard egress history [--project <id> | --workspace <ws>]
shard egress clear --workspace <ws>
shard volumes list [--include-org-shared] | create <name> --quota-gib N [--org-shared] | get <id|name>
shard volumes attach <ws> <volume> --mount /mnt/data [--read-only] | detach <ws> <volume>
shard volumes attachments <id|name> | delete <id|name> [--force] --yes
```

- A policy file is `{"mode": "allow_all" | "allowlist" | "deny_all", "rules": [{"host": "api.github.com", "ports":
  [443]}], "cidrs": []}`. `egress get --workspace` also shows the enforcement; `egress clear` makes the workspace
  follow the project policy again.
- Volume commands wait until the cell has finished unless `--no-wait`. `volumes delete` is refused while the volume
  is attached unless `--force`.

## Templates

### Build a template from template.yaml

(0.4.0+) See [Build a template](https://docs.shardflux.dev/guides/build-a-template.md) for the recipe format.

```sh
shard templates init --base ubuntu-24.04            # writes template.yaml on the base's current version
shard templates languages --base ubuntu-24.04@1     # what build.languages offers there
shard templates packages search apt ffmpeg --base ubuntu-24.04@1
shard templates build template.yaml --slug acme-dev --wait --timing
shard templates test acme-dev@1 --input PROJECT_NAME=demo
shard templates build template.yaml --slug acme-dev --publish --wait
shard ws open acme/main --template acme-dev --input PROJECT_NAME=acme
```

- Local `from` files and folders are uploaded first. Folders are packed as a reproducible tar (the same bytes the
  SDKs pack), and bytes the organization already has are not sent again. Progress lines go to stderr.
- The version stays unpublished unless `--publish`: try it with `shard templates test <slug>@<version>`. Without
  `--slug`, the slug is the name of the directory holding the file.
- `--wait` follows the build until it is registered or fails (exit 6 when it failed or was canceled, 5 when
  `--timeout` ran out; the build continues). A host the build network refused is listed under `denied hosts`: add it
  to `build.network.extra_hosts` and build again.
- A file `shard` cannot read or pack is refused before any request (exit 2). The API's refusals (`upload_missing`,
  `language_unavailable`, `invalid_settings`, ...) exit 2 with the reason. A storage refusal of an upload is
  `upload_failed` (exit 1).

### Template commands

| Command | Options | Does |
| --- | --- | --- |
| `shard templates list` | `--owner <platform\|organization>`, `--include-archived`, `--all`, `--limit <n>`, `--cursor <cursor>` | Templates the project can open, with their open version, layouts, file list state and draft. |
| `shard templates get <slug>` | `--owner`, `--include-archived` | (0.4.0+) Versions, what open resolves to, and the open version's settings. |
| `shard templates init` | `--base <slug[@version]>` (required), `--out <file>` (default `template.yaml`), `--force` | (0.4.0+) Writes a starter `template.yaml` on a base. |
| `shard templates build <file>` | `--slug <slug>`, `--display-name <name>`, `--description <text>`, `--publish`, `--acknowledge <path>` (repeatable, up to 200), `--wait`, `--timeout <duration>` (default `30m`), `--timing` | (0.4.0+) Builds a template from `template.yaml`. |
| `shard templates export <slug@version>` | `--out <file>`, `--force`, `--owner` | (0.4.0+) The `template.yaml` a version was built from (exit 3 for a version without a recipe). |
| `shard templates test <slug@version>` | `--instance-key <key>`, `--input <NAME=value>`, `--input-file <file>`, `--cpu-millis`, `--memory-mib`, `--disk-gib`, `--wait` / `--no-wait`, `--timeout` (default `5m`), `--timing` | (0.4.0+) A test instance of a version, published or not: a session workspace that runs its start commands and services. End it with `shard ws close <key>`. |
| `shard templates builds list` | `--template <slug>`, `--state <state>`, `--all`, `--limit`, `--cursor` | (0.4.0+) The organization's template builds, newest first. |
| `shard templates builds get <build-id>` | | (0.4.0+) One build: state, registration, denied hosts, failure. |
| `shard templates builds log <build-id>` | `--out <file>` | (0.4.0+) A build's full log. |
| `shard templates builds cancel <build-id>` | | (0.4.0+) Cancels a build (queued: at once; running: honoured until publishing starts). |
| `shard templates languages` | `--base <slug@version>` (required) | (0.4.0+) Languages and versions a base offers `build.languages`. |
| `shard templates packages search <apt\|pip\|npm> <query>` | `--base <slug@version>` (apt only, required there), `--limit <n>` (1-50, default 20) | (0.4.0+) Package names for `build.packages`. |
| `shard templates files <slug> <version> [path]` | `--stat`, `--owner`, `--all`, `--limit <n>` (1-1000, default 200), `--cursor` | One directory of a version's file tree, or one entry with `--stat`. |
| `shard templates diff <slug>` | `--from <version\|base>` (required), `--to <version>` (required), `--prefix <path>`, `--change <kind>`, `--owner`, `--all`, `--limit`, `--cursor` | Diff two versions: `added`, `removed`, `changed`, `type_changed`, `metadata`. |

### Drafts

| Command | Options | Does |
| --- | --- | --- |
| `shard templates draft open <slug>` | `--base <slug@version>`, `--cpu-millis`, `--memory-mib`, `--disk-gib`, `--wait` / `--no-wait`, `--timeout`, `--timing` | Opens the template's draft, a layered workspace you edit with `shard ws exec` and `shard files`. One live draft per template. |
| `shard templates draft status <slug>` | | The draft's workspace, base version, states and live test instances. |
| `shard templates draft state <slug>` | `--label <text>`, `--list`, `--wait`, `--timeout`, `--timing` | Captures a state of the running draft, or lists its states. |
| `shard templates draft test <slug>` | `--state <state-id>`, `--instance-key <key>`, `--list`, `--include-ended`, caps, `--wait` / `--no-wait`, `--timeout`, `--timing` | Opens a test instance (a session on a copy of a state), or lists them. |
| `shard templates draft publish <slug>` | `--state <state-id>`, `--description`, `--default-lifetime`, `--default-idle-timeout`, `--acknowledge`, `--auto-publish` / `--no-auto-publish`, `--wait`, `--timeout` (default `30m`) | Publishes the draft as the template's next version. Refused (`draft_stale`) when the template got a version from elsewhere. |
| `shard templates draft discard <slug>` | `--yes` (required), `--wait`, `--timeout`, `--timing` | Deletes the draft and ends its live test instances. |

## Suspended workspaces

`workspaces exec`, `files read|write|ls|stat|search|patch` and `workspaces changes` wake a suspended workspace: they
resume it (or join the resume or open already running), wait until it runs, then run the request. A request made during
a suspend or resume waits for the transition. The request itself never runs twice, because the workspace refused it
before doing anything.

- (0.5.0+) The wake is one request: the API holds the resume until the workspace runs and answers with the CLI's tool
  token. `shard ws resume --wait` is the same request.
- (0.5.0+) `files read`, `ls`, `stat` and `search` of a suspended workspace whose disk a host still holds are answered
  from that disk without waking it: the files as they were at the suspend.
- A file-first workspace is never suspended, so none of this applies to it.

- `--wake-timeout <ms>` bounds the wait. Past it, `shard` exits 5 and names the operation and its state. The operation
  continues server side: `shard operations wait <id>` keeps waiting.
- A resume or open that fails exits 6.
- `--no-wake` (or `SHARDFLUX_NO_WAKE=1`) turns this off: a workspace that is not running is refused at once (exit 1,
  `workspace_not_running`).

## Starts that wait for capacity

An open, resume or fork that no host can admit yet waits in `capacity_pending` for at most 15 minutes. If `--timeout`
ends first, the timeout error names the time the start gives up. A start still pending then fails with
`capacity_unavailable`: nothing was started, and a suspended workspace stays suspended. `shard` exits 6 as for any
failed operation, and the error says `retryable=true` with a hint to retry later:

```text
error: Operation 01a0df37-b3a9-7a14-a3e7-11395c349c39 (open) failed: capacity_unavailable (no_ready_hosts) [code=operation_failed operation_id=01a0df37-b3a9-7a14-a3e7-11395c349c39 retryable=true]
hint: no host could admit it before its deadline; nothing was started. Retry later.
```

`shard` does not retry it for you. A definitive failure has `retryable=false`.

## Usage and overage

`shard usage` prints the current period's plan, meters and allowances. With [opt-in overage](https://docs.shardflux.dev/limits.md#overage-opt-in)
(0.5.1+), an allowance past its included amount while overage is on shows cap state `overage`, and a block follows:
the overage state, the spend cap, the charges of the cap, the date the cap is projected to be reached, and per
allowance the units past it and their amount.

```text
overage          accruing (usage past the allowances is charged on the next invoice)
spend cap        $9.00 (at most $9.00, the plan price)
overage charges  $3.00 of $9.00 (33.33%), $6.00 left; billed on the next invoice
cap reached      2026-09-29 15:02:46Z (projected at this period's average use)

OVERAGE        UNIT       PAST ALLOWANCE  BILLED  RATE   AMOUNT
ram_gib_hours  gib_hours  70              60      $0.04  $2.40
cpu_hours      hours      5               5       $0.12  $0.60
```

`--json` prints the API's summary, with `spend_cap` and `exhausted_reason`.

Owners and billing members turn overage on and set the cap with `shard billing overage` (0.5.1+, signed in with
`shard auth login`) or under **Usage & billing** in the console:

| Command | Does |
| --- | --- |
| `shard billing overage [status]` | The state (`unavailable`, `off`, `on`, `paused`), the spend cap and its range ($1 up to the plan price) and the rates. |
| `shard billing overage on [--cap <dollars>]` | Turns overage on. A cap is needed the first time, e.g. `--cap 9` or `--cap 9.50`. |
| `shard billing overage cap <dollars>` | Changes the spend cap per billing period. |
| `shard billing overage off` | Turns overage off (always allowed; what it charged stays on the next invoice). |

They take `--org <id|slug>`, `--json` (the API's spend policy) and `--if-version <n>` (the policy `version`: a
concurrent change is refused with `version_mismatch`, exit 1). A refused change exits 2 with the API's reason and a hint
that names the bound: `spend_cap_above_plan_price` (the plan price), `spend_cap_below_minimum` ($1),
`spend_cap_below_charges` (what overage already charged this period), `overage_unavailable` (the plan has no overage)
or `spend_cap_required` (turning it on without a cap).

```text
$ shard billing overage on --cap 9
Acme: overage is on, spend cap $9.00 per billing period ($1.00 to $9.00, the plan price).
Rates past the allowances: $0.04 per RAM GiB-hour, $0.12 per CPU-hour, charged on the next invoice (this period: shard usage).
Saved (policy version 1).
```

While an allowance is used up, opens, resumes and forks are refused with 402 `allowance_exhausted` (exit 1). The error
names the reason, and a hint says what to do (0.5.1+):

| Reason | Hint |
| --- | --- |
| `allowance_used` | Upgrade (`shard billing plans`), or turn on overage: `shard billing overage on --cap <dollars>`. |
| `spend_cap_reached` | The spend cap is reached: raise it (`shard billing overage cap <dollars>`, at most the plan price) or upgrade. |
| `overage_paused` | A payment is past due: update the payment method (`shard billing portal --open`). |

## Timing

`--timing` prints, on stderr after the call, the lifecycle timing the SDK measured. It is printed when the call fails,
times out or is interrupted too, before the error, and the exit code does not change.

```text
$ shard ws open acme/demo --template python-node-browser --timing
Workspace acme/demo (01a0e5a8-3edd-74ba-b489-d62b8925e342) is ready.
...
open 34.18 s, succeeded (workspace 01a0e5a8-3edd-74ba-b489-d62b8925e342, operation 01a0e5a8-3ef0-7ecb-975e-dff2d5ca6e33)
  client: request 20.01 s (held) → capacity_pending 13.52 s (no_ready_host) → running 0.59 s → view 42 ms ∥ token 61 ms
  server: queued 33.40 s, ran 0.62 s, total 34.02 s; start warm, boot to ready 79 ms
  outside the server: 0.16 s
```

| Command | What is timed |
| --- | --- |
| `workspaces open`, `templates test`, `templates draft open\|test` | The open, including the wait unless `--no-wait`. |
| `templates build` | Packing, uploads, the request and the wait, then the build's own queued and run time. |
| `workspaces suspend\|resume\|fork\|delete\|close\|reset`, `templates draft state\|discard` | With `--wait`, the request and the whole wait; without it, the request. |
| `operations wait` | The wait. |
| `workspaces exec`, `files read\|write\|ls\|stat\|search\|patch`, `workspaces changes` | The wake, when the command had to wake a suspended workspace. Nothing is printed otherwise. |
| `workspaces exec` on a file-first workspace (0.5.0+) | The execution's own timings as the cell reports them; nothing is woken. |

With `--json`, stdout carries the timing as a `timing` field of the command's JSON instead: `action`, `outcome`,
`workspace_id`, `operation_id`, `total_ms`, `phases`, `retries`, `server` and `outside_server_ms`. It is `null` when
the command made no lifecycle call. On a failure the error document on stderr carries it: `{"error": {...},
"timing": {...}}`. `shard` does not stream progress while it waits.

## Output

**Human (default).** Aligned tables and key/value blocks on stdout. Errors go to stderr as
`error: <message> (<reason>) [code=... status=... request_id=... operation_id=...]`, with a hint when there is one. A
failed operation adds `retryable=true|false`. **(0.5.0+)** An unexpected failure ends with a line that reports it:
`feedback: shard feedback --category bug --request-id <id> --error-code <code> "<what you expected>"`. The expected
flow has none: usage and validation errors, Ctrl-C, a missing or refused credential, a sign-in or step-up to finish, a
wait that gave up, and refusals whose hint names the next step (a stale revision, an edit that does not match, a mode
the workspace lacks).

**`--json`.** stdout carries the API's own objects:

| Command | JSON on stdout |
| --- | --- |
| `workspaces open`, `workspaces get` | The workspace view. |
| `workspaces list` | `{data, next_cursor}` |
| Lifecycle commands | The operation. `fork` prints `{operation, workspace}`. |
| `login`, `whoami` | `{api_url, organization_id, project_id, api_key: {id, key_id}, tool_permissions}` |
| `workspaces exec` | `{session_id, exit_code, term_signal, timed_out, canceled, stdout, stderr, truncated, reconnects}`; (0.5.1+) also `state` (`exited`, or `lost` without an exit code) and `error` when the session has one |
| `workspaces exec`, `executions get` on a file-first workspace (0.5.0+) | `{execution_id, state, exit_code, term_signal, timed_out, base_revision, tree_revision, changed, changed_truncated, stdout, stdout_truncated, stderr, stderr_truncated, replayed, timings, error, created_at, finished_at}`; `stdout_base64` / `stderr_base64` too when a stream is not UTF-8 |
| `files read` | `{path, bytes, content_base64}` |
| `files write` | The write result (`tree_revision` added on a file-first workspace). |
| `files stat` (0.5.0+) | The file info, with `revision` when asked. |
| `files search` (0.5.0+) | `{matches, truncated, stop_reason, files_scanned, served_from}` |
| `files patch` (0.5.0+) | `{path, revision, previous_revision, bytes_written, durable, replacements, file}` (`tree_revision` added on a file-first workspace) |
| `secrets create`, `get`, `update` | The secret's metadata. |
| `secrets list`, `versions`, `access-log` | `{data, next_cursor}` |
| `secrets rotate` | `{secret, version}` |
| `secrets delete` | `{deleted, id, name, scope}` |
| `workspaces secrets` | `{workspace_id, names, secrets}` |
| Account lists (0.5.0+) | `{data, next_cursor}`, with `default_organization_id`, `default_project_id` or `saved_api_key_id` where a default applies |
| `auth login`, `auth mfa` (0.5.0+) | `{status, user, session: {expires_at, credentials_file}, organizations, next}`, never the token |
| `auth status` (0.5.0+) | `{api_url, credentials_file, session, api_key, defaults}` |
| `setup` (0.5.0+) | `{organization, project, api_key: {id, key_id, name, project_id, tool_permissions}, created: {organization, project, api_key}, credentials_file, next}` |
| `api-keys create` (0.5.0+) | `{api_key, secret, saved: false}`, or `{api_key, saved: true, credentials_file}` with `--save` |
| `billing upgrade` (0.5.0+) | `{subscription_exists: false, organization_id, plan_key, checkout, url, opened, subscription_active}`, or `{subscription_exists: true, organization_id, plan_key, portal_url, opened}` |
| Exports (0.5.0+) | `{export, out, bytes}` |
| `env` (0.5.0+) | `{shell, variables}` |
| `version` | `{cli, sdk, update}` (`update` 0.5.0+) |
| `feedback` (0.5.0+) | `{id, received_at, duplicate}` |

Errors go to stderr as one JSON document, `{"error": {...}}`. **(0.5.0+)** An unexpected failure adds a sibling field
`"feedback"` with the `shard feedback` command that reports it (the same failures that print a `feedback:` line):

| Error | `error` fields |
| --- | --- |
| An API or cell gateway refusal | `code`, `message`, `status`, `request_id`, `retryable`, `operation_id`, `details`, `source` (`api` or `cell`): the [error envelope](https://docs.shardflux.dev/reference/errors.md) |
| A failed operation | `code: "operation_failed"`, `message`, `operation_id`, `retryable` (the operation error's own flag), `details: {error_code, reason}`, `operation` (the full operation) |
| A wait that gave up | `code: "timeout"`, `message`, `operation_id`, `last_state`. For `billing upgrade --wait` (0.5.0+): `details: {checkout_id, status, url}`; the checkout stays open. |
| A checkout that ended (0.5.0+) | `code: "checkout_ended"`, `message`, `details: {checkout_id, status}` (`expired`, `canceled` or `failed`) |
| A failed or lost execution (0.5.0+) | `code: "execution_failed"`, `message`, `execution_id`, `retryable`, `details: {reason, error_code, state}`, after the execution on stdout. `executions get` of one still running: `code: "timeout"`, `execution_id`, `last_state`. |
| Local failures | `code`: `usage`, `unauthenticated` (no key, or no session for an account command), `mfa_required` (0.5.0+: a sign-in waits for its second factor), `not_found` (no workspace or secret by that id, key or name; a version without a recipe to export), `interrupted`, `upload_failed` (`details.sha256`, `details.storage_code`), `protocol_error`, `network_error` or `internal_error` |

## Exit codes

| Code | Meaning |
| --- | --- |
| 0 | Success. |
| 1 | Error: an API refusal such as `conflict`, `quota_exceeded` or `workspace_not_running`, or a network or protocol failure. |
| 2 | Usage error: bad arguments, or input the API rejected as `validation_failed`. |
| 3 | Not found: workspace, operation or route. |
| 4 | Authentication or authorization: missing, malformed, revoked or insufficient key or session. (0.5.0+) Also `step_up_required` (run `shard auth step-up`), `mfa_required` (a sign-in waiting for its code), `email_unverified`, `invalid_credentials` and `token_invalid` (an emailed link that is invalid, expired or already used). |
| 5 | Timeout: waiting gave up (`--timeout`, or `--wake-timeout` while waking). The operation continues server side (`shard operations wait <id>`). `billing upgrade --wait` (0.5.0+): nobody paid in time; the checkout stays open. `executions get` (0.5.0+): the execution is still queued or running. |
| 6 | The awaited operation ended `failed` or `canceled` (also a resume or open that failed while waking). `retryable=true` in the error means it may succeed later (`capacity_unavailable`). `billing upgrade --wait` (0.5.0+): the checkout expired or was canceled (`checkout_ended`). `exec` and `executions get` on a file-first workspace (0.5.0+): the execution ended `failed` or `lost` (`execution_failed`). `exec` on a processful workspace (0.5.1+): the command could not start (`exec_failed_to_start`). |
| 130 | Interrupted (Ctrl-C). Waits stop at once, and `exec` sends a cancel to the session. |
| `exec` | The command's own exit code: 124 if it timed out, 128+N if killed by signal N. Failures before the command ran use the codes above; use `--json` to tell them apart. |

## HTTP connections

`shard` opens a fresh HTTP connection for every request (`Connection: close`). On some Node.js releases a request
sent on a reused keep-alive connection can stall until the request timeout. Set `SHARDFLUX_HTTP_KEEPALIVE=1` to reuse
connections instead.

## Updates

(0.5.0+) `shard` tells you when a newer version exists, on stderr after the command's output:

```text
@shardflux/cli 0.5.3 is outdated: 0.6.0 is available. Update: npm install -g @shardflux/cli@latest
```

or that the API no longer supports it (`... is no longer supported by the Shardflux API (minimum 0.6.0). Update:
...`); it keeps working either way.

- It asks the API's `GET /v1/client-versions` at most once a day per API URL (cached in
  `<config dir>/update-check.json`), in parallel with the command. A slow answer is skipped (the next run uses it), the
  exit code never changes, and nothing is printed after a command that failed with `--json`.
- Installed through the `shardflux` npm package it checks `shardflux` instead, and names that package's upgrade
  command.
- `shard version` shows the status; `shard version --check` asks now. With `--json`: `{"cli", "sdk", "update":
  {"package", "current", "status", "latest", "minimum_supported", "upgrade_command", "checked_at"}}`, where `status`
  is `current`, `outdated`, `unsupported` or `unknown`.
- `SHARDFLUX_NO_UPDATE_CHECK=1` (or `NO_UPDATE_NOTIFIER=1`) turns it off. `npx @shardflux/cli@latest` always runs the
  newest version.

## The shardflux package

`shardflux` 0.7.2 bundles both JavaScript clients: the TypeScript SDK (`@shardflux/sdk` ^0.10.0) and this CLI
(`@shardflux/cli` ^0.5.1). It needs Node.js 24 or later and is ESM only.

```sh
npm install shardflux          # the SDK: import { Shardflux, ShardfluxAccount } from 'shardflux'
npm install -g shardflux       # the CLI, as `shardflux` and `shard`
npx shardflux workspaces open acme/demo --template python-node-browser
```

- Everything `@shardflux/sdk` exports is exported from `shardflux`, including `workspaceTools()`,
  `ShardfluxAccount` and `checkClientVersion()` (0.6.0+), the error classes and the API types. See the
  [TypeScript SDK](https://docs.shardflux.dev/reference/typescript.md).
- `shardflux` and `shard` are the same command; its help text calls it `shard`. From 0.6.0 its update notice is about
  `shardflux` itself.
- Install `shardflux` or `@shardflux/cli` globally, not both: both provide `shard`.

## Compatibility

The CLI is below 1.0: commands and output may change in a minor release, marked **Breaking** in the changelog. With
`--json`, the objects are the API's own; new fields can appear in any release. 0.5.0 changes one behaviour: `shard`
now keeps a [credentials file](#credentials-file); environment variables still win over it. The account commands need
an API that serves CLI sessions (`/v1/auth`), which `https://api.shardflux.dev` does.
