These guides are primarily for agent readers. They explain how agents use Sails. If you are a person, start at sails.app or Add Sails.

Publish

The bash command is ship (not publish). ship create binds one folder to one URL. The mount tracks branch head and redeploys on every autosave checkpoint. Run create once per path. Do not mint a second mount to “refresh” a link. Do not run repo save as a ritual.

A folder with sails.site.json at the mount root compiles as a static site. Any other folder is served as files. Tailwind CSS and TypeScript/TSX transpile when served.

Done looks like

ship list shows a row with a public_url you can open. For --policy 644, that URL returns 200 without sign-in. Edit a file under the mount path; the same URL updates. You did not run ship create again.

Worked example: public site once

cp -r /templates/static-site-demo ./site
ship create site --policy 644
ship list

<name> is required and unique in the repo. When --path is omitted, the folder defaults to <name>, so this ships ./site as mount site.

Expected create output is JSON (shape, not exact ids):

{
  "id": "…",
  "name": "site",
  "path": "site",
  "policy": "644",
  "public_url": "https://{id}.sails.app/"
}

Open public_url. Edit under site/. Stop. Autosave already checkpointed.

Commands

ship help
ship create <name> [--path PATH] [--branch name] [--hostname host] [--policy MODE] [--mcp] [--stage-of LIVE]
ship stage <live-name-or-id> [--path DEST] [--name NAME] [--hostname H] [--policy MODE]
ship list
ship update <id> [--name NAME] [--policy MODE] [--stage-of LIVE]
ship verify <id>
ship analytics <id-or-name> [--since 7d] [--granularity day|hour] [--visitor UUID] [--exclude-visitor UUID] [--visitors-limit 50]
ship promote <id-or-name>
ship delete <id>          # tombstone 7 days; ship restore
ship delete <id> --now    # hard-delete the URL
ship restore <id>
Flag / commandWhat it does
--pathFolder to mount (default <name>)
--policyHTTP access for the whole mount (static + *.api.sh)
--hostnamePlatform host, custom domain, or host/prefix
--mcpStreamable HTTP MCP at /mcp for *.mcp.sh
ship stageCopy live folder to {path}-staging (mode 0700) and link with --stage-of
ship promoteThree-way-merge staging folder into live (see .shipignore)
ship deleteTombstone 7 days (restorable). Avoid --now unless the URL should die

Policy

PolicyWho can load the URL
644Anyone (public web)
640Owner or default-group members (others sign in, then 403)
600Owner only (default)

Anonymous visitors on 600 / 640 go to /auth/login. Public sites need --policy 644. Policy does not rewrite per-file VFS modes in the workspace.

Who can ship

Actorcreate / update / delete / restore / promote / stageCustom --hostname / ship verify
Repository ownerAny path, including repo rootYes
write member / agentNon-root path they have Unix write on (and inside an agent jail)No
read memberNoNo

Agents can ship a preview URL for a folder they can write. Binding a custom hostname stays owner-only.

Hostnames

Platform hosts (*.sails.app, *.localtest.me, *.localhost) include the org slug by default. ship create crm --hostname crm binds {org}-crm.sails.app, not crm.sails.app. Those hosts are auto-verified.

Custom domains do not get the org prefix. The first host on a new domain needs DNS TXT. After that, this org can ship further hosts on that domain without another TXT.

--hostname host/prefix (example: {org}.sails.app/admin) mounts under a path. Ship the host root first in this repository. You cannot hang a prefix on another repo’s host.

ship update cannot change --hostname. To move a custom domain, delete the old mount and ship create the new one. Keep at least one verified host on the domain if you want to skip a new TXT.

Custom domain TXT

Do not invent a TXT value. Sails prints a one-time token.

ship create site --path ./site --hostname example.com --policy 644
ship list
ship verify <id>

Until verify succeeds, public_url stays {published-path-id}.sails.app. hostname already shows example.com. Unverified JSON includes hostname_verification_txt as _sails-challenge.example.com = <token>. Copy the token (the part after =).

DNS (zone example.com): Type TXT, name _sails-challenge, value = the token only. Then CNAME the hostname to cname.sails.app (apex often needs ALIAS / ANAME / CNAME-flattening). ship verify checks ownership only — it does not wait for the CNAME.

Staging → promote

Use a folder pair, not a hostname swap. Both URLs keep serving their own folders.

ship stage site                 # copy → site-staging (0700) + linked mount
# edit site-staging/; optional .env with KEY=ssec_…
# list .env in site/.shipignore if prod env must not be overwritten
ship promote site               # merge staging → live
# optional: ship delete site-staging && rm -rf site-staging

.shipignore at the live folder root is gitignore syntax and the only ignore source. .shipignore itself is never promoted. Do not default-ignore *.json — sails.site.json must promote. Conflicts fail closed: nothing applied, conflicting paths printed, non-zero exit.

Analytics

HTML page views are recorded automatically (no SDK). Assets are not counted. Unique visitors use a sails_vid cookie. Bots are classified from User-Agent and excluded from headline counts.

ship analytics site --since 7d --granularity day
ship analytics site --exclude-visitor 0199a0c0-1111-7000-8000-000000000001

Custom events (both strings, max 128 chars):

navigator.sendBeacon(
  "/sails/analytics/events",
  JSON.stringify({ name: "signup", value: "pro" }),
);

On a path-prefixed URL, POST /prefix/sails/analytics/events or POST /sails/analytics/events with a same-origin Referer. In published *.api.sh / *.mcp.sh, only ship analytics is allowed — create / update / delete / list / verify stay blocked.

Failure modes

What you seeWhat it meansWhat to do
Second ship create for the same folderYou tried to mint a “fresh” URLKeep the first mount. ship list is the URL
ship delete --now then a new URLYou hard-deleted the old linksPrefer plain ship delete (7-day tombstone) unless the URL must die
Agent cannot --hostnameCustom hosts are owner-onlyOwner binds the host; agent can still ship a preview path
ship create denied for repo rootWrite members / agents cannot ship /Ship a subfolder they can write
Custom domain still shows *.sails.appTXT not verifiedCopy hostname_verification_txt, add the TXT, ship verify <id>
Promote fails closedMerge conflictResolve conflicting paths in staging; promote again
You typed publish createBinary is shipship help

How to: Publish a site, Check a site before you publish.