# sf CLI

Install `sf`, sign in, and learn the contract every command shares: global flags, JSON receipts, exit codes, and how the CLI picks a Space

`sf` publishes folders and projects to Spacefast, and manages everything that follows: versions, domains, access, teams, keys. Every command reads the same global flags, prints the same `--json` receipt, and exits with the same code table. Learn that once here and every command page is short.

## Install

```
npm install -g spacefast
```

The package is `spacefast` and it installs two binaries, `spacefast` and `sf`. Homebrew (`brew install spacefast`) and standalone binaries for macOS (arm64, x64), Linux (arm64, x64), and Windows (x64) are also available.

The CLI needs Node.js 20.3 or newer. Check the install:

```
sf --version
```

```
spacefast/0.3.0 darwin-arm64 node-v26.7.0
```

## First login

```
sf login
sf whoami
sf publish ./dist
```

`sf login` prints a code and a link, opens your browser, and polls until you approve. See [Authentication](/content/docs/cli/login/index.html) for the read-only variant and for signing in with an API key.

Bare `sf` runs `sf status`, so typing `sf` in a project tells you who you are, which team is default, and which Space the directory is linked to.

## Global flags

Every command accepts these.

| Flag | Default | What it does |
| --- | --- | --- |
| `--json` | off | Print a machine receipt instead of human output. Success goes to stdout, failure to stderr. |
| `--api-url=<url>` | resolved, see below | Spacefast API base URL. Env `SPACEFAST_API_URL`. |
| `--profile=<name>` | current profile | Named provider profile from `sf profiles`. Env `SPACEFAST_PROFILE`. |
| `--token=<key>` | none | API key for non-interactive auth. Env `SPACEFAST_TOKEN`. |
| `-y, --yes` | `false` | Skip confirmation prompts. Env `SPACEFAST_YES`. |
| `--cwd=<path>` | current directory | Change directory before the command runs. Parsed before anything else, so it applies to Space resolution too. |

Commands that act on a Space add `--space`, `-o, --team`, and `--claim-token`. Commands that queue work add `--wait` / `--no-wait` and `--wait-timeout=<seconds>` (default 900).

## JSON receipts

`--json` prints `{ data }` on stdout when the command succeeds, and nothing else. It prints `{ error }` on stderr when it fails. Piping stdout into a parser can never turn a failure into data.

```
{
  "data": {
    "account": {
      "id": "usr_0123456789abcdef",
      "email": "dev@example.com",
      "name": "Dev Example"
    },
    "teams": [{ "id": "team_0123456789abcdef", "name": "Acme", "slug": "acme", "role": "owner" }]
  }
}
```

```
{
  "error": {
    "code": "credential_expired",
    "message": "The credential used for this request is expired.",
    "type": "https://spacefast.com/docs/errors/credential_expired",
    "title": "Credential expired",
    "status": 401,
    "retryable": false,
    "recovery": "sf login",
    "requestId": "req_0123456789abcdef",
    "suggestions": ["Create a new API key or sign in again."]
  }
}
```

`error.code` is stable, `error.retryable` says whether re-running the same command unchanged could work, and `error.recovery` is one runnable hint. Validation failures add `error.pointer`, an RFC 6901 JSON Pointer into the failing input. List endpoints pass their `{ data, pagination }` envelope through without double-wrapping. Capability query parameters and blob paths are replaced with `[redacted]`.

## Exit codes

| Code | Name | Meaning |
| --- | --- | --- |
| `0` | ok | Success. |
| `1` | unknown | Unexpected or uncategorized failure. |
| `2` | validation | Bad flags or arguments, or validation failure (HTTP 422). |
| `10` | auth | Authentication or authorization failure (HTTP 401/403). Run `sf login`. |
| `20` | not-found | Resource not found or gone (HTTP 404/410). |
| `30` | conflict | Request conflicts with the resource’s current state (HTTP 409). |
| `40` | network | Network failure reaching the API. Retryable. |
| `50` | timeout | Timed out waiting for queued work. Retryable. |
| `130` | cancelled | Interrupted with Ctrl-C, or a confirmation prompt was declined. |

`sf doctor` is the one exception: it exits `1` when any check fails, without classifying.

## Interactive and non-interactive

The CLI prompts only when stdin and stdout are both TTYs, `--json` is absent, and neither `CI` nor `SPACEFAST_NON_INTERACTIVE` is set. Otherwise it never prompts.

Destructive actions need an answer either way. Pass `--yes` (or set `SPACEFAST_YES=1`) to proceed without a prompt. Without it, a non-interactive run fails with `confirmation_required` and exit 2. Declining a prompt exits 130.

Setting `SF_AGENT=1`, or running anywhere `CI` is set, appends `--json` to your command for you and suppresses prompts. It never implies `--yes`: remote destructive actions still demand an explicit `--yes` on every invocation. `SF_AGENT_ALLOW_DESTRUCTIVE=1` auto-confirms local protection-removing actions only, like `sf unlink`.

Some commands are exempt from the automatic `--json`: `sf mcp`, help and `--version`, and `sf api`, which has its own output contract.

## How the CLI finds your Space

In order, highest first:

1. `--space` or `SPACEFAST_SPACE`.
2. Anonymous claim state in `.spacefast/state.json`, unless the committed link names a different Space ID.
3. The `space` field in `.spacefast/space.json`.
4. The cached `spaceId` in `.spacefast/state.json`.

If nothing selects a Space and your account has exactly one, the CLI uses it, in agent mode too. With several, it prompts when interactive and fails with `no_space_selected` (exit 2) when not. A blank or whitespace-only `--space` is an error, not “absent”, so a misconfigured CI variable cannot silently create a second Space.

## Environment variables

| Variable | What it does |
| --- | --- |
| `SPACEFAST_TOKEN` | API key to authenticate with. Same as `--token`. |
| `SPACEFAST_SPACE` | Space ID, live URL, domain, or slug. Same as `--space`. |
| `SPACEFAST_TEAM` | Team slug, ID, or name. Same as `--team`. |
| `SPACEFAST_CLAIM_TOKEN` | Space key (`sfc_…`) for an unclaimed anonymous Space. |
| `SPACEFAST_API_URL` | API base URL. Same as `--api-url`. |
| `SPACEFAST_PROFILE` | Profile to use. Same as `--profile`. |
| `SPACEFAST_YES` | `1`, `true`, or `yes` skips confirmation prompts. |
| `SPACEFAST_NON_INTERACTIVE` | Any value stops the CLI from ever prompting. |
| `SF_AGENT` | `1`, `true`, or `yes` turns on agent mode. |
| `SF_AGENT_ALLOW_DESTRUCTIVE` | Lets agent mode auto-confirm local protection-removing actions. |
| `SPACEFAST_HOME` | CLI state directory, default `~/.spacefast`. |
| `SPACEFAST_CREDENTIAL_STORE` | `keyring` forces the OS keyring, `plaintext` forces the file fallback. |
| `SF_DEBUG` | Turns on diagnostics on stderr. Add `LOG_FORMAT=json` for JSON lines. |
| `DO_NOT_TRACK`, `SPACEFAST_TELEMETRY_DISABLED` | `1`, `true`, `yes`, or `on` disables agent-setup telemetry. |
| `NO_COLOR`, `FORCE_COLOR` | Color control. |

## API URL resolution

| Order | Source |
| --- | --- |
| 1 | `--api-url` passed on the command line |
| 2 | The `apiUrl` of an explicitly named `--profile` |
| 3 | `SPACEFAST_API_URL` |
| 4 | The provider recorded in `.spacefast/state.json` |
| 5 | `apiBaseUrl` in `.spacefast/space.json` |
| 6 | The `apiUrl` of the currently selected profile |
| 7 | The `apiUrl` of your stored login |
| 8 | `https://api.spacefast.com` |

Every credential is bound to the base URL it was issued for. Point the CLI somewhere else and it will not send that credential along.

## Where credentials live

On macOS the login goes into the Keychain as a generic password under the service `Spacefast CLI`. On Linux it goes into the Secret Service through `secret-tool`. Anywhere else, including Windows, there is no keyring and the CLI writes `~/.spacefast/auth.json`, telling you it did.

The keyring wins when both exist. A stale plaintext copy is deleted after the keyring is read. If the keyring is locked, the CLI fails with `credential_keyring_unavailable` (exit 10). Open the keyring, or set `SPACEFAST_CREDENTIAL_STORE=plaintext`. Under `CI`, the plaintext store is the default.

Other files the CLI owns:

| Path | Contents |
| --- | --- |
| `~/.spacefast/config.json` | Profiles and defaults, including your default team |
| `~/.spacefast/credentials/profile-*.json` | One credential file per profile |
| `.spacefast/space.json` | Committable link: which Space, team, and API base URL |
| `.spacefast/state.json` | Local cache and credentials. Never commit it, and `sf` adds it to `.gitignore` for you |
| `sf.jsonc` | Project config written by `sf init` |

## Update checks

The CLI checks npm for a newer version every 7 days and prints the exact upgrade command for how you installed it. It skips the check whenever the invocation looks machine-driven: `--json` anywhere in the arguments, an `mcp` invocation, or a non-TTY stderr.

## Command reference

[Authentication\
\
`login`, `logout`, `whoami`, `auth`, `access`](/content/docs/cli/login/index.html) [Project setup\
\
`init`, `link`, `unlink`, `switch`, `profiles`, `open`, `status`](/content/docs/cli/project/index.html) [API keys\
\
`api-keys create`, `list`, `revoke`](/content/docs/cli/api-keys/index.html) [Raw API access\
\
`sf api`](/content/docs/cli/api/index.html) [Operations and activity\
\
`operations`, `activity`](/content/docs/cli/operations/index.html) [Inspection and agent helpers\
\
`inspect`, `fetch`, `apply`, `continue`, `map`, `docs`, `doctor`, `feedback`, `plugins`](/content/docs/cli/agent-commands/index.html)
