What the server sees
Hwip relays messages between your computer and your phone through a server I run. This page lists what that server can and cannot read, and where the protection stops.
The short version
- The contents of your questions, answers, notifications, diffs, terminal output and session history are encrypted on your devices before they are sent. The server stores and forwards ciphertext.
- The server does see who is talking to whom: your account, your devices, session names, sizes and timing.
- Two kinds of message are not encrypted: channel posts, and the status notifications the server writes itself ("session finished", "session waiting"), which contain your project and session names.
- The encryption protects you from a server that is leaked or read. It does not fully protect you from a server that actively lies about your device list. Details below.
How it is encrypted
Each device (your phone, each computer) generates its own ECDH P-256 key pair on first use. The private key never leaves the device, and the server stores public keys only and rejects a private-key upload. A message is encrypted once with AES-256-GCM, and its key is wrapped separately for every device on your account.
Encryption is always on, on every plan. There is no setting for it and no plaintext fallback: if a message cannot be sealed, the CLI refuses to send it and says why.
What is sealed
| What | Notes |
|---|---|
| Notifications | Title, body and URL travel inside the ciphertext |
| Questions | Title, body and the wording of each button |
| Answers | Sealed by the phone for the computer that asked |
| Question context | The git diff and the last 60 lines of the terminal that ride with a question |
| Live session view | Terminal frames, your keystrokes, screen snapshots, scrollback pages |
| Diff review | Which file, which comparison and which page you asked for are sealed too |
| Session history | Your prompts, the agent's replies and one line per tool call |
| Approval prompts | The command or diff a session is waiting on, and the numbered answers |
| Files you send from the app | Stored padded, under a record that says only file, application/octet-stream and the padded size |
| Messages between your computers | hwip agent send |
What the server sees
Sealing covers contents. Routing needs some facts in the clear.
| What | Why |
|---|---|
| Your account and your device list, with each device's public key | To know where to deliver |
Session names, such as hwip-myapp | To route a message to the right session. The name comes from your project directory, so the directory's name is visible |
| Which devices a message is sealed for, and which device answered | The key-wrap table is keyed by device id |
| For a question: each button's id (not its label), the timeout, its priority, and a few flags | To expire it and record the answer |
| Sizes and timing | When messages are sent, and roughly how large they are |
| Error codes | A failed request is answered with a short code in the clear |
| Your session list | For each session: its name, the project directory's name (not the path), the label and title it carries, the agent's own session id, its state (working, waiting, done) and when it was last active. Aliases and folders you give sessions in the app are stored too |
| Push type and priority | Whether a push is a note, a clipboard item, a file, a command and so on |
Two details that go further than "sizes":
- The context attachment on a question is always sent as 64 KiB, named
file, also when there is nothing in it or you turned it off. The server can't tell whether a question had context or how much changed. - Files sent from the app are padded, and their real name, type and size are inside the seal. Files sent by the CLI or the MCP server keep their names in the record.
The server never receives a full directory path, only the directory's name.
What is not encrypted
- Channel posts. A push to a channel's subscribers (
hwip notify --channel) is plain text, because subscribers could not open a sealed one. The CLI says so each time you send one. - Status notifications. When a session finishes, ends, or has been waiting for you, the server itself writes a short notification from the session list above. Its title holds the project name or the alias you gave the session, and its body holds the tmux session names. It is not sealed, and it is handed to Google's and Apple's push services as written. What the agent said, and what it is waiting for, is not in it.
- Messages typed before your computer has seen your phone's key. A command sent from the phone while no live view is open goes over a plain request until the phone and that computer have sealed with each other once. After that first time the computer refuses plaintext for good. The same applies to the screen of a
hwip shshell session. - Old app versions. An app build from before per-device keys has no key to hand over, so its live view is plaintext. A computer that has ever seen a keyed device refuses those requests.
Sealing is done by the CLI, the MCP server and the apps. The server does not refuse a plaintext message from a client that chooses to send one, so the "always on" promise is a property of the clients you can read, not of the server.
Where the protection stops
A server that actively lies. Your computer learns your devices' public keys from the server, and that list is not signed. A server that adds a device of its own, or swaps the key of one you know, would have later messages sealed for it too and could read them. It could also send input to your sessions as if it were your phone. Because input from the phone is typed into the agent's terminal, and Claude Code runs a line that starts with ! as a shell command, that means running commands on your computer.
What stands in the way: every device's key is pinned the first time it is seen, and a changed key is reported once (on the terminal, in the Claude Code plugin at session start, or at the end of an MCP tool result) and recorded in ~/.hwip/key-changes.json. It is reported, not blocked. The check you can do yourself is to compare fingerprints: hwip trust prints this computer's fingerprint and the pinned devices, and the app shows the same string on each device's page.
No forward secrecy. The key agreement between two devices is static. If a device's private key is ever stolen, every past message sealed for that pair can be opened.
Secret masking is best effort. The diff and terminal lines attached to a question are scanned for secret-looking text first. A secret in a shape the scan does not know stays in. The attachment is still sealed, so this is about what reaches your own phone, not the server. Turn the context off per project with hwip ask-context off.
Your API key is a key to your sessions. Anyone who holds the API key in ~/.hwip/config.json can send pushes on your account. Keep that file private; the CLI keeps ~/.hwip at mode 0700 and refuses to send the key to a host that answers with a redirect.
Two OS users on one computer share a device id. Each has their own key, so they replace each other's on the device list, and the app warns of a changed key each time.
What stays on your computer
- Private keys:
~/.hwip/device-keys.json. - Tool arguments, tool output and file contents. Session history and the live timeline send tool names, what a tool acted on and line counts, not what it read or wrote.
- Whether question context is switched off for a project. That switch is a local file.
You can turn session history upload off with hwip listener --no-turn-history or "turnHistory": false in ~/.hwip/config.json.
How long things are kept
| What | Kept for |
|---|---|
| Pushes, questions and channel posts | 30 days |
| Files and attachments | 30 days |
| Session history (sealed) | 365 days |
| Server logs | 1 month |
| Your device list | A device not seen for 30 days is removed the next time you register a device |
| Live session view | Not stored. It is relayed while you are watching |
Deletion relies on the database's expiry, which can run some hours late. The production database also has point-in-time recovery turned on, so deleted rows remain recoverable by me for the recovery window.
Deleting your account removes your files, sessions, history, devices, channels you own and your sign-in record. Copies of channel posts already delivered to other subscribers stay until their 30 days run out, and logs and database recovery data age out on their own schedule.
Who else is involved
- Amazon Web Services hosts everything, in the Seoul region (
ap-northeast-2): the database, file storage, the relay and sign-in. - Google (Firebase Cloud Messaging) and Apple (APNs) deliver notifications. iPhone notifications go through Google first, then Apple. For a sealed push they receive a fixed title (
Hwip), a fixed body (New encrypted message), the push id, its type and a hash that names the account, and the app fetches and opens the real content on the device. For the two unencrypted kinds above they receive the text itself. - Google and Apple sign-in. You sign in with one of them, and the server receives your name and email address from it. With Apple you can hide your email.
There is no analytics SDK, no crash reporter and no advertising SDK in the app.
Check it yourself
The CLI, the Claude Code plugin and the MCP server are open source (Apache-2.0), and all sealing happens there and in the app. The server and the apps are closed source, so the server-side statements on this page are my word, not something you can audit.
Questions or a report: support@hwip.app