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
| Situation | Use |
|---|---|
| A human has the file in a browser | upload 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 internet | curl -o path https://… |
| File is generated in this workspace | Write it here; skip transfer |
| Small text edit | MCP write_file / edit_file |
--dir stays browser / tar only. Reverse-curl is a single file.
Done looks like
- In (browser):
upload poll IDprints success and the file exists at the path you named. - In (curl):
curl -Treturns200withok: true, thenupload pollsucceeds. - Out: the person opens the URL once and the browser saves the file (or a
.tar.gzfor 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
| Command | What it does |
|---|---|
upload PATH | One-time browser URL for a single file at PATH |
upload PATH --curl | JSON capability URL for curl -T (file only) |
upload PATH --dir | One-time browser URL for a directory (folder picker or .tar / .tar.gz / .tgz) |
upload poll ID | Block until that upload finishes, fails, or expires |
upload PATH --wait | Block in one command — avoid inside bash_exec. Not valid with --curl |
download PATH | One-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 see | What it means | What to do |
|---|---|---|
You used upload --to DIR | That flag does not exist | upload DIR/filename or upload DIR --dir |
| Browser asks to sign in / 403 | Browser URLs need a repository member cookie | They must be signed in as a member of this repo |
| Link dead on second open | Single-use (download on first GET; curl 410) | Run upload / download again |
upload poll times out | Nobody finished the browser / curl step, or --timeout elapsed | New upload PATH (or --curl), new URL |
| Path rejected | Outside $HOME / the active repo mount | Use a path under the workspace |
| 100 MiB exceeded | Hard limit | Split the archive, or curl -o a public URL |
--curl with --dir or --wait | Reverse-curl is a single file; --wait is browser-only | Drop those flags; mint with --curl, then upload poll |
Curl ok: false / checksum reject | Body never committed | Fix the file or X-Content-SHA256, mint a new URL |
When not to use this
| Job | Use instead |
|---|---|
| Small text already in chat | MCP write_file / edit_file |
| File is already on the public internet | curl -o path https://… |
| API key, token, DSN | secret open — see Store a secret |
| Public URL for a folder | Publish a site |
| Another repository should copy a folder | Share a directory with another repo |
How to: Send a file.