Plain SSH. A key that stays on your phone.
Legio is an SSH client. The free legio command prepares your machine and pairs your phone with it. After that, every terminal, file and metric goes straight from your iPhone to your machine.
Two parts. One connection.
The app on your iPhone and the legio command on your machine. They talk over the SSH server that your machine already runs.
Everything you do
Terminals, panes, Docker, files, ports, metrics
Holds its own ed25519 key in the Keychain. Opens one SSH link for each machine.
The paired key can open a terminal and reach a short list of local ports. Nothing more.
- PTY → herdr terminal attach or a shell
- 127.0.0.1:4499 → Herdr socket (the bridge)
- Ports you allow with --forward-port
Notifications only
When an agent needs input or finishes
Reads Herdr every 2 seconds. Sends a push when an agent blocks or finishes.
Applies your notification rules, then passes the push to Apple.
Tap it to open that machine in the app.
Three steps to your first pane.
Install legio
One binary for macOS and Linux. The installer checks it against the release's SHA256SUMS before it puts it in ~/.local/bin.
$ curl -fsSL https://legioapp.cloud/install | sh
Run it and pair
It sets up the Herdr bridge and the notification watcher, checks your SSH server, and shows a QR code. Scan it with the app.
$ legio
Tap a machine
The app logs in with its key, lists your Herdr workspaces or tmux sessions, and attaches to the real terminal of a pane — live, with no polling.
Your phone makes the key. You approve it.
The QR code holds no password and no key. Only the public half of a key ever leaves your phone, and it gets in only when you say yes at the terminal.
- MachineShows a QR code
It holds the address, the SSH user, the ports, and a one-time token: 32 random bytes that work for one pairing, for 10 minutes.
- PhoneMakes an ed25519 key pair
The private key goes into the iOS Keychain and stays there.
- PhoneSends the public key, signed with the token
An HMAC-SHA256 made with the token proves the request came from someone who scanned the code. The machine checks it in constant time.
- YouCompare five words
The terminal and the phone both turn the key into a phrase like fee-jazz-naive-fruit-equip. If the words match, type y.
- MachineAdds one restricted line to authorized_keys
Then it closes the pairing port. The token is spent, whatever the answer was.
- PhoneSaves the connection and logs in
From now on it is plain SSH with its own key.
$ legio ✓ Bridge 127.0.0.1:4499 → herdr.sock ✓ Notification watcher is running ✓ sshd accepts key login Scan this code with the Legio app. It works for 10 minutes, and you confirm that phone here before it is let in. A phone sent its key: iPhone The app shows five words. Check that they match these: fee jazz naive fruit equip Add it to authorized_keys? [y/N] y ✓ Paired. Sent a test notification.
Everything legio changes. Nothing else.
The services run as your user, and only the pairing port listens beyond the machine — until the pairing ends. You can run legio again at any time: a step that is done is checked, not done again.
The bridge
Herdr listens on a Unix socket. The bridge makes that socket a TCP port on 127.0.0.1:4499 only, so the app reaches it through the SSH link and never over the network. On Linux it uses systemd-socket-proxyd, which ships with systemd. On a Mac it is a LaunchAgent with socat.
The notification watcher
A user service that runs legio watch. iOS stops an app's SSH connection soon after it goes to the background, so the machine tells the phone when an agent changes to blocked or done.
One line for each phone
Each paired phone gets one line, with a comment that names it. legio keeps the lines that are not its own byte for byte.
restrict,pty,port-forwarding,permitopen="127.0.0.1:4499" ssh-ed25519 AAAA… legio-app:iPhone
The push list
One device secret for each phone that gets notifications. Only you can read the file. It holds no Apple token and no APNs key.
What it never touches
The system SSH configuration. legio checks for key login and tells you how to turn off password login, but you make that change yourself.
Safe by design, not by promise.
A phone is easy to lose. So Legio gives each phone the smallest access that works, and makes every step of the pairing one you can see.
The private key never leaves the phone
The phone makes its own ed25519 key and keeps it in the iOS Keychain, which encrypts it at rest. The machine gets only the public half. Passwords for connections that you add by hand go in the Keychain too.
A QR code that opens nothing
The code holds a one-time token, not a login. The token works for one pairing and for 10 minutes. After 5 requests with a bad signature, the pairing port closes.
You approve every phone
No key goes in until a person at the terminal types y. You compare five words, not a long fingerprint. A key made to match those words takes about 3.6 × 1016 tries, inside the 10 minutes the code lives.
A key that can do only what the app needs
OpenSSH's restrict turns everything off. Legio turns back on a terminal and forwards to the bridge port. A stolen phone key gets no agent forwarding, no X11 and no tunnel to other hosts.
The Ports tab reaches more only when you allow it with legio options --forward-port 3000.
Loopback only, on both ends
The bridge binds 127.0.0.1 on the machine, so only an SSH user can reach Herdr. A port forward binds 127.0.0.1 on the phone, so a dev server never shows up on the café Wi-Fi.
Push without the keys to Apple
Only the relay holds the APNs key. Your machine holds one device secret for each phone, and never sees the phone's Apple token. The relay stores the SHA-256 of each secret, not the secret, and sends 30 pushes a minute at most for one phone.
A sealed push secret
The pairing request is plain HTTP, so the phone seals its push secret with ChaCha20-Poly1305 and a key made from the token. The seal is bound to one connection. Later, the app sends the secret again over the authenticated SSH link.
Careful with authorized_keys
Before each change, legio makes a backup copy. It writes the new file beside the old one and renames it into place, so a full disk or a crash cannot leave half a file. It never edits a line that is not its own.
Verified updates
The installer and legio update check each download against the release's SHA256SUMS. The update runs the new binary once before it replaces the old one. If a step fails, the old binary stays.
| What | Where it goes | Who can read it |
|---|---|---|
| Terminals, panes, Docker, files, ports, metrics | iPhone ⇄ your machine, over SSH | You and your machine |
| Your private key and passwords | iOS Keychain only | The app on your phone |
| A notification | Machine → relay → Apple → iPhone | The relay and Apple see its text: the agent's name, its status, and the workspace and pane title |
| Your notification rules | The relay | The relay, to drop the pushes you do not want |
| An account | There is none | Apple keeps your purchase with your Apple ID |
Lost a phone? One command.
Every phone has a name and a key phrase. Remove one, and its key cannot log in again.
See and remove phones
List the paired phones with their key phrases and what each one may do. Remove one by its name or its phrase.
$ legio devices $ legio unpair iPhone $ legio push remove iPhone
Check, or remove it all
check reports on the bridge, Herdr, the watcher, sshd and the phones, and changes nothing. uninstall removes the services, the push list and every phone key.
$ legio check $ legio uninstall
Tip: put the machine on Tailscale so port 22 never faces the internet. legio finds the Tailscale address and puts it in the QR code for you.
Read the code. Then pair.
The legio command and its pairing protocol are open on GitHub.