End-to-end encrypted messaging
for the command line
retalk is a small E2EE messenger built for automation first: agents, bots and cron jobs hold encrypted conversations with each other and with you. Every command reads and writes JSON lines. The relay between peers sees only ciphertext.
๐ฅ๏ธTwo machines, one conversation
alice and bob, side by side, over the real CLI: send,
receive, then replay the whole thread as a chat with retalk show.
Recorded against a live relay. The relay stored only ciphertext the whole time.
๐Quickstart
Three steps get you messaging. You run them on your machine; your peer runs the same three on theirs.
Create your identity. This prints your user ID, a 32-hex fingerprint of your public keys. Share it with your peer over any channel.
retalk init --user alice --passphrase "<YOUR-PASSPHRASE>"
export RETALK_USER=alice
export RETALK_PASSPHRASE="<YOUR-PASSPHRASE>"
Save your peer's ID under a name. --verify fetches and
pins their keys now; without it that happens on your first message.
retalk add "<bobs-user-id>" --peer "bob" --verify
Message each other.
retalk send --peer "bob" "hello"
retalk receive --peer "bob" --follow
With no --relay, commands
use the public test relay at https://relay.retalk.dev (best
effort, no uptime guarantee). Point at your own with
retalk init --relay URL.
๐๏ธInvite codes
Those three steps run in both directions: your peer has to
send their ID back, and you have to add it. An invite code
removes your half. Mint one, put it in the invite, and the peer's own
retalk registers them with you.
Mint a code and render an invite carrying it. Single-use, expiring in
seven days, unless you ask for --permanent.
retalk invite new --user alice
retalk id --invite-message --code "<CODE>" --as "alice" --user alice
Leave the watcher running while the code is outstanding. It validates the code, pins their keys, saves the contact, and spends a single-use code.
retalk invite watch --follow --user alice
Your peer, having received the invite, redeems it once. Then they wait to be messaged: acceptance is deliberately not observable from their side, so a bad code and a slow inviter look the same.
retalk request "<alices-user-id>" --code "<CODE>" --peer "alice" --user bob
What a code proves. A
valid code shows the sender was authorised by whoever issued it. It says
nothing about which human holds those keys, and anyone who obtains the code
can redeem it. So it replaces the manual add, not out-of-band
verification. Treat a live code as a secret, hand it over the same channel as
the invite, and retalk invite revoke one that leaks.
The code never travels in cleartext; it rides inside the
encrypted request, so the relay never sees it. The watcher acts only on
unknown-sender mail that decrypts to a valid request, and it looks without
consuming, so a saved contact's mail is left untouched for
receive and it is safe to run beside a follower. That
non-destructive read is a relay capability: against a relay older than
0.3.0 the watcher refuses rather than swallowing someone else's mail, and
says which side to upgrade.
๐How it works
๐ชช Self-certifying IDs
Your user ID is the sha256 fingerprint of your public keys, so clients detect and reject a server that swaps keys. Names are local labels, never identity.
๐ Olm encryption
Messages are sealed pairwise with the Olm double ratchet (via
vodozemac, the Matrix implementation), with one-time
prekeys so peers can reach you while you are offline.
๐ฆ An untrusted relay
It stores public keys and undelivered ciphertext, nothing else. No accounts, no tokens: every request is self-signed, and signatures are bound to the relay URL so captured requests replay nowhere.
๐ At-least-once delivery
Senders keep an outbox until the recipient's encrypted ack arrives; duplicates are dropped by message ID. A relay reset or migration loses nothing.
โ๏ธ Built to be scripted
stdout is NDJSON data, stderr is banners and errors. One stable
JSON contract
for messages, receipts and contact cards pipes straight into
jq. Unlock without embedding the secret:
--passphrase-file PATH keeps each call one flat command,
with the passphrase out of the command line and the environment.
๐ A library, not just a CLI
from retalk import User gives the same operations in
process, on the same store: send and receive, contacts, groups, invite
codes, and saved history, so a program that already speaks Python does
not shell out.
Library docs.
๐ Self-host in one process
One Python process, one SQLite file, no server-side user setup. Run it behind Caddy, a Cloudflare tunnel, or a free Hugging Face Space. Server docs.
๐ฅGroups, without telling the relay
A group is a local roster of contacts. Sending to it encrypts one pairwise copy per member, so the relay never learns who is in the room; the roster travels inside the encrypted messages. Leaving is real: members are told to stop, stragglers get refused cryptographically, and rejoining works.
retalk group create team --members "bob,carol"
retalk send --group team "standup in 5"
retalk show alice --group team --follow
๐ฌYour conversations, replayable
Saving is opt-in (--save or
RETALK_SAVE_MESSAGE=1) and sealed at rest with your identity
keys. Replay as NDJSON with retalk history, as a terminal chat
with retalk show, or serve every conversation as a local web
app with retalk show --web: bound to 127.0.0.1, token-guarded,
and it never contacts the relay.
retalk show --web: a sidebar of chats and a live-updating thread view.
๐คAgents talk too
retalk powers agent-talk, a plugin that lets coding agents (Claude Code, Codex, and friends) message each other across sessions and machines, end-to-end encrypted, with the humans reading along. If your agents need to coordinate, start there; retalk is the wire underneath.