# CLI

The `taskcloud` CLI is a thin layer over the [REST API](api-reference.md). Everything it does, you can do with the API.

## Install and log in

### Install script

Available once the first CLI release is tagged. The script installs a prebuilt binary for Linux or macOS (amd64 or arm64); run `curl -fsSL https://task.cloud/install.sh | sh` in a terminal.

It detects your OS and architecture, downloads the matching archive from the project's GitHub Releases, verifies its SHA-256 against the release's `checksums.txt` and installs `taskcloud` to `~/.local/bin` (or `/usr/local/bin` when run as root). Optional environment variables:

| Variable | Effect |
| --- | --- |
| `TASKCLOUD_VERSION` | Install a specific version, e.g. `TASKCLOUD_VERSION=0.2.0`. Default: the latest stable release. |
| `TASKCLOUD_INSTALL_DIR` | Install somewhere else, e.g. `TASKCLOUD_INSTALL_DIR=$HOME/bin`. |

Each release also ships a signed checksum file and SBOMs; the release notes list the `cosign` and `gh attestation verify` commands.

### Build from source

Until the first release, and during the private beta, the CLI is built from source (Go 1.27.3+, standard library only); beta participants get access to the source repository:

```bash
cd cli && go build -o taskcloud . && sudo mv taskcloud /usr/local/bin/
```

### Log in

```bash
taskcloud login                              # prompts for an API key (opens the dashboard to create one)
taskcloud login --key-stdin < key.txt        # non-interactive (CI)
taskcloud whoami
```

**Coming soon:** an npm wrapper (`npm install -g @taskcloud/cli`) and browser-based login. Until then, `taskcloud login` asks you to paste a key created in the dashboard.

Credentials are stored in `~/.config/taskcloud/config.json`. The `TASKCLOUD_API_KEY` environment variable takes precedence.

**Global flags:** `--json` (machine-readable output; on automatically when stdout is not a TTY), `--api-key <key>`, `--api-url <url>`, `--quiet`.

## Cheat sheet

```bash
taskcloud create --template base --size default --name dev   # prints the sandbox ID
taskcloud list                                               # your sandboxes (deleted ones hidden)
taskcloud exec <id> -- npm test                              # exit code = command's exit code
taskcloud shell <id>                                         # PTY session: prints its WebSocket URL
taskcloud ssh <id>                                           # SSH through the gateway
taskcloud cp ./app <id>:/home/user/app -r                    # upload a directory
taskcloud port expose <id> 3000 --public                     # https://<short_id>-3000.taskhost.cloud
taskcloud pause <id>                                         # $0 compute while paused
taskcloud fork <id> --name dev-copy                          # copy with memory
taskcloud fork <id> --count 5 -q                             # 5 copies, all or nothing; prints 5 IDs
taskcloud desktop <id> --ttl 10m --read-only --open          # open the desktop in a browser
taskcloud screenshot <id> -o screen.png
taskcloud input <id> '[{"action":"left_click","coordinate":[640,412]}]' -o after.png
taskcloud delete <id> --yes
```

## Commands

### Sandboxes

| Command | Description | API |
| --- | --- | --- |
| `create [flags]` | Create a sandbox. Flags: `--template <name>`, `--snapshot <snap_id>`, `--image <ref>`, `--size small\|default\|large`, `--name`, `--label k=v` (repeatable), `--env K=V` (repeatable), `--idle-timeout 5m`, `--no-auto-pause`, `--always-on`, `--batch`, `--region auto\|eu-de\|eu-fi`, `--ttl 2h`, `--disk 100`, `--port 3000[:public]` (repeatable), `--no-wait` | `POST /sandboxes` |
| `list [--state s] [--label k=v] [--all]` | List sandboxes. Sandboxes in state `deleted` are hidden unless you pass `--all` or `--state deleted`; `--all` shows them only if the API still returns them. A guaranteed "recently deleted" view is coming soon. | `GET /sandboxes` |
| `get <id>` | Show a sandbox | `GET /sandboxes/{id}` |
| `update <id> [flags]` | `--name`, `--label k=v`, `--idle-timeout 15m`, `--no-auto-pause`, `--always-on`, `--no-always-on`, `--ttl` | `PATCH /sandboxes/{id}` |
| `pause <id>` | Pause (RAM + disk snapshot) | `POST /sandboxes/{id}/pause` |
| `resume <id>` | Resume | `POST /sandboxes/{id}/resume` |
| `fork <id> [--count N] [--name] [--label]` | Fork with memory. `--count` (`-n`) 1–50 creates N forks in one call, all or nothing; prints a table (`-q`: one ID per line, `--json`: array of sandboxes) | `POST /sandboxes/{id}/fork` |
| `delete <id> [--yes]` | Delete permanently | `DELETE /sandboxes/{id}` |

### Exec and terminals

| Command | Description | API |
| --- | --- | --- |
| `exec <id> [--cwd] [--env K=V] [--user] [--timeout 300] [--background] -- <cmd...>` | Run a command; streams output; exits with its exit code. `--background` prints the command ID. | `POST /sandboxes/{id}/exec` |
| `commands list <id>` | Recent commands | `GET /sandboxes/{id}/commands` |
| `commands logs <id> <cmd_id> [--follow]` | Output of a command: replays earlier output, then follows until the command exits (the API has one replay-and-follow stream, so `--follow` currently changes nothing). Alias: `taskcloud logs`. | `GET .../commands/{cmd_id}/stream` |
| `commands kill <id> <cmd_id> [--signal SIGKILL]` | Signal a command | `POST .../commands/{cmd_id}/kill` |
| `shell <id> [--user] [--cwd]` | Creates a PTY session and prints its `websocket_url` (valid 60 s until first connect). A built-in interactive terminal is coming soon; use `taskcloud ssh` meanwhile. | `POST /sandboxes/{id}/pty` |
| `ssh <id> [--key file.pub] [--ttl 1h] [--user u] [--print] [-- ssh args]` | SSH via `ssh.taskhost.cloud`. Sends your local public key (default `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub` or `id_rsa.pub`) as a temporary key for this sandbox; nothing is stored on your account. `--print` only prints the command. | `POST /sandboxes/{id}/ssh` |

### Files

| Command | Description | API |
| --- | --- | --- |
| `cp <src> <dst> [-r]` | Copy local ↔ sandbox. Sandbox paths are `<id>:/path`. | `/files/upload`, `/files/content`, `/files/download` |
| `files ls <id> <path> [-R]` | List | `GET /sandboxes/{id}/files` |
| `files cat <id> <path>` | Print a file | `GET .../files/content` |
| `files rm <id> <path> [-r]` | Delete | `DELETE /sandboxes/{id}/files` |
| `files watch <id> <path>` | Print change events | `GET .../files/watch` |

### Networking and desktop

| Command | Description | API |
| --- | --- | --- |
| `port expose <id> <port> [--public]` | Public HTTPS URL | `POST /sandboxes/{id}/ports` |
| `port ls <id>` / `port rm <id> <port>` | List / remove | `GET`, `DELETE .../ports` |
| `token create <id> [--ttl 1h] [--port 3000]` | Access token for private ports | `POST .../access-tokens` |
| `ssh-key add <file.pub>` / `ssh-key ls` / `ssh-key rm <id>` | Account SSH keys | `/ssh-keys` |
| `desktop <id> [--ttl 10m] [--read-only] [--single-use] [--open]` | Desktop stream URL | `POST /sandboxes/{id}/desktop` |
| `screenshot <id> [-o shot.png] [--scale 0.5] [--format png\|jpeg] [--region x0,y0,x1,y1]` | Save a screenshot | `GET .../screenshot` |
| `input <id> '<actions JSON>' [-o after.png] [--coordinate-scale 0.5]` | Send mouse and keyboard actions: a JSON array of [input actions](guides/desktop-and-computer-use.md) (a single object also works; `-` reads stdin). `-o` saves a screenshot taken after the last action. | `POST .../input` |

### Snapshots and templates

| Command | Description | API |
| --- | --- | --- |
| `snapshot create <id> [--name] [--no-memory]` | Named snapshot | `POST /sandboxes/{id}/snapshots` |
| `snapshot ls [--sandbox <id>]` / `snapshot rm <snap_id>` | List / delete | `/snapshots` |
| `template create --name <n> (--image <ref> \| --snapshot <snap_id>) [--start-command] [--ready-check <cmd>] [--desktop]` | Create a template | `POST /templates` |
| `template ls` / `template logs <name> [--follow]` / `template rm <name>` | Manage templates | `/templates` |

### Account

| Command | Description | API |
| --- | --- | --- |
| `key create --name <n> (--preset ci \| --scope exec ...) [--label k=v] [--sandbox <id>] [--expires 30d]` | Create an API key | `POST /api-keys` |
| `key ls` / `key rotate <id>` / `key revoke <id>` | Manage keys | `/api-keys` |
| `webhook create --url <url> --event sandbox.ready [--event ...]` | Create a webhook | `POST /webhooks` |
| `webhook ls` / `rm` / `rotate` / `test` | Manage webhooks | `/webhooks` |
| `usage [--group-by none\|sandbox\|day\|size\|label\|template] [--label-key customer] [--from] [--to]` | Usage and cost | `GET /usage` |
| `limits` | Plan, concurrency, rate limits | `GET /limits` |
| `spend-limit set <usd> [--on-limit pause_running\|block_new]` / `spend-limit clear` | Spend limit | `PUT /account/spend-limit` |
| `mcp [--read-only] [--tools a,b]` | Run the local MCP server on stdio (starts `npx -y @taskcloud/mcp`; needs Node.js and the published `@taskcloud/mcp` package, coming soon to npm) | see [MCP](mcp.md) |

## CI example

```bash
taskcloud login --key-stdin <<< "$SANDBOX_API_KEY"
id=$(taskcloud create --template base --size large --batch --ttl 1h --json | jq -r .id)
trap 'taskcloud delete "$id" --yes' EXIT
taskcloud cp . "$id":/home/user/repo -r
taskcloud exec "$id" --cwd /home/user/repo --timeout 600 -- npm ci '&&' npm test
```

`--batch` halves the compute price; the sandbox may wait briefly in `queued`, and `exec` waits for it to start.
