# Ship

> Deploy static websites to ShipStatic. Use when the user wants to deploy a site, publish a website, upload to hosting, go live, set up a custom domain, manage deployments, or share a site URL. Free, no account needed. CLI (`ship`) and Node.js/browser SDK.

- Skill: `shipstatic/ship` (Agent Skill, multi-file: 17 files)
- Install (CLI): `npx skillmds@latest add shipstatic/ship`
- Raw SKILL.md: https://api.skillmd.com/api/skills/shipstatic/ship/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: shipstatic (https://skillmd.com/u/shipstatic)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/shipstatic/ship

---


Deploy static sites. No account, no config — just ship it.

## No-install usage (recommended for agents)

You don't need to install anything. Run any command via `npx`:

```bash
npx -y @shipstatic/ship ./dist                       # deploy (shortcut)
npx -y @shipstatic/ship deployments list             # any subcommand works the same
npx -y @shipstatic/ship domains set www.example.com  # ...
```

`-y` skips the install prompt — important for non-interactive runtimes (CI, sandboxes, agent containers). Every example below uses the bare `ship` command for readability; substitute `npx -y @shipstatic/ship` if it isn't installed globally.

## Deploy

```bash
ship ./dist
```

Site is live. Output includes the URL and a claim link.

Pass a build output directory (e.g. `./dist`, `./build`, `./out`) or a single file. Ship strips the directory prefix for clean URLs — `dist/assets/app.js` serves at `/assets/app.js`. A single file keeps its name: `ship page.html` deploys as `/page.html`. Deploying a project root (contains `package.json`, `node_modules`) is rejected — build first, then deploy the output.

Without credentials, deployments are public and expire in 3 days. **Always show the user both the deployment URL and the claim link** — the claim link lets them keep the site permanently.

The deployment ID **is** the URL hostname. Use the full ID (e.g. `happy-cat-abc1234.shipstatic.com`) as the argument to all other commands. The site lives at `https://<deployment>`.

### Deployments that clean themselves up

```bash
ship ./dist --ttl 1h      # gone in an hour
ship ./dist --ttl 7d      # a week-long preview
```

For a preview nobody needs to keep — a draft, a diff, a one-off render. The
platform reclaims it when the time is up, so nothing accumulates in the user's
account and nobody has to remember to delete it. Seconds or a `<n><unit>`
duration (`s`/`m`/`h`/`d`), up to a year.

**It needs a token.** Without one the deploy is anonymous and already expires
in 3 days on the platform's own schedule — there is no deployer to choose a
different lifetime, and the CLI refuses before uploading anything. **It cannot
be combined with `--domain`**, because a domain must not point at something
about to be reclaimed.

The response's `expires` is the answer, in unix seconds — read it there rather
than computing it, since the platform stamps it against its own clock.

### Parsing output

```bash
ship ./dist --json
```

```json
{
  "deployment": "happy-cat-abc1234.shipstatic.com",
  "url": "https://happy-cat-abc1234.shipstatic.com",
  "files": 12,
  "size": 348160,
  "status": "success",
  "config": false,
  "password": false,
  "labels": [],
  "via": "cli",
  "created": 1743552000,
  "expires": 1743811200,
  "claim": "https://my.shipstatic.com/claim/1234567890abcdef1234567890abcdef"
}
```

`claim` only appears on the initial deploy without credentials. `expires` is `null` for authenticated (permanent) deploys. `config: true` indicates a `ship.json` is present in the deployment; `password: true` indicates the deployment is password-protected.

### Piping

```bash
ship ./dist -q              # → happy-cat-abc1234.shipstatic.com
```

`-q` outputs only the identifier — use it when piping or scripting.

### Labels

```bash
ship ./dist --label v1.0 --label production
```

Labels **replace all existing**, not append. Include current labels to keep them.

### Password protection

```bash
ship ./dist --password "hunter22"          # protect deployment
SHIP_PASSWORD="hunter22" ship ./dist        # via env var
```

Visitors get an unlock page until they enter the password. Length: 6–128 characters. Set per-deployment at upload time — cannot be added or changed later (deploy a new version to rotate). Works on both internal (`*.shipstatic.com`) and custom domains. **Always show the password to the user** if you set one — they need it to view the site.

### SPA routing

Ship auto-detects single-page apps from `index.html` content and configures client-side routing rewrites — all paths serve `index.html`. No action needed. Skipped if a `ship.json` config is already included in the deployment. Disable with `--no-spa-detect`.

## Authentication

Deploy works without credentials. Everything else requires an API key.

| Needs API key | No auth needed |
|---------------|----------------|
| Permanent deploys, domains, tokens, account | Deploy (public, 3-day TTL) |

```bash
export SHIP_TOKEN=<token>          # Environment variable (best for automation)
ship --token <token> ...           # Per-command override
ship config                        # Interactive setup → ~/.shiprc (requires TTY)
```

Any ship token works: an API key (`ship-…`, durable, full account) or a deploy token (`deploy-…`, scoped, revocable — set a short TTL for one-shot CI/CD workflows).

Free API key: https://my.shipstatic.com/api-key

## Custom Domains

Requires an API key. Full workflow:

```bash
# 1. Validate
ship domains validate www.example.com

# 2. Deploy + link in one command
ship ./dist --domain www.example.com

# 3. Show DNS records to the user
ship domains records www.example.com

# 4. After user configures DNS → verify
ship domains verify www.example.com
```

Step 2 auto-prints DNS records and a setup link in text mode. With `--json`, call `domains records` separately.

`--domain` answers **as the domain** — same output as `ship domains set`, with the freshly linked deployment in the `deployment` field. Prefer it over the pipe (`ship ./dist -q | ship domains set www.example.com`), which still works: one process means one exit code and one JSON document, so a failed deploy cannot be masked by the second command. It requires a token and refuses before uploading anything if there isn't one. If the link fails, the deployment still exists and is reported first — re-run to link it again.

Verification is async — DNS propagation takes minutes to hours. Check status with `ship domains get <name> --json` and look for `"status": "success"`.

### Domain types

| Type | Example | DNS needed | Goes live |
|------|---------|------------|-----------|
| Internal | `my-site.shipstatic.com` | No | Instantly |
| Custom | `www.example.com` | CNAME + A | After DNS verified |

**No apex domains.** Always `www.example.com`, not `example.com`. The A record only redirects apex to www.

### Upsert operations

`domains set` creates if new, updates if exists:

```bash
ship domains set www.example.com                   # Reserve (no deployment yet)
ship domains set www.example.com <deployment>       # Link to deployment
ship domains set www.example.com <other-dep>        # Switch (instant rollback)
ship domains set www.example.com --label prod       # Update labels
```

Reads deployment from stdin when piped: `ship ./dist -q | ship domains set www.example.com`

**No unlinking.** Once linked, switch deployments or delete the domain. Setting deployment to null returns 400.

### Parsing domain output

```bash
ship domains set www.example.com <dep> --json
```

```json
{
  "domain": "www.example.com",
  "url": "https://www.example.com",
  "deployment": "happy-cat-abc1234.shipstatic.com",
  "status": "pending",
  "labels": [],
  "created": 1743552000,
  "linked": 1743552000,
  "links": 1
}
```

Always show the user the records **from this response**, never values copied out of this document — they come from the platform and can change.

```bash
ship domains records www.example.com --json
```

```json
{
  "domain": "www.example.com",
  "apex": "example.com",
  "records": [
    {"type": "A", "name": "@", "value": "15.204.149.253"},
    {"type": "CNAME", "name": "www", "value": "cname.shipstatic.com"}
  ]
}
```

### DNS helpers (custom domains only)

```bash
ship domains dns www.example.com                  # Provider name
ship domains share www.example.com                # Shareable setup link
ship domains records www.example.com -q           # TYPE NAME VALUE (one per line)
```

### Validation

Exit codes as the answer:

```bash
ship domains validate www.example.com -q && echo "valid" || echo "invalid"
```

Exit 0 = valid (outputs normalized name). Exit 1 = invalid (no output).

## Output Modes

Every command supports three modes:

| Flag | Output | When to use |
|------|--------|-------------|
| *(default)* | Human-readable | Showing results to the user |
| `--json` | JSON on stdout | Parsing programmatically |
| `-q` | Identifier only | Piping between commands |

`-q` prints the resource identifier — except `tokens create -q`, which prints the token **secret** (shown once, never again).

Errors go to stderr in all modes. Exit 0 = success, 1 = error.

List commands return `{"<resource>s": [...], "cursor": null}`. A non-null `cursor` means more pages remain — pass it back with `--cursor` to continue, and size pages with `--limit`. There is no total; a count is an aggregate over a collection, not a property of one page. `domains list` text mode omits status — use `--json` to see `pending` vs `success`.

## Commands

### Deployments

```bash
ship ./dist                          # Deploy (shortcut)
ship ./dist --domain <name>          # Deploy and serve it at that domain
ship ./dist --ttl 1h                 # Expires in an hour (needs a token)
ship deployments upload <path>       # Deploy (explicit)
ship deployments list                # List all
ship deployments get <deployment>    # Details
ship deployments set <deployment>    # Update labels (--label)
ship deployments delete <deployment> # Delete (async)
```

### Domains

```bash
ship domains list                     # List all
ship domains get <name>               # Details
ship domains set <name> [deployment]  # Create, link, or update
ship domains validate <name>          # Check validity (exit code)
ship domains records <name>           # Required DNS records
ship domains dns <name>               # DNS provider lookup
ship domains share <name>             # Shareable setup link
ship domains verify <name>            # Trigger DNS verification
ship domains delete <name>            # Delete
```

### Account & Tokens

```bash
ship whoami                           # Account info
ship ping                             # Connectivity check
ship tokens create                    # New deploy token (shown once)
ship tokens create --ttl 30d          # With expiry — 3600, 90s, 1h, 30d
ship tokens list                      # List tokens
ship tokens get <token>               # Details for one token
ship tokens delete <token>            # Delete (revokes immediately)
```

## Flags

| Flag | Purpose |
|------|---------|
| `--json` | JSON output |
| `-q, --quiet` | Identifier only |
| `--token <token>` | Any ship token: API key or deploy token |
| `--domain <domain>` | Deploy and serve it there — creates or repoints. Needs a token |
| `--label <label>` | Set label (repeatable, replaces all) |
| `--password <pwd>` | Password-protect deployment (6–128 chars) |
| `--ttl <duration>` | Expire after that long — `3600`, `90s`, `1h`, `7d`. Needs a token; not with `--domain` |
| `--no-path-detect` | Skip build output auto-detection |
| `--no-spa-detect` | Skip SPA rewrite auto-configuration |
| `--no-color` | Disable colors |
| `--config <file>` | Custom config path |

## Errors

Exit code is non-zero on failure, and with `--json` the error goes to **stderr** as the platform's wire shape:

```json
{
  "error": "validation_failed",
  "message": "File \"index.html\" too large. Maximum 20 MB allowed.",
  "status": 400
}
```

**Branch on `error` and `status`, never on `message`.** Messages are written for the human reading them and get reworded; the type tag and the status are the contract.

| `error` | `status` | Means | Do |
|---------|----------|-------|----|
| `validation_failed` | 400 | Bad input — path, domain name, label, password, ttl | Read `message`, fix the input, retry |
| `authentication_failed` | 401 | Missing or bad credential | Check `SHIP_TOKEN`; do not retry unchanged |
| `forbidden` | 403 | Plan cap reached, account terminated, or an action this credential may never take | **Do not retry.** Suggest upgrading if it is a cap |
| `not_found` | 404 | No such deployment or domain | Verify the identifier |
| `rate_limit_exceeded` | 429 | Too many requests | Wait — `details.expires` is when it clears. An API key raises the limit |
| `business_logic_error` | 400 or 422 | Valid request, refused by a state rule (e.g. unlinking a domain, suspended account) | Read `message`; the rule will not change on retry |
| `maintenance` | 503 | Platform closed on purpose | `message` says when it reopens. Wait; do not retry in a loop |

Common cases worth recognising before they happen:

| Situation | What you get |
|-----------|--------------|
| Deploying a project root (`package.json`, `node_modules`) | `validation_failed` — build first, deploy the output |
| `--ttl` or `--domain` with no credential | `validation_failed`, refused **before** anything uploads |
| `--ttl` together with `--domain` | `validation_failed` — a domain must not point at something expiring |
| DNS commands on an internal `*.shipstatic.com` domain | `validation_failed` — only custom domains have DNS |
| `domains verify` called again too soon | `rate_limit_exceeded` — verification is already queued |

