What SSH Terminal knows about you
Plain answers, and where in the code each one is enforced. This is the user-facing version of ADR 0006; if the two ever disagree, the code decides and this document is wrong.
The short version
Your SSH session is encrypted between your browser and your server. The relay moves bytes it cannot read. We never see your passwords, your keys, or anything you type or run.
What never leaves your device
Private keys. Imported keys live in IndexedDB, which is per-device and per-origin. They are deliberately not in the synced settings, so they do not travel to your other machines — you import them again there. The agent signs with them locally and sends a signature, never a key.
Host keys (known_hosts). Written by OpenSSH into the same local filesystem. One accepted host leaves several entries — the server hands over its other host keys once the first is verified — so the manager lists a host once and forgetting it drops every key it was trusted for. Forgetting one and leaving the rest would keep the host trusted.
Passwords and passphrases. Typed into a form that hands them straight to the SSH client running in your browser. They are not stored, not logged, and not sent anywhere but into the encrypted session.
Session output. What a server prints stays in the terminal's scrollback in the page, and goes nowhere else. "Save output" writes that scrollback to a file on your machine, through the browser's own download — nothing is uploaded, and nothing is written unless you ask for it.
What syncs through your Google account
Connection profiles — label, host, port, username — and terminal preferences, via chrome.storage.sync. That is Google's sync, tied to your Chrome profile. It does not reach our servers. Delete the extension's data in Chrome and it is gone.
Importing an ssh_config reads the file in the browser and keeps nothing of it: the hosts it describes become ordinary profiles, and so they sync like any other. The file itself is not stored or uploaded, and the keys it names are not read — a key still has to be imported by hand, and stays on the device.
What the relay sees
MV3 extensions cannot open a TCP connection, so every session goes through a relay we run. It is a byte pipe: SSH is end-to-end encrypted, so what passes through is ciphertext.
Picking a relay is a measurement, not tracking: the extension pings each hosted relay's /healthz and connects to the fastest. Like any web request, a ping shows that host your IP address. The winner is cached on your device (chrome.storage.local) for six hours, so most sessions ping nothing; a custom endpoint is never probed at all.
For each session its journal records, and nothing else:
| Recorded | Why |
|---|---|
| Destination host and port | The only thing that answers an abuse report |
| A random per-install id | Counts concurrent sessions so one client cannot park hundreds |
| A random per-session id | Ties one failed session to one entry when you report a problem |
| The extension version | Which release a failed session came from |
| Byte count and duration | Capacity, and the transfer cap |
| Time opened and closed | The above |
Retained 30 days, then rotated out by the systemd journal (infra/cloud-init.yaml.tftpl). There is no database, no account, and no profile of you: the per-install id is a random UUID that maps to nothing.
What the relay does not record: your username, anything you type, anything the server sends back, or your IP beyond the connection itself.
You can take our relay out of the path entirely: Settings → Relay accepts any endpoint, and the protocol is deliberately plain — the extension opens wss://<your relay>/connect?host=&port= with the binary subprotocol, and from then on every WebSocket frame is raw TCP payload in one direction or the other. Nothing else is sent on a fresh connection, so anything that speaks that will do.
Error reports
The extension reports crashes to Sentry unless you switch that off. The rule is enforced in code, not by policy: telemetry never carries a destination. Reports contain the shape of the error, a random per-install id and the session id; the scrubbers in telemetry/scrub.ts strip hosts, users and IP literals from anything on its way out, on both the extension and the relay. A custom relay you point the extension at is reported only as custom-relay, never by name. A local build has no DSN, so the SDK never starts there.
A report also names the extension version and the browser and operating-system version it came from, which is how a crash is tied to the release that caused it. Like any web request it reaches Sentry from your IP address. The report itself carries no IP address; Sentry derives an approximate country (not a city) from the connection and keeps that with it. Nothing else about you is sent: no page visits, no usage counts, no locale or time zone.
Crash reporting is on by default and has an off switch — the Privacy section of the manager — and the choice is honoured before the SDK starts, per device. The first time the terminal opens after an update that changes what is reported, a card says so and points at that switch. One honest caveat: the session id in a crash report also appears in the relay journal above, so for the journal's 30 days we can join a crash to one session's destination. That join is the feature — it is how a reported failure gets diagnosed — and it expires with the journal.
If you uninstall
The extension sends nothing on its way out. Your keys and known hosts go with it: they lived in this browser's IndexedDB, and Chrome deletes that along with the extension. Your profiles and preferences sat in chrome.storage.sync, so removing the extension clears them from your Chrome profile and that removal syncs on — reinstalling later starts you empty, not where you left off.
Chrome then opens sshterm.dev/uninstall, which asks why you left. It is the only thing that crosses the gap: by then there is no extension left to ask anything in. Answering is optional and closing the tab is a complete answer — it is a plain form, and it sends nothing until you press the button. What is stored if you do press it is the option you picked, whatever you typed in the box, and the time — no name, no address, no identifier tying the answer to your installation, your servers, or anything you did with them. Which means we cannot reply to it, and cannot delete "your" response on request, because nothing marks it as yours. Responses expire after a year.
The address the extension hands Chrome carries one thing besides the path: hl, the interface language, so the page can be translated later without a new release. It is the same preference every request of yours already sends in its Accept-Language header, and it is not kept with your answer.
The site itself carries no analytics and sets no cookies. That form is the only thing on it that sends anything anywhere.
What we do not do
No analytics. No advertising. No selling or sharing data. No AI features reading your terminal. No account required, because there is no account.
Contact
Abuse reports and privacy questions: <[email protected]>. Anything else: <[email protected]>.
Last updated 19 October 2026. Questions about any of it: [email protected].