eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

πŸ€– 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/eleven

From source (developers):

go build -o eleven ./eleven

Sign 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:

  1. eleven signin mints 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).
  2. 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.
  3. 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 message

eleven 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.