---
title: "sctl"
description: "Everything the dashboard can do, from a terminal."
url: "https://saved.sh/docs/cli"
---

`sctl` is not a subset of the dashboard. Anything you can do in the browser you can do here,
because both call the same API.

It runs on Linux, macOS and Windows, as a single static binary with no runtime dependencies.

## Signing in [#signing-in]

```bash
sctl login          # device code: prints a code, you approve it in a browser
sctl auth whoami    # who am I, in which workspace, with which permissions
```

The session lives in a config file written mode `0600` on Unix-like systems:

| Platform | Path                                             |
| -------- | ------------------------------------------------ |
| Linux    | `~/.config/sctl/config.yaml`                     |
| macOS    | `~/Library/Application Support/sctl/config.yaml` |
| Windows  | `%AppData%\sctl\config.yaml`                     |

Switching workspace re-mints your token into that workspace, so **every command acts on
exactly one workspace**, the one your session is scoped to.

```bash
sctl workspace list
sctl workspace switch <workspace-id>
sctl workspace current
```

## Everything takes an ID [#everything-takes-an-id]

<Callout type="warn">
  **Commands take IDs, not names.** `sctl backup trigger prod-db` fails with `invalid backup
    id`; it wants the UUID. Names exist for you to read in listings, and are not accepted as
  arguments anywhere.
</Callout>

The idiom for scripts, and the one used throughout these docs:

```bash
BACKUP_ID=$(sctl backup list | awk '$1 == "prod-db" { print $2 }')
sctl backup trigger "$BACKUP_ID"
```

```powershell
# Windows PowerShell
$BackupId = (sctl backup list | Select-String '^prod-db\s').Line.Split()[1]
sctl backup trigger $BackupId
```

Or just read it once from `sctl backup list` and paste it.

## Command surface [#command-surface]

| Command       | Covers                                                                          |
| ------------- | ------------------------------------------------------------------------------- |
| `login`       | Device-code sign-in                                                             |
| `auth whoami` | Session state: user, workspace, permissions                                     |
| `workspace`   | `list`, `create`, `switch`, `current`                                           |
| `member`      | `list`, `invite`, `role`, `remove`, `invitations`, `resend`, `revoke`           |
| `role`        | `list`, `create`, `update`, `delete`                                            |
| `apikey`      | `create`, `list`, `revoke`                                                      |
| `worker`      | `provision`, `list`, `rotate`, `delete`                                         |
| `backup`      | `list`, `create`, `configure`, `trigger`, `pause`, `resume`, `delete`, `submit` |
| `run`         | `list --backup`, `get`                                                          |
| `artifact`    | `list`, `get`, `download`, `delete`                                             |
| `restore`     | Client-side restore of an artifact                                              |
| `apply`       | Reconcile a YAML manifest                                                       |

Two things that look like omissions and are not:

* **`backup create` takes `--kind`; `backup configure` does not.** Kind is immutable after
  creation, so there is no field to patch.
* **There is no `artifact lock`.** Protection is a property of the backup's retention policy
  (`--lock-for`), stamped onto each artifact as it is archived. A per-artifact lock would be a
  second source of truth for the same guarantee.

## Configuration [#configuration]

Every setting can come from the config file, a flag, or an environment variable prefixed
`SCTL_`.

| Setting      | Flag            | Environment variable |
| ------------ | --------------- | -------------------- |
| Backend URL  | `--backend-url` | `SCTL_BACKEND_URL`   |
| Access token |                 | `SCTL_ACCESS_TOKEN`  |
| Workspace    |                 | `SCTL_WORKSPACE_ID`  |

Flags win over the environment, which wins over the file.

## Automation [#automation]

For CI, use an API key rather than your own session. It is scoped to exactly the permissions
you grant it, and revoking it does not sign you out.

```bash
sctl apikey create ci-backups --perms backups:read,backups:write
```

Then run non-interactively by putting the key where the session token would go:

```bash
export SCTL_ACCESS_TOKEN='sk_live_...'
export SCTL_WORKSPACE_ID='<workspace-id>'
sctl backup trigger "$BACKUP_ID"
```

<Callout>
  There is no separate "api key mode". The key is a bearer credential exactly like a session
  token, so `SCTL_ACCESS_TOKEN` takes either. `SCTL_WORKSPACE_ID` is required too, because a
  machine never ran `sctl workspace switch`.
</Callout>

A key that lacks a permission is refused with `403` naming the permission, rather than a bare
failure. See [API keys](/docs/cli/api-keys).

## Declarative [#declarative]

```bash
sctl apply -f saved.yaml
```

`apply` reconciles what the file declares and **never deletes what the file omits**. Removing
something is always explicit:

```bash
sctl backup delete "$BACKUP_ID"
```

That asymmetry is deliberate. A file is easy to typo, easy to check out at the wrong revision,
and easy to run from the wrong directory; none of those should be able to destroy a retention
policy.

## Next [#next]

<Cards>
  <Card href="/docs/cli/install" title="Install" description="Linux, macOS and Windows." />

  <Card href="/docs/cli/login" title="Login" description="Sessions, workspaces, and CI credentials." />

  <Card href="/docs/cli/apply" title="apply" description="Manifests, and what reconcile means here." />

  <Card href="/docs/cli/reference" title="Reference" description="Every command and flag." />
</Cards>
