What it is for
- Send a password, an API key or a recovery phrase to a colleague — without it living forever in a chat log.
- Move a file off a server you are SSH'd into, with no shared clipboard and no scp credentials.
- Hand a journalist or a lawyer something that must not sit in an inbox.
- Mirror a working directory between two machines, live, without a git remote or a cloud folder.
- Give a source a way to reach you that keeps no history anywhere.
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
- Send — pipe a file to the server and get a link.
- Share — send the link to one person.
- 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).
Live sharing — tars link
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
- Two-way and live. Save a file on either side and it appears on the other within about a second.
- Mirrors land in a fresh folder. With no destination given it is
tars-<selector>; pass one to choose. A folder that already holds files is refused unless you pass--force, because the host's version overwrites same-named local files. - The host is the source of truth. If the same file is edited on both sides at once, the host's version wins and your version is kept beside it as
name.conflict-<timestamp>.ext— nothing is silently lost. - Encrypted, relay is blind. The session key is derived from the phrase and never reaches the server. Anyone who has the phrase can join and edit, so treat it like a password.
- It stays running. Both sides keep a process open;
Ctrl-Cends the session. It auto-reconnects across brief network drops. - Efficient at any size. Files stream in chunks (up to 2 GiB each), and after the first sync an edit re-sends only the changed pieces, not the whole file.
- Everything syncs unless you exclude it. Dotfiles,
.gitandnode_modulesall travel; a.tarsignoreat the link root narrows that with gitignore-style patterns (.gitignoreitself is not consulted). With no.tarsignorethe host prints one line naming what it is about to share. Note that a live.gitsynced between two machines can corrupt the repo, and symlinks are never followed, so a mirrorednode_moduleswill not work — exclude both. - Interrupted transfers resume. Lose the connection at 90% and the reconnect asks for the missing 10%, not the whole file. Bytes already received are reused only when their chunk hashes match, so resuming can save bandwidth but can never assemble a wrong file.
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.
Under the hood
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
- A sent tz works exactly once. If the download is interrupted, the tz is burned and the sender must re-send.
- Unburned links expire after 72 hours.
- Maximum file size is 5 GiB.
- No accounts, no sign-up. A tz URL is the only credential for a send; a phrase is the only credential for a live share.
- Encryption is on by default and happens on your machine — the server stores ciphertext and never sees the key. Anyone holding the full link (including the part after
#) can decrypt, so share it like the file itself. Use--plainonly when you want a publicly fetchable link. - The revoke credential is printed once and is otherwise unrecoverable — pass
--log FILEtosendto keep it. The key (the part after#) is deliberately never written to that log. tars linkneeds both ends on the same recent version. Runtars upgradeif a session won't connect.
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.