eleven / messenger Public
π€ eleven β the command-line client
eleven is Eleven Messenger in your terminal: a full E2EE member, not a gateway or a bot. It signs in with the same passkey backup or invite links every device uses, holds its private keys in local files, and encrypts/decrypts everything itself β the servers see exactly what they see from any other device: ciphertext.
It's a separate small binary (like push/ and home/): members install the client, operators install the server.
Installing
From the Mac app (how members get it): the Mac app bundles the CLI at Eleven.app/Contents/Helpers/eleven β universal, signed, and updated by Sparkle alongside the app (the "Embed eleven CLI" build phase in ios/project.yml compiles it into every build). Settings β General β Command-line tool β Installβ¦ drops a symlink where you choose (/usr/local/bin keeps it on PATH); the app is sandboxed, so the folder pick runs through the powerbox, and when a folder needs an administrator the pane hands you the one Terminal command instead:
sudo mkdir -p /usr/local/bin && \
sudo ln -sf "/Applications/Eleven.app/Contents/Helpers/eleven" /usr/local/bin/elevenFrom source (developers):
go build -o eleven ./elevenSign in with Eleven (eleven signin)
One passkey, every space. eleven signin pulls the spaces saved in your Eleven account (the home server's passkey-protected backup) and signs the terminal into all of them at once.
A terminal can't run a passkey ceremony, so the browser does it β the same handoff the native Mac app uses when its passkey provider can't help:
eleven signinmints an ephemeral keypair + nonce, opens a local loopback listener, and sends your browser to home's restore page (payload in the#fragmentβ the home server never sees it).- You approve with Face ID / Touch ID / your passkey. The page decrypts your backup in the browser, seals the home session to the ephemeral key, and redirects to
http://127.0.0.1:<port>β hardcoded loopback, ciphertext all the way. - The CLI unwraps it, pulls the vault, and writes one identity per space under
~/.eleven/spaces/. Archived spaces are skipped.
SSH'd into a box whose browser is elsewhere? eleven signin --paste prints the link and takes the sealed code by paste instead of a redirect.
The home session lands in ~/.eleven/home.json, so later eleven signin runs re-sync silently β no browser. Note the trust profile: that file (like the native apps' Keychain copy) can fetch and open your whole backup, and the space session files are the space keys themselves. ~/.eleven is your key ring; deleting it signs the terminal out of everything.
Joining one space by invite (eleven join)
eleven join # prompts for an invite link (and your name)
eleven join --name "Paul" 'https://yourspace.11ch.at/join/β¦#β¦'All three link shapes work:
- A fresh invite makes you a new member with a new keypair.
- A friend's invite link (the kind that opens a DM with them) joins you and unlocks that chat β the DM key is re-wrapped to your new keypair locally during the claim.
- An add-device link (from your own account) signs the terminal in as you: the link carries your keypair in its
#fragment, so the terminal becomes another of your devices and can read everything your others can.
Your identity lands in ~/.eleven/spaces/<host>.json (0600) β or pass -session file (ELEVEN_SESSION) to pin one identity file and ignore the store, for scripts. Session files are private keys: anyone who reads one can read those chats, and deleting your only device's key loses it for good β same as any device.
Two honest gaps on a fresh join, both inherent to how keys travel: your roster name waits for the instance key, which another member's device grants once it's online (eleven retries the publish on every run until it lands β your messages carry your name regardless), and chats appear only once someone DMs you or adds you to a group. Joining a space where Eleven runs, the welcome DM shows up on its own.
Talking
eleven spaces # what this terminal is signed into
eleven chats # your chats across every space
eleven chat # open your most recently active chat
eleven chat kitchen # β¦or match one by name (case-insensitive)
eleven chat family/kitchen # space/name scopes when names collide
eleven send eleven "what's the weather like for Saturday?"
git log --oneline -5 | eleven send eleven # pipe stdin as the messageeleven chat is a live loop: incoming messages print as they arrive over the WebSocket, and anything you type sends on Enter (Ctrl-C or Ctrl-D leaves). DMing one of the space's bots this way is Claude-in-your-terminal over your own E2EE infrastructure β the bot can't tell the terminal from the app.
What v1 deliberately leaves out
Rendered messages are new chat lines (plus /me-style action lines and a "[n button(s)]" note on messages carrying chips). Not yet rendered: edits, deletions, reactions, typing indicators, attachments, link previews β and there's no thread creation from the CLI (start chats in the app). Incoming lines also interleave with whatever you're typing, classic-IRC style; a proper composer line is a follow-up.
How it's built
internal/client is the E2EE member client layer β join/claim (join.go, mirroring web/static/join.js headlessly), the WebSocket transport with thread-key unwrap, cursor-gated delivery and reconnect (client.go, the transport half of internal/bot's runtime with the bot policy left out), and the invite-link parser (invite.go, mirroring web/static/invite.js). The bot sidecar still carries its own copy of the transport; moving it onto internal/client is the planned follow-up, so the two can't drift. Session files are byte-compatible across the CLI, the Go bot, and the Node sidecar.
E2E coverage lives in eleven_client_test.go (root package, against the real router): all three join paths, name decryption, live delivery, and both-ways sends.