# The CLI

> The s3nd command line: move a file between machines with a code, check that a bucket is actually set up to hold transfers, and keep the settings in a committable file instead of your shell history. Straight to S3 or through your own server.

Canonical: https://www.s3nd.sh/cli · Markdown: https://www.s3nd.sh/cli.md · Français: https://www.s3nd.sh/fr/cli

Move a file with put and get, check that a bucket is actually set up to hold transfers with doctor, and keep the settings in a file you can commit. Straight to S3 with nothing deployed, or through your own server with a token.

```sh
npm install -g @s3nd/cli
```
```sh
$ s3nd put ./report.pdf
report.pdf · 284 kB · expires in 1 day
K7QP2M4X

$ CODE=$(s3nd put ./report.pdf)          # the code is stdout, the rest is stderr
$ tar cz ./project | s3nd put - --name project.tar.gz

# on the other machine
$ s3nd get K7QP2M4X
Wrote /home/you/report.pdf · 284 kB

$ s3nd get K7QP2M4X -o -  | less        # or to stdout
$ s3nd rm K7QP2M4X
Burned K7QP2M4X
```

## A few questions, then proof it works.
setup asks where transfers go, the bucket and the keys, saves them for this machine and runs doctor. S3 misconfiguration fails late and vaguely; doctor performs the operations s3nd actually needs and reports what happened, rather than reading your policy and reasoning about it. The probe object is deleted before it returns.

**one question at a time, from any directory after**
```sh
$ s3nd setup
? Where should transfers be stored?
  AWS S3
❯ Cloudflare R2                  no egress fees
  Scaleway
  …
  ↑↓ to move · Enter to pick
  Dashboard → R2 → Overview, "Account ID" in the side panel.
? Cloudflare account ID 8c4f…
? Bucket name transfers
  R2 → Manage API tokens → Create API token, with Object Read & Write on this bucket.
? Access key ID 5d1e…1a2b
? Secret access key (hidden) ••••••••••••••••

✓ Saved ~/.config/s3nd/config.json (readable by you only)
  Cloudflare R2 · bucket "transfers" · credentials: key …1a2b

Checking it works…
✓ Write, read, delete: round-tripped a probe object
Ready. Send something:
  s3nd put ./a-file
```
**and the check that earns the command**
```sh
$ s3nd doctor
Using ~/.config/s3nd/config.json
✓ Configuration: bucket "transfers", region "auto"
✓ Credentials: resolved, key ends in 1a2b
✓ Bucket reachable: HeadBucket succeeded
✓ Write, read, delete: round-tripped a probe object
! Expiry cleanup: no enabled expiration rule
  → Add an S3 lifecycle rule that expires objects under this bucket after a day or
    two. Without it, expired transfers stay stored and billed.

1 check(s) failed.
```

That last check is the one nobody discovers until a bill arrives. `expiresIn` stops a transfer being handed over; only a lifecycle rule deletes the object. Pointed at a server, `doctor` checks the one thing that matters there, a real round trip, and exits non-zero on failure, so it works as a deployment smoke test.

## Committable settings, uncommittable keys.
${VAR} is read from the environment, and envFile names a file to load first without overwriting what the shell already set. Profiles hold several setups in one file. A flag beats a variable, which beats the file.

**s3nd.config.json**
```json
{
  "envFile": ".env",
  "profiles": {
    "r2": {
      "bucket": "transfers",
      "region": "auto",
      "endpoint": "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com",
      "expiresIn": "24h",
      "credentials": { "accessKeyId": "${R2_ACCESS_KEY_ID}", "secretAccessKey": "${R2_SECRET_ACCESS_KEY}" }
    },
    "local": { "bucket": "transfers", "endpoint": "http://localhost:9000" },
    "prod":  { "remote": "https://drop.example.com/api/transfers", "token": "${S3ND_TOKEN}" }
  }
}
```
```sh
$ s3nd -p local doctor          # against a MinIO container, offline
$ s3nd -p r2 put ./report.pdf
$ s3nd -p prod put ./report.pdf # through your server, with a token

$ s3nd config                   # which value won, and where it came from
file         /home/you/transfers/s3nd.config.json (profile "r2")
mode         straight to S3
bucket       transfers                                 $S3ND_BUCKET
endpoint     https://8c4….r2.cloudflarestorage.com     s3nd.config.json (r2)
credentials  …1a2b                                     s3nd.config.json (r2)
expires in   1 day                                     s3nd.config.json (r2)
```

The file is looked for from the working directory upwards, then in `~/.config/s3nd/config.json`. `s3nd config` masks the access key id and never prints the secret or the token. Environment-only works too, which is the shape CI wants.

## One implementation, two wirings.
Every command takes --remote, pointing it at a deployment of the transfer protocol instead of at S3. The CLI has exactly one implementation, the protocol client, wired either to fetch or straight into the handler in the same process. The two modes cannot drift apart.

```sh
$ s3nd --remote https://drop.example.com/api/transfers --token "$TOKEN" put ./report.pdf
K7QP2M4X

$ s3nd doctor --remote https://drop.example.com/api/transfers --token "$TOKEN"
✓ Server: https://drop.example.com/api/transfers answered
✓ Create, read, delete: round-tripped code 8WTXQC8R
```

The practical consequence: the machine you run this from needs a token for your own deployment rather than S3 credentials. That is the shape for a team, where handing every laptop the bucket keys is not.

`s3nd init --provider remote` writes the matching starter, and `doctor --remote` proves the server answers before anyone depends on it.

[Setting up a server](https://doc.s3nd.sh/docs/server)

## Six commands, a dozen options.
### Commands
- `put <file>`: Store a file and print the code to carry. "-" reads stdin
- `get <code>`: Fetch what a code points at, to the stored filename, -o, or stdout
- `rm <code>`: Burn a code
- `doctor`: Check this setup can actually store transfers. Exits non-zero when a check fails
- `init`: Write a starter s3nd.config.json for aws, r2, minio, scaleway, wasabi or remote
- `config`: Print the resolved configuration, and where each value came from

### Options
- `-c, --config`: Configuration file to read
- `-p, --profile`: Profile to use inside it
- `--env-file`: Read KEY=value pairs from this file first
- `--bucket, --prefix, --region, --endpoint`: Bucket settings, overriding the file and the environment
- `--expires-in`: 3600, 30m, 24h, 7d, or never
- `--remote, --token`: Talk to a s3nd server instead of S3 directly
- `--name`: Filename to store the transfer under
- `--json`: Machine-readable output, for scripts and for doctor in CI

Configuration otherwise comes from the same environment variables as the library: `S3ND_BUCKET`, `S3ND_ENDPOINT`, `S3ND_REMOTE`, `S3ND_TOKEN` and the usual AWS credentials. Node 20 or later; the argument parser is `node:util`’s `parseArgs`, so there is nothing else to install.
