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.

$ pip install retalk

๐Ÿ–ฅ๏ธ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 conversations and a chat-bubble thread view

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.