End-to-end encryption
Turn on end-to-end encryption for a host so approvals, prompts, and usage leave it sealed. Only your phones can read them, and Moshi's servers can't approve anything for you.
Most of what you do in Moshi never touches Moshi's servers. Terminal sessions, Chat View transcripts, diffs, files, and Browser preview all travel directly between your phone and your host. The exception is the small set of events moshi-hook sends so your phone can be woken up: inbox rows, push notifications, approvals, Live Activities, and usage. Hooks privacy lists them.
End-to-end encryption (E2EE) seals that content on the host before it leaves. Moshi's servers, Apple's and Google's push services, and anything in between only relay ciphertext. Your phones open it with a key that never leaves the host except over your own SSH connection.
E2EE is a per-host switch and is off by default. It needs moshi-hook 0.4.32 or later and Moshi 4.1.0 or later on every phone you use with that host. Earlier app versions can't read or approve anything an encrypted host sends.
Turn it on
On the host:
$moshi-hook e2ee enableE2EE is on (key 1).Open Moshi on your phone and connect to this host over SSH so it can fetch the key.
You can also turn on End-to-end encryption from Moshi: on Home, tap Hooks, pick the host, and use the switch under Options. The switch is only reachable over your SSH connection, never through Moshi's servers.
Then connect to the host once from each phone. Moshi fetches the key over the verified SSH session and keeps it in the phone's secure storage (the Keychain on iOS), only on that device: not in iCloud, not in backups. From then on that phone decrypts the inbox, notifications, Live Activities, the usage widget, and its paired Apple Watch by itself. The Watch never holds the key: the iPhone opens content for it and seals its answers.
How the key works
- The key lives on the host.
moshi-hook e2ee enablecreates it in a file next to moshi-hook's other secrets on the host. - Phones get it over SSH only. A phone that has never connected to the host over SSH in Moshi has no key. It shows "Encrypted" in place of the content and can't approve.
- Moshi's servers can't approve for you. With E2EE on, the host ignores any approve or deny that isn't sealed with its key. Your phone seals each decision to one specific approval, so it can't be replayed or redirected.
- Notifications fall back safely. If a phone can't open something (no key yet, or a push too large to carry the sealed content), it shows a generic "Approval needed on a host" with no action buttons. Open the app to read and answer it.
What Moshi's servers still see
Encryption covers content, not the fact that something happened. With E2EE on, this is everything that still reaches Moshi's servers in readable form:
| What | Readable by Moshi's servers | Sealed |
|---|---|---|
| Agent events (inbox, notifications, Live Activity) | Event and session IDs, which agent (Claude Code, Codex, …), the event type and category (for example "approval required"), expiry, and whether the push is silent | Title, prompt and response text, approval command or question, choices, project and pane names, model, tool, context remaining, account |
| Approvals | Approval ID and its timestamps | Title, message, choices, your decision, and the result |
| Usage | Which agent, when it was captured, and each window's used percentage and reset time | Account ID, account name, and host name. Each account shows up as an opaque ID, so Moshi can tell two uploads are the same account but not whose. |
| Session picker | That the host sent one | Every session, window, and pane in it |
| Host status | Counts of detected dev servers and simulators, and your current and latest moshi-hook versions | Nothing in them needs sealing |
Usage percentages stay readable so the server can keep your widgets up to date.
Like any service, Moshi's servers also see request metadata: your host's ID, your IP address, and when and how often your host sends things. Together with the readable fields above, that shows when an agent is busy or waiting on you, but not what it's doing.
Pairing a host happens before E2EE applies. It shares the host's display name, platform, hostname, SSH login user and port, and whether Remote Login and Mosh are available, so Moshi can set up the connection on your phone.
What never reaches Moshi's servers
With or without E2EE:
- Your terminal sessions, which run over SSH, Mosh, or Eternal Terminal directly to the host.
- Chat View transcripts, diffs, files, Browser preview, and Simulator preview, which use the gateway that
moshi-hookserves on the host and your phone reaches through its SSH connection. - Your agent account credentials.
moshi-hookreads rate limits directly from each provider (Anthropic, OpenAI, and others) on the host. - The E2EE key.
Rotate, revoke, or turn it off
# Is it on, and which key is current (never prints the key)$moshi-hook e2ee status# Start a new key; older keys still read past items for 7 days$moshi-hook e2ee rotate# New key, and older keys can no longer approve anything$moshi-hook e2ee rotate --revoke# Back to plaintext; keys are kept 7 days to read old items$moshi-hook e2ee disable
Phones pick up a new key or a disabled policy the next time they connect to the host over SSH. Until then, items under the new key show as "Encrypted" on that phone.
Approvals that are still open when you run rotate --revoke can't be answered from a phone. Answer them on the host.
Limits
- Revoking doesn't lock out a phone that can still log in. Any phone with SSH access to the host can fetch the current key again. To cut a phone off, remove its SSH access first (
moshi-hook host revoke <id>), then runmoshi-hook e2ee rotate --revoke. - A compromised host or phone isn't protected. Whoever controls the host account or an unlocked phone holding the key can read what it reads.
- No forward secrecy. Someone who later obtains a key can read anything sealed under it that they captured.
- Notification banners are drawn by the push service. Moshi never lets a banner's text decide what an approval does: the app only acts on content it opened and checked itself.