tars

Send a file once. One download, then it is gone.

tars v0.67.0

What it is for

Everything is end-to-end encrypted by default and the server is built so it cannot read any of it — not as a promise, as a consequence of where the keys live. See the FAQ if that sounds like the sort of claim everyone makes.

How it works

  1. Send — pipe a file to the server and get a link.
  2. Share — send the link to one person.
  3. Burn — the first download streams the file and destroys the server copy. A second request returns 410 Gone.

Install

curl -fsSL https://tz.tarbase.com/install | bash

Usage

Send a tz — end-to-end encrypted by default. The key lives after the # and never reaches the server:

$ tars send report.pdf
https://tz.tarbase.com/d/AbCdEfGhIjKlMnOpQrStUvWxYz#k=Uk1vTm9QcVJzVHVWd1h5Wg

Receive a tz (get decrypts automatically):

$ tars get https://tz.tarbase.com/d/AbCdEfGhIjKlMnOpQrStUvWxYz#k=Uk1vTm9QcVJzVHVWd1h5Wg
report.pdf  4.2 MiB  100%  done

Publish in the clear with --plain so any HTTP client (curl, a browser) can fetch it:

$ tars send notes.txt --plain
https://tz.tarbase.com/d/AbCdEfGhIjKlMnOpQrStUvWxYz

Expire it sooner than the 72-hour default with --ttl (90, 30s, 10m, 2h, 1d) — it can only shorten the ceiling, never extend it. --log FILE keeps an audit line per transfer, which is the only way to retain the revoke credential: it is printed once.

$ tars send secrets.db --ttl 30m --log tars.log

Send a folder

Point send at a directory and it streams as one archive — no zipping first, and nothing is buffered whole no matter how big the tree is. Symlinks are skipped rather than followed.

$ tars send ./project
https://tz.tarbase.com/d/<token>#k=<key>

$ tars get <url> -o - | tar x        # or -o project.tar

Shell completions come with the binary: eval "$(tars completion zsh)" (also bash and fish).

A tz is a one-shot send. tars link is the opposite: a live, two-way mirror of a directory. Share a folder and edits flow between you and the other side in real time, end-to-end encrypted — the server only relays ciphertext and can read nothing. The two ends pair on a short phrase, so there is no long URL to transcribe.

The two ends never learn each other's address. Every byte goes through the relay, so neither side sees the other's IP — tools that connect peers directly for speed reveal exactly that, on every transfer. It costs bandwidth and it is the trade we chose: a source and a journalist can share a directory without either learning where the other is sitting.

Share a directory (this keeps running until you stop it):

$ tars link .
sharing this directory — live, guests can edit. Ctrl-C to stop.
on the other machine:  tars link 8823-cyan-falcon-jasmine
8823-cyan-falcon-jasmine

On the other machine, mirror it and open that folder in your editor:

$ tars link 8823-cyan-falcon-jasmine          # → ./tars-8823
$ tars link 8823-cyan-falcon-jasmine work     # → ./work
$ tars link 8823-cyan-falcon-jasmine .        # → here
mirroring into /…/tars-8823 — live, edits sync back. Ctrl-C to stop.
synced. watching for changes…

With no folder given the mirror lands in tars-<selector>, named from the phrase's leading digits — never the whole phrase, since the words are the session secret and a folder name outlives the session. A destination that already holds files is refused unless you pass --force: mirroring overwrites same-named files with the host's version.

Share read-only (guests can mirror but not write back) with --ro.

Why three words are enough

A short phrase is easy to read down a phone or type by hand. It is safe because of how it is used, not because it is long: the phrase never travels.

Both ends run SPAKE2, a password-authenticated key exchange (a PAKE). Each side mixes the phrase into a one-off public value, and only two parties who started from the same phrase end up deriving the same session key. The relay forwards that negotiation without being able to learn anything from it — and, the property that matters, there is nothing to grind offline. Recovering the phrase from a captured session is not a slow computation; it is an impossible one.

So an attacker gets exactly one online guess per attempt, and the host stops serving after five wrong ones. That is what makes a three-word secret sufficient here when it would be laughable for a password file. Nothing is served until the phrase is proven, so a wrong guess leaks no filename and no bytes — it just says so, immediately:

$ tars link 8823-cyan-falcon-zebra
link: wrong phrase — check it and try again (nothing was synced)

Prefer the old long link? --url shares via a URL with the key after the #, exactly like a send.

How live sharing behaves

A drop — tars link --inbox

Guests submit and can never read. They receive an empty manifest, every read request is refused, and your own changes are never announced — so one source cannot learn what another submitted, or what you already had.

$ tars link ./drop --inbox
on the other machine:  tars link 2413-candle-puffin-bamboo

# the source, running the ordinary mirror command:
$ tars link 2413-candle-puffin-bamboo .
link: this is a drop — your files go up, nothing comes down

Each guest writes into from-guest-N/, so a submission can never overwrite your files or another source's, whatever path it claims. The honest limit: this protects what sources can read, not who they are — for that, reach them over Tor.

Detached sessions — tars session

A share normally lives as long as its terminal. --detach unhooks it: the session ignores SIGHUP and SIGINT, so closing the terminal changes nothing, and it reconnects across relay restarts on its own.

$ tars link ./dir --detach --name work
passphrase: ********
session "work" started (detached). Manage it with: tars session

$ tars session
NAME  STATE    KIND  VERSION  DIR       UPTIME / REASON
work  running  link  0.60.0   /srv/dir  2d 4h

stop work ends it (guests see you leave), start work brings it back in the same room, attach work follows its log — Ctrl-C there detaches you, not the session — and rm work forgets it.

Two limits worth knowing: a session does not survive a reboot — there is no autostart yet, so start it again yourself — and stop discards whatever was mid-transfer, which begins again from zero on the next start.

A detached share is always paired by URL, never by a spoken phrase. Three words are 24 secret bits: fine for a pairing you watch happen in a minute, reckless for a room that stands for months, where an attacker can guess at leisure.

The room credential is sealed on disk with a passphrase you choose. start prompts for it; TARS_PASSPHRASE covers cron and boot. The honest limit: on a machine shared with other admins, root can read a running session's memory whatever you do — the seal protects the credential at rest, against co-admins, backups and disk images.

A session you set a passphrase on is encrypted whole, log included: the log names every file operation, so it is sealed on disk with that same passphrase and attach asks for it. cat on the log gives ciphertext. What that protects is the session's history — months of operations, including files that existed and were deleted, which nothing else on the machine keeps. It is not a shield for the shared directory itself, which sits there unencrypted for anyone who could have read the log anyway; and root reads the running process's memory regardless.

The passphrase never leaves your machine, and guests are never asked for it. It unlocks your copy of the room credential and nothing more. A guest joins with the URL alone, exactly as with tars link --url — the passphrase is not a second factor on the room, and the URL remains the only thing that grants access. Treat that URL as the secret it is.

Chat is deliberately not detachable: a detached chat would have to write messages to disk, and keeping no history is the point of it. Use tars link ./drop --inbox --detach to receive while you are away. The listing also shows the version that spawned each session, with a ← restart marker when it lags the installed binary — upgrading does not touch a running session, stop then start re-spawns it on the new binary.

Ephemeral chat — tars chat

The same blind relay, the same spoken-phrase pairing, the same sealed frames — carrying messages instead of files. Nothing is written to disk at either end and no history is kept anywhere, so there is nothing to seize, subpoena or leak after the fact.

$ tars chat
on the other machine:  tars chat 7949-barley-dune-linen
you are cedar-koala. nothing here is written to disk — no history, no log.
type a line and press enter; /quit to leave.
[16:38:48] *: guest 1 joined (1 connected)
[16:38:48] cedar-koala (you): first from host
[16:38:49] siren-pine: reply from guest

$ tars chat 7949-barley-dune-linen
joined as siren-pine. type a line and press enter; /quit to leave.
[16:38:48] cedar-koala: first from host
[16:38:49] siren-pine (you): reply from guest

On a terminal it runs as a split screen — transcript above, prompt below — so a message arriving while you type is drawn above the divider and never smears the line you are writing. Each nickname gets a colour derived from the name, so the same person looks the same on both screens with nothing to negotiate. Piped output drops the split screen and all colour.

Messages can span multiple lines: Alt+Enter adds a line (or end a line with \), and Enter sends. A line beginning with ! runs that shell command on your machine and places its output in the input box to review — nothing is transmitted until you press Enter on it, so a reflexive !cat .env can never reach a peer on its own.

── 2026-08-06 ──
[17:03:26] oasis-ivory: hello from the host
[17:03:30] basil-clay (you): reply from the guest
──────────────────────────────────────────────
> half-typed line

Every line is stamped and named — sent, received, and status lines alike. The date is a divider printed once (and again if the day rolls over) rather than repeated per line; the time travels with the message (the sender's clock, in UTC) so both ends agree. Each end picks a random two-word nickname; it is cosmetic and self-asserted and authenticates nothing — the phrase pairing already proved who your peer is. Joins, leaves and renames are all announced by name, and a newcomer is handed the roster on arrival. /me, /nick, /who and /quit are the commands (/help lists them). Departures are best effort — a crash sends no goodbye, so the host also announces a departure it notices at the transport level, and a peer that vanishes is still named.

A chat session shares no files, even when it happens to run inside a directory full of them.

Ordered event logs — tars stream

A producer somewhere pipes lines in; a consumer somewhere else reads them later, in order, with proof that nothing was lost, reordered or relabeled in between. Think remote log shipping or an audit trail, not a message bus. Each line is sealed on the producer with the key from the URL fragment before it leaves the machine; the relay keeps ciphertext and hands out sequence numbers.

on the producing host:
$ tail -F /var/log/app.log | tars stream send --tag app
https://tz.tarbase.com/s/Qm3…#k=…

anywhere else, later:
$ tars stream get <url> --tag app
2026-08-20T09:12:01Z worker started
2026-08-20T09:12:04Z job 1182 done

send mints that URL itself and prints it, the way send and link do — nothing exists on the relay until a producer connects, so an unused URL costs it nothing. Give send a URL to add a second producer to a stream that already exists, or run tars stream new to get one without starting anything.

send keeps a local outbox and deletes a line only after the relay confirms it is on disk, so a relay outage costs nothing but delay — lines queue and drain in order when it returns. get keeps a cursor and resumes where it left off; a second consumer has its own cursor. --tag labels events on the way in and filters them on the way out; --from N restarts at a sequence number (0 = everything the relay still keeps); --json prints the envelope — sequence, producer, tag, timestamp — with the line; --once catches up and exits, for cron. Unattended: --follow FILE tails a file (it remembers its position, so a restart resumes without a duplicate and rotation is handled), --cmd "journalctl -f -n0" ships a command's output, --out FILE appends events to a file, and --detach runs any of those as a session you manage with tars session, next to your links.

The guarantee is checked, not assumed: every event's producer id, counter, timestamp and tag are bound into its ciphertext, so the relay can filter by tag but cannot change one; the consumer requires each sequence number to follow the last and each producer's counter to follow its previous one, and stops with a non-zero exit on the first hole rather than printing a log with a silent gap. Tags are the one thing the relay sees in the clear, by choice: it is what lets it filter for you. Events are kept for 72 hours.

Passphrase armour — --pass

Links leak: forwarded, screenshotted, backed up by a chat app. --pass folds a passphrase into the key so that neither half opens the tz alone — a leaked link is useless without the phrase, and the phrase is useless without the link. Other tools bolt a password onto a link and make it the only protection once the link escapes; this one keeps both factors.

$ tars send secrets.db --pass
passphrase: ********
confirm passphrase: ********
https://tz.tarbase.com/d/<token>#k=<key>&p=<salt>

$ tars get https://tz.tarbase.com/d/<token>#k=<key>&p=<salt>
passphrase: ********
secrets.db  1.2 MiB  100%  done

The passphrase is never an argument (shell history, ps) and never read from stdin — that is where your file is. Only the random salt travels in the link; the phrase never leaves your machine. Send it by a different route than the link: a phone call is ideal. TARS_PASSPHRASE exists for scripts, with the usual caveat that the environment is not private.

Splitting the key — --split

The link carries no key at all. Send the shares by different routes — one by Signal, one by voice, one on paper — and intercepting any single channel gains an attacker nothing.

$ tars send secrets.db --split 2of3
key split 2-of-3. Send each share by a DIFFERENT route;
any 2 reconstruct it, fewer reveal nothing at all:
  Aa5e6GLJQ05qweVmR2_uD_pytiwQ2NPOBl9DNLX4D_H6
  AnFiPkzQ-D-fPW79ZSobCJU93ys2BdEjNFW2Zf4ogTee
  A812hVYukRDMaRd9e-BI_LDx-N_dTiZ401Plo8eR-3VL
https://tz.tarbase.com/d/<token>#x=2of3

$ tars get <url> --share <a> --share <b>

"Nothing" is literal: Shamir's scheme is information-theoretically secure below the threshold, so with one share short every possible key stays exactly as likely and there is no brute force to mount. Wrong or too-few shares fail before the tz is burned, so a mistake costs nothing.

Time-lock — --unlock

A tz that cannot be opened before a moment you choose — not by policy, by mathematics.

$ tars send embargoed-report.pdf --unlock 2026-08-07T09:00Z
time-locked to drand round 31084260 — openable from 2026-08-07 09:00Z.
Nobody can open it before then, including you.

$ tars get <url>          # before the moment
error: this tz is time-locked until 2026-08-07 09:00Z — the key to open it
does not exist yet. Nothing was downloaded and the tz is NOT burned

drand is a public randomness beacon: a group of independent organisations jointly publish a signature every three seconds, forever. Round numbers are predictable; their signatures are not. tars encrypts the key so only a chosen future round's signature can recover it — and that signature does not exist yet. Not for the recipient, not for the relay, not for us, not for anyone holding a subpoena.

This is the trustless counterpart to the dead man's switch. That one needs something to witness your silence, and the witness is the relay; this needs no witness at all. Neither can do the other's job — no amount of cryptography can notice you have gone quiet.

Honest limits: it depends on drand continuing to publish, and get must reach drand to fetch the round, so opening a locked tz is not an offline operation.

Dead man's switch — --deadman

$ tars send insurance.tar --deadman 7d            # release (default)
$ tars send creds.env     --deadman 24h:destroy

$ tars checkin <token>
checked in — release fires 2026-08-13 19:18Z (in 168h) unless you check in again

release — the tz answers 410 to everyone until you stop checking in, then becomes burnable once. Nothing is published anywhere: it stops refusing a fetch from whoever already holds the link. destroy inverts it — delivers normally, collects itself if you go quiet.

Only you can see the state, through tars watch; to everyone else an armed tz is indistinguishable from one that never existed. A distinct "not yet" would leak both the existence and the schedule of an insurance file to anyone holding the URL.

Be clear about the trust this adds: "if I stop checking in" cannot be made trustless — someone must witness your silence, and here that witness is the relay. It still cannot read the file, so the exposure is timing rather than content, but this is the one place the server can affect an outcome.

Tripwire — a burn is also an alarm

A tz's bytes can only be read by burning it. There is no way to look without taking. That makes a burn a detection event: plant a decoy in a channel you suspect someone is reading, and if it ever burns, someone was reading.

$ tars send q3-board-minutes.pdf --tripwire
tripwire armed. Watch it with:  tars watch AbCdEfGhIjK
https://tz.tarbase.com/d/…#k=…

$ tars watch AbCdEfGhIjK
armed — watching every 30s. Ctrl-C to stop.
TRIPPED 2026-08-06T14:32:11.204Z — someone burned it

Name one per channel — the inbox, the shared drive, the group chat — and the one that burns names the leak. Link previewers cannot set it off: unfurlers issue a plain GET, which only ever returns the landing page.

watch's exit code is the signal (1 = tripped), so alerting is yours to wire up and tars never learns your phone, your email, or a webhook. --once checks and exits, for cron; --interval N sets the poll seconds.

$ tars watch AbCdEfGhIjK || ntfy send "that channel is being read"

Two honest limits. It tells you that it was read, never who — no IP, no user agent, just a timestamp, because behind a proxy the rest is fiction anyway. And whoever trips it does get the decoy's contents, so plant bait, never real secrets.

Stripping metadata — --sanitize

Metadata is what burns sources. A photograph carries the coordinates where it was taken and the camera's serial number; the content was never the leak. --sanitize strips EXIF, XMP, comments and PNG text chunks on your machine, before encryption.

$ tars send photo.jpg --sanitize
sanitized: removed EXIF/XMP (GPS 51.5074,-0.1278) (156B)

JPEG, PNG, PDF and Office (docx/xlsx/pptx). A PDF's Info dictionary and plaintext XMP are blanked in place; an Office file's docProps are removed while every other part stays byte-for-byte.

The rule is strip what we can reach, and name what we cannot. An encrypted PDF, or one that keeps its objects in compressed streams (most modern PDFs), is refused whole rather than half-cleaned. Author names hiding in an Office file's tracked changes or comments are flagged, never silently kept. Anything unrecognised is reported not cleaned and sent unchanged — a sanitizer that quietly passes a file through is worse than none, because you would believe it worked.

Vaults — tars lock / unlock

Everything else here is built to vanish. A vault is the opposite: durable encrypted storage you keep on purpose, so your work survives someone deleting it.

$ tars lock ./repo
passphrase: ********
confirm:    ********
verifying...
/home/you/repo.tars — 1,204 files, 892MiB
delete /home/you/repo/ (1,204 files, 892MiB)? [y/N] y
deleted /home/you/repo
the passphrase is the only key — there is no recovery

$ tars verify repo.tars       # before you rely on it as the only copy
vault intact — 1,204 files, 892MiB

$ tars unlock repo.tars       # restores the tree where it was

lock deletes the original only after writing the vault, reading it back in full and asking you — a tool that deletes your work on the strength of an unverified write has no business existing. --yes skips the question for scripts. Anything that fails leaves you exactly where you started.

There is no recovery. No shares, no printed backup, no support address that can help — the passphrase is the only key, and that is the point rather than an omission.

A vault is an ordinary file, so it composes with the rest: tars send repo.tars puts an off-machine copy somewhere and tars get brings it back. Sending is optional; the feature stands alone.

Moving a credential between machines

On a terminal, send and link both print a scannable QR of the credential — the tz URL for a send, the phrase for a live share. It goes to stderr, never stdout, so it can never end up inside a pipe or a captured variable: only on a screen you are looking at.

Scan it with a phone, or with a screen reader when the two machines share no clipboard — a locked-down VM, a remote console, a screen share:

$ screencapture -i /tmp/qr.png && zbarimg -q --raw /tmp/qr.png | pbcopy

Because a phrase is far shorter than a URL, its QR is smaller and scans more easily than the one a send produces.

Behind a proxy

HTTP_PROXY, HTTPS_PROXY, NO_PROXY and ALL_PROXY are honored by send, get and link alike — the live WebSocket too, not just the HTTP calls.

ALL_PROXY may be a SOCKS5 URL, which covers what a corporate HTTP proxy does not: Tor, and an improvised tunnel through any host you can SSH into — no firewall ticket required.

$ ssh -D 1080 jump-host
$ ALL_PROXY=socks5://127.0.0.1:1080 tars send report.pdf
$ ALL_PROXY=socks5://127.0.0.1:9050 tars link .   # over Tor

Hostnames are resolved at the proxy, so DNS never leaks locally. One limitation: through any proxy an upload is buffered in memory instead of streamed, so proxied sends are capped at 256 MiB and say so rather than failing halfway. Direct sends still stream to the full 5 GiB.

If you want the mechanism rather than the usage — the single SQL statement that makes “exactly once” true, the tz1 encryption framing, what the relay can and cannot see, how SPAKE2 makes a three-word phrase safe, and how an interrupted transfer resumes — read the internals.

What to expect

Questions people actually ask

Why not just use Signal or WhatsApp?

Because a disappearing message still lived in two chat logs on two phones before it disappeared, and you control neither. Backups, screenshots, a borrowed handset, a compromised OS — any of them keeps it. With tars what remains in the conversation is a link, and once it has been used that link is a 410 forever. The link outlives the secret, and a dead link tells nothing about what it once pointed at.

Signal is excellent at being a messenger. This is a different job: moving one thing, once, and leaving nothing behind on either side.

Can the recipient just screenshot it, or keep a copy?

Yes, of course. Anyone who can read a file can keep it. tars controls the server copy and the number of deliveries — it cannot control a person who already has the bytes, and no tool that claims otherwise is telling you the truth.

Who runs this, and why should I trust you?

You should not have to, and that is the point of the design rather than a slogan. Files are encrypted on your machine, and the key travels in the part of the URL after # — which browsers and clients never send to a server. The server stores sha256(token), not the token; ciphertext, not content. A live share derives its key from a phrase typed on both machines, so the key never crosses the wire at all.

Compare that with the usual reassurance, which is jurisdictional: a company promising that its country's laws will keep it from looking. That is a promise about behaviour. This is a claim about capability — the difference between "we will not read it" and "we are not able to." Everything above is checkable in the source, and the internals page shows exactly where.

What happens if the server is seized?

Whoever takes it gets the ciphertext of whatever has not yet been downloaded, the size and the hashed token of each pending tz, and nothing else. No keys, because they were never there. No filenames for encrypted sends — the name rides inside the ciphertext. No history of live shares beyond session sizes and times, and no chat content at all, because chat is never written to disk anywhere.

Is it actually deleted, or just marked deleted?

The bytes are unlinked from the filesystem as the download streams — the transfer completes from a file handle that no longer has a name. Afterwards the request is indistinguishable from any other failure: burned, expired and never-existed all answer 410, so the response cannot be used to prove that something was ever there.

I lost the link. Can you recover it?

No. There is no account to log into, no copy of the key, and no way to identify which tz was yours. That is the same property that makes the rest of it true; it is not an omission we plan to fix.

Is there a web uploader?

Deliberately not. Encrypting in a page means re-trusting whatever JavaScript the server sends you, on every single visit. A compiled binary is verified once. Browsers can still receive: opening a link decrypts in the page, which is a much smaller thing to trust than handing it your file.