What we can and can't see¶
What we (the roamd service) can see¶
Open document paths and reader layout are remembered in your locked vault. We cannot read them. File contents and unsaved edits are not saved there. Conversation view messages, tool details and drafts travel over the same encrypted connection. They stay in page memory, not the vault, and we cannot read them. The open Codex conversation picker uses that connection too. Conversation names, folders and IDs are not readable by the hub. Your computer keeps your chosen link for the life of that terminal's agent; no Codex login details are sent to roamd's hub.
reload -> unlock vault -> reopen file on your computer
| Yes, we can see | No, we can't see |
|---|---|
| Your email address | Your master password |
| That you have an account, and when you log in | What's inside your vault, including remembered document paths and reader layout |
| The names of your computers, whether they're online, which version of roamd they run and when they last connected | Your browser key |
| When you connect to a computer, and for how long | Anything you type or see in a terminal, or the files you send and fetch |
| Who you share a computer with (their email address), the limits you chose, and when a guest connects | What a guest types or sees |
| Your internet address (as any website does) | Your recovery key |
| Which passkeys you've added (their names and when they were used) | Your passkeys' secrets, or your face or fingerprint (those never leave your device) |
| What Voice says or hears (it happens in your browser; no terminal text leaves it) | |
| That your browser fetched the speech model Dictate uses (a plain download, the same file for everyone) | What you say, with Dictate or hands-free (your browser turns it into words; the sound never leaves your device) |
| Your tmux sessions, the programs in them and their screen text (what the tmux add-ons save to bring them back after a restart stays on your computer) | |
| That a computer reported on its sessions, and when | Session names and detailed status (encrypted for your browser); notification events separately reveal an input request or finished turn |
| How busy your computers are (CPU, memory, disk and network): the readings go from your computer to your browser over the encrypted connection | |
| Push addresses, event types and timing, opaque session identifiers, and shared sent/read/handled state | Session names, prompts, answers and terminal text. The push provider gets an empty message |
| The address of a server you set up from the website, and when | Its SSH password or key, or anything sent during setup (your browser runs SSH itself) |
| What our own weekly AI check of the AI apps' command lists cost: how many calls, how big, and the price. It involves no account's text | Anything from a terminal: nothing of yours goes to any AI service through us |
| If you belong to an organisation: its name, your role, whether it has switched you off, and invitations to your address | Your computers, terminals, vault or settings. An organisation's admins see only who is in, their roles, who is switched off, invitations and its audit log |
| If you belong to an organisation: a log of what your account did while you were in it (signing in, failed logins, changes, sharing, guests connecting to your computers), with the internet address, place and browser it came from. If the organisation chose, also which of your computers you connected to, when and for how long | Anything from a terminal. The address is kept nowhere else on disk (the hub only counts requests by address in memory, to limit them), only for members of an organisation, and for the time the owner chose. Nothing new is added once you leave, but what was already written stays until that time is up. Rows about connections carry no address, and a failed login carries the address of whoever tried. Only the organisation's owners and admins can read it, and every member is told on Account |
what travels through us:
your typing --> [ LOCKED ] --> passes through us --> [ LOCKED ]
^ |
| v
locked by your browser your computer opens
it (only it can)
Notification titles can show computer and session names remembered on your own device. These display names stay there while notifications are enabled; locking, signing out or turning notifications off clears them. They do not give the service worker a key to your vault or terminals. Push providers receive no session names.
Amazon SES, which delivers our emails, sees your email address, the codes we send you and the text of our alert emails: what changed, the browser's name and a rough place, and the name of a computer, passkey or organisation, or another person's email address (in an invitation or a share), where the email mentions one. Alert emails never contain a password or a code. It never sees anything from the right column.
Cloudflare, the network company in front of our website, handles the same traffic we do, so it can see the same things we can (the left column), and nothing in the right column.
If our servers were broken into¶
Someone who stole our database would get emails, locked vaults and the records listed above (such as computer names, shares and, for organisations, audit logs with addresses), and nothing they could use to guess your master password on their own computers: the extra secret that would need is kept separately. Someone who took over our servers completely, and so got both, could try guesses on their own computers, one slow try at a time. A long, unique master password is still the best protection.
Limits¶
We'd rather you hear these from us.
- roamd is a website. Your browser runs code our server sends it. If someone took over our server, they could send changed code that captures your password as you type it. We block code from anywhere else, but a complete fix for this isn't there today.
- Logging in on a new browser needs a second step as well as your password: a code we email you, unless you've chosen an authenticator app or a passkey instead. With emailed codes, whoever controls your email inbox and knows your password can still get in; choosing an app or a passkey closes that. A passkey (Face ID, fingerprint, security key) can replace the password (a new browser still needs the second step) and can't be phished. On a browser you've already used, whoever has your passkey device and can unlock it can get in.
- If someone did get into your vault once, changing your password stops them logging in again, but they may have copied your browser key. Replace browser key (Account page) switches every computer to a new one. Computers that are offline keep trusting the old key until they're next online with a roamd page open.
- An open terminal stays open if you revoke one of your own browsers' access, until it's closed. (Removing the computer ends its open terminals, and a guest's terminal is cut as soon as their access ends.)
- Mac and Windows versions haven't yet been tested on real computers.
- Installing roamd keeps a copy of the page on your device, so it opens without a connection. It checks for a newer page every time you open it online. It keeps only the page itself (and the speech model, if you use Dictate): no account data, no keys, nothing from your computers. If our server were taken over, the first limit above applies to this copy too.
- This is a preview: don't rely on it for anything critical yet.
Document-reader restoration stores only the path, optional line target, width and desktop presentation with each browser's encrypted tab references. It uses the vault's authenticated encryption; neither document text nor drafts are persisted. Reload or unlock reads the current file over the owner connection.
encrypted tab references -> unlocked browser -> owner file read
Data inventory (hub)¶
| Data | Stored as | Purpose |
|---|---|---|
| Plain, normalised; an account is only stored once its emailed code is entered | Account lookup | |
| Emailed codes | Live codes as hashes with expiry and tries left (pending sign-ups in memory only) | Proving the address, new browsers, recovery |
| OPAQUE record, Argon2 settings | As OPAQUE requires; useless for guessing without the hub's OPAQUE secrets, which a production hub keeps outside the database (otherwise they sit in it, and a stolen database would allow offline guessing) | Password logins |
| Recovery proof | Hashed; a replaced key and a waiting new key are kept the same way, each with the vault key locked by it | Recovery |
| Vault key | Sealed twice (password key, recovery key) | Unlocked client-side only |
| Vault items | XChaCha20-Poly1305 ciphertext + revision | Sync (browser key, host pins, snippets, machine layout, project labels on sessions, Voice settings, update and idle-lock choices, each browser's open terminals and document-reader references/layout, which guests may send files). The hub sees an item's name and size, not what it holds |
| Sessions and device tokens | Hashed | Web sessions, machine connections |
| Browsers signed in | For each login session: the browser's plain name ("Chrome on Android"), a country or city if Cloudflare supplies one, when it started and when it was last used. No address, no User-Agent string. Ends with the session | The My browsers list |
| Remembered browsers | Hashed random token, per account | Skipping the second step on known browsers |
| Second step | Which one; an authenticator-app key sealed under a key made from the hub's OPAQUE secrets (kept outside the database in production) | New-browser checks |
| Passkeys | Credential ID, public key, signature counter, name, dates; the vault key sealed under a key from the passkey's PRF output (which the hub never sees) | Logging in without the password (new browsers still need the emailed code) |
| Plan | Which plan the account is on, Stripe's reference number for the customer, when the paid period ends or renews, and whether it is set to cancel. No card details: payment happens on Stripe's own pages | The terminal limit on the free plan and the paid features |
| Devices | Owner, name, host public key | Routing |
| Shares | The invited address, the machine, the guest's public key, where the share stands, and the limits the owner chose (watch only, permissions, how long, and the end time once let in). Who is connected to which machine right now, in memory only | Routing, showing each side, ending a share on time, cutting a guest off |
| Open pages | While a page is open and unlocked: which account it is signed in to and which machines it may see, in memory only, gone when it closes. Not its address. Not written anywhere | Telling the page when something changes |
| Session status | The latest sealed reports a machine posted, one per owner key and per guest allowed to see it: opaque bytes, when they arrived, their size. Held in memory, dropped with the machine; written to the database only while the hub restarts (on stopping, read back and deleted on starting); the browser checks the machine's signature and opens them | Overview on Open sessions |
| Notification inbox | Opaque event/session identifiers, machine identity, event type, timestamps, short-lived presence and delivery/read/handled state; no conversation or terminal text | Deduplication, routing and shared state across browsers |
| Push addresses | Each subscribed browser's push address, per account (at most a few); machines' mutes. No keys: pushes are empty | Sending the owner's browsers an empty push when a machine asks |
| Activity | For each account: what happened (kind, a short detail such as a machine name or a guest's address), when, the browser's name, and a country or city if Cloudflare supplies one. No address, no User-Agent string, nothing from terminals. Old events are dropped: a free account keeps the last 30 days and its newest 100 events, a paid one the last year with no cap on how many | The Activity page and emails about changes |
| Conversation view | Nothing readable. Live agent transcripts, terminal captures, Codex picker metadata and input use the existing encrypted owner connection. The daemon keeps automatic or owner-selected process/thread associations locally. The local Codex protocol lists loaded root threads and routes selected Send/Stop by exact thread identity; it does not transmit Codex credentials to the hub. The browser holds view content and drafts only in memory | Inline AI/tmux view, open-conversation picker and Send/Stop |
| AI history | Nothing. What you asked Claude Code or Codex in a computer's tmux sessions, and their final answers, are copied from the app's own record and kept on that computer, and sent only to its owner's browser, over the encrypted connection, when the History page is open. It passes through the relay as bytes it can't read. Summarise runs the session's own app on that computer with the owner's own login: nothing goes through the hub; the last summary is kept on that computer with the history | The History page (read straight from the computer) |
| Machine health | For each machine: when its connection to us last started, when it last ended, whether it ended with a goodbye, and which roamd release it said it runs. Times and a version only; nothing from inside the machine | The machine list ("offline since …", "out of date") and nothing else: no email or push when a machine goes offline |
| Projects (labels on sessions) | Nothing. The labels are sealed in your vault; the server holds only bytes | Projects |
| Snippets | Nothing. Your saved commands are sealed in your vault; the server holds only bytes | Snippets |
| AI app commands | For each AI coding app, the list of its slash commands (title and command), re-read weekly from the app's own documentation, and any change held back for the operator to approve (with a hash of the secret in the link emailed to them). Public information, the same for everyone; nothing from any account, machine or terminal | The command lists in Snippets, and the weekly update itself | | Organisations | The organisation's name and rules (second step allowed, who may share, idle-lock limit); for each member their account, role, whether switched off and when they joined; invitations (address, who sent it, when). Nothing about members' machines, sessions or vaults | Seats, roles, rules, switching people off | | Organisation audit log | For each event of a member while they are in the organisation: when, the account's email as it was then, what happened, a short detail (such as a machine's name), the browser, the place, the member's IP address (not on rows the hub writes with no browser behind them: connections, guests connecting), and for connections how long. Written only while they are members; rows stay after they leave. Never terminal content. Deleted after the retention the owner chose, and with the organisation | What a member did and from where, for the organisation's owners and admins | | AI usage | For each day, account, feature and model: the number of calls, tokens in and out, and the cost. Numbers only, never text sent or returned | Each account's monthly allowance, the cap for the whole service, warnings to the operator, and the usage report | | Transient (memory only) | Login challenges, pending sign-ups and approvals, online tunnels, rate-limit counters (by visitor address) | — | | Logs | Startup and errors only: no request log, no bodies, no shell data | Operations |
The database is owner-only on the hub host. It contains no key that decrypts user data or
logs in to a machine (tested: no raw proofs or tokens on disk).
## Threat model
| Adversary | Protected | Not protected |
|---|---|---|
| Network attacker | TLS to the hub (browsers told to use only HTTPS); SSH end to end; pinned host keys | Traffic analysis (timing, sizes) |
| Cloudflare (in front of the hub) | Shell traffic, vault contents, keys | The same metadata the hub sees |
| Email provider (Amazon SES) | Shell traffic, vault, keys, passwords | Account email addresses, confirmation codes, when they're sent, and the text of alert emails (what changed, browser and rough place, a machine's, passkey's or organisation's name, another person's address in invitations and shares) |
| Hub database leak | Vault contents; tokens and sessions (hashed); passwords (nothing to guess against without the hub's separate OPAQUE secrets) | Emails, device and share records, activity, push addresses, organisation audit logs (with addresses) |
| Someone with the master password | The vault, and so the machines, from any browser not already known (second step) | With the default emailed code: anyone who also controls the inbox |
| Organisation admin or owner | Machines, terminals, vault and settings: no answer to them carries any | Who is in, roles, when people joined; what each member's account did while in the organisation and the address, place and browser it came from (the audit log); they can switch an account off or remove it (stopping its logins), and an invited person learns the sender's address |
| Phishing site | Passkeys (bound to the real site) | A master password and emailed code typed into a convincing fake |
| Malicious or compromised hub (passive) | Shell traffic, vault, keys; the login traffic and stored records alone give nothing to test password guesses on (OPAQUE). With the hub's own secrets as well, offline guessing is possible, slowed by Argon2id | Metadata: who connects to what, when |
| Malicious or compromised hub (active) | Machine authorisation (pairing codes), host impersonation (pins) | **Serving modified web-app code** (see below); denial of service |
| Stolen device token | Can't impersonate the machine (no host key) | Can connect to the hub as that device (disruption) |
| Copied machine settings | Sealed on Windows, macOS and Linux desktops with a keyring | Linux without a keyring (servers, `--no-keyring`) relies on file permissions |
| Malware on the user's machine or browser | — | Out of scope (as for any remote-access tool) |
## Limits
- **Push notifications**: push providers receive empty messages and learn their timing. The hub
separately stores event types and shared delivery/read/handled state, with opaque session
identifiers. It receives no terminal text or conversation contents. When an alert is dealt
with, a second push tells your other devices to take it away; the push provider learns only
that timing too, and the message names just the alert. Its signing key is in
its database; a database leak exposes notification metadata and permits forged empty pushes.
See [notification privacy](notifications.md).
- **Session status**: sealed to the owner's browser key and signed by the machine, so the hub
can neither read nor forge it; but a hub can withhold a report, or hand back an old one. An
old one is ignored once a newer has been seen (remembered across reloads in that browser). The
page never ages a report, since the machine alone decides what a session is doing and says
again whenever that changes; so a hub that withholds reports can leave the last one showing
until the machine is seen offline. The machine's clock only puts its own reports in order,
and is never compared with the browser's. The
report is derived from what is on the terminal, so a guest's key only gets a copy if the owner
allowed it for that guest, and that copy has the same content as the owner's. It also names
which AI coding app a session is running, if one is (a short name such as `claude`, worked
out from the machine's process list), so the page can offer that app's commands; it never
carries the app's arguments.
- **Installed app copy**: the offline cache keeps the public app files (page, scripts,
styles, WebAssembly, icons). A separate notification-display cache holds only local
computer/session labels learned from decrypted owner status, plus translated alert
words. Labels are cleared on lock, sign-out or disabling notifications. No SSH or vault
key, API answer, vault item or terminal bytes are stored by the worker. The page and
worker are always revalidated, so a new release is seen on the next online open. A compromised hub could still serve a changed worker; the mitigation is
the same as for the web-delivered code.
- **Web-delivered code**: the web app (including its WebAssembly crypto) is served by the hub.
An active hub compromise could serve code that captures the master password. Today's
mitigations: a strict Content-Security-Policy (no third-party or inline script) and TLS.
There is no complete fix today; signed, pinned web bundles would be one.
- **Account security**: email addresses are confirmed, and a browser new to the account needs
a second step as well as the password: an emailed code by default (so the inbox is the
second factor), or an authenticator app or passkey if the owner chooses. Passkeys are also
the phishing-resistant alternative to the password. On a known
browser a passkey alone opens the vault, so there it is as strong as the device (or sync
account) that holds it; adding one needs the master password and emails the owner.
- **Browser key**: one Ed25519 key per account, shared by its browsers through the vault. A
password change locks out other browsers; replacing the key retires a copied one, machine
by machine, as each is reached. Until a machine is reached (offline, or an old agent), it
still accepts the old key.
- **Revocation**: revoking a browser key stops new logins at once, but an owner's terminals
already open with it stay open until closed (a guest's are cut). Removing a machine ends
its open terminals, the owner's included.
- **Platform coverage**: the macOS and Windows agents haven't been tested on real hardware
yet (Windows code paths are tested under Wine).
- **Voice**: it never sends terminal text or audio off the device. (An optional AI "Smart" mode
once did; it has been removed.) Speech is turned into words in the
browser; no audio is sent to the hub, the machine or anyone else. The speech model is a
plain file the page downloads from the hub once; the page checks it against a list signed
with roamd's release key before using it, and the page's security policy only lets it talk
back to the hub.
- **Server setup from the web app**: the hub opens the TCP connection to the server's SSH
port, so it learns the address and timing, and relays SSH ciphertext. On that first
connection, the user's fingerprint comparison is what stops a hub from posing as the
server (see [Installation](../manual/install.md)); key authentication removes the risk to
the credential. Nothing about the setup is stored.
- **Preview**: not yet for critical use.