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.

File transfer

upload and download mint one-time links. A human opens the browser URL, or another machine streams with curl -T after upload PATH --curl. You never need their local disk from MCP, and you should not paste binaries into chat.

There is no upload --to. The path is the first argument. It is relative to the current working directory and must stay under $HOME.

MCP write_file / read_file are UTF-8 text only. Use transfer for CSVs, zips, images, and anything awkward to paste.

When to use which path

SituationUse
A human has the file in a browserupload PATH → open the URL
Another machine has the file (Grok Bot, laptop, Cursor)upload PATH --curl → curl -T that URL
File already on the public internetcurl -o path https://…
File is generated in this workspaceWrite it here; skip transfer
Small text editMCP write_file / edit_file

--dir stays browser / tar only. Reverse-curl is a single file.

Done looks like

  • In (browser): upload poll ID prints success and the file exists at the path you named.
  • In (curl): curl -T returns 200 with ok: true, then upload poll succeeds.
  • Out: the person opens the URL once and the browser saves the file (or a .tar.gz for a directory). No poll step.

Worked example: get a spreadsheet in (browser)

Job: a human has q2-leads.csv on their laptop. You need it at inbox/q2-leads.csv.

upload inbox/q2-leads.csv

Expected shape:

Open this URL in your browser to upload (repository members):
  https://…/sails/transfer/…
Target: ~/inbox/q2-leads.csv
Transfer session: 3f2a9c1e-…

Send that URL. They sign in as a repository member and drop the file. Then:

upload poll 3f2a9c1e-…

When poll returns success, the file is in the live workspace and autosave has it.

Do not pass --wait from bash_exec. That call buffers until the upload finishes, so you never see the URL.

Worked example: reverse-curl from another machine

Job: a laptop or box already has hero.png. Mint a capability URL, then stream from that machine. Bytes never ride in MCP tool JSON. There is no MCP tool that accepts file bytes.

upload ~/looks/hero.png --curl

Expected shape (JSON):

{
  "ok": true,
  "id": "3f2a9c1e-…",
  "url": "https://…/sails/transfer/…",
  "method": "PUT",
  "target": "/org/repo/looks/hero.png",
  "max_bytes": 104857600,
  "expires_at": "…"
}

On the machine that has the file:

curl -T ./hero.png \
  -H "Content-Type: application/octet-stream" \
  -H "Content-Length: <n>" \
  "$UPLOAD_URL"

Optional checksum — reject on mismatch before commit:

curl -T ./hero.png \
  -H "Content-Type: application/octet-stream" \
  -H "X-Content-SHA256: $(sha256sum ./hero.png | awk '{print $1}')" \
  "$UPLOAD_URL"

Success is 200 plus { "ok": true, "path": "…", "bytes": N, "sha256": "…" }. Then:

upload poll 3f2a9c1e-…

--curl is file only. --wait is not valid with --curl.

Hand a file out

download inbox/q2-leads.csv
# one-time URL; first successful GET consumes it

download ./site
# browser saves site.tar.gz

Commands

upload help
download help
CommandWhat it does
upload PATHOne-time browser URL for a single file at PATH
upload PATH --curlJSON capability URL for curl -T (file only)
upload PATH --dirOne-time browser URL for a directory (folder picker or .tar / .tar.gz / .tgz)
upload poll IDBlock until that upload finishes, fails, or expires
upload PATH --waitBlock in one command — avoid inside bash_exec. Not valid with --curl
download PATHOne-time URL; directories become .tar.gz

Common flag: --timeout SECS (default 900). After that the session expires; run the command again.

Auth: minting needs a repository member session. Browser upload URLs still need a signed-in member cookie. Reverse-curl URLs are the capability (unguessable id, single-use, short TTL, path-bound) — no browser cookie. Query cannot rewrite the dest path. Hard size limit: 100 MiB. Uploads write into the live session and trigger autosave. Curl writes to a temp file then renames so a failed PUT leaves no partial dest. A second use of a curl URL returns 410.

Failure modes

What you seeWhat it meansWhat to do
You used upload --to DIRThat flag does not existupload DIR/filename or upload DIR --dir
Browser asks to sign in / 403Browser URLs need a repository member cookieThey must be signed in as a member of this repo
Link dead on second openSingle-use (download on first GET; curl 410)Run upload / download again
upload poll times outNobody finished the browser / curl step, or --timeout elapsedNew upload PATH (or --curl), new URL
Path rejectedOutside $HOME / the active repo mountUse a path under the workspace
100 MiB exceededHard limitSplit the archive, or curl -o a public URL
--curl with --dir or --waitReverse-curl is a single file; --wait is browser-onlyDrop those flags; mint with --curl, then upload poll
Curl ok: false / checksum rejectBody never committedFix the file or X-Content-SHA256, mint a new URL

When not to use this

JobUse instead
Small text already in chatMCP write_file / edit_file
File is already on the public internetcurl -o path https://…
API key, token, DSNsecret open — see Store a secret
Public URL for a folderPublish a site
Another repository should copy a folderShare a directory with another repo

How to: Send a file.