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.
5 stars
0 votes
0 copies
0 views
Added September 3, 2026
ai-agentsgobashnodegitapici/cd
Works with
cursor
cli
api
Security analysis
A92/100
mediumInstalls packages at runtime which could introduce malicious dependencies
Installs into .claude/skills of the current project.
Are you the author of ship?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/shipstatic-ship)
---
name: ship
description: "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."
compatibility: "Node.js >= 20.19. Run via npx (no install) or install globally: npm install -g @shipstatic/ship"
metadata:
openclaw:
requires:
bins:
- ship
emoji: "🚀"
homepage: https://github.com/shipstatic/ship
install:
- kind: node
package: "@shipstatic/ship"
bins: [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`. Usually no action needed. If the app's routes return 404 on reload, add `{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }` as `ship.json` in the deployed folder. 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 with `ship domains get <name> --json` and read `status`: `unverified` means DNS is not pointing here yet, `unlinked` means DNS is right but nothing is published there, `live` means it serves.
### 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.
### Domain status
`status` is the one word to read: what the domain needs from its owner. Read it rather than working it out from the other fields.
| `status` | Means | Do |
|----------|-------|-----|
| `unverified` | DNS is not pointing here yet, in full or in part | `ship domains records <name>`, set those records, then `ship domains verify <name>` |
| `unlinked` | DNS is right and nothing is published there | `ship domains set <name> <deployment>` |
| `live` | It serves the linked deployment | Nothing |
| `paused` | The plan has no room for it, so it serves nothing | Delete another domain, or upgrade the plan |
`verification` is the DNS detail under `unverified`: `pending` (the CNAME does not point here, so nothing is served), `partial` (a `www` domain's CNAME does and its apex A record does not, so the site is served and `example.com` does not redirect yet), `verified` (every required record does). `verified` is when that happened, `verifications` how many attempts, `paused` when serving stopped.
### 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",
"status": "unverified",
"deployment": "happy-cat-abc1234.shipstatic.com",
"linked": 1743552000,
"links": 1,
"verification": "pending",
"verified": null,
"verifications": 0,
"paused": null,
"labels": [],
"created": 1743552000
}
```
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.
## 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 \"setup.exe\" has an extension that is not 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 |