Sign-out never cleared local storage. It stopped push, flushed settings and removed the subscription — that last one reasoning explicitly that a browser left holding someone's mail becomes somebody else's next — and then left the settings cache and <accountId>:recent on disk. That list is other people's email addresses, and nothing ever removed it.
Changes
1. Sign-out clears local data, unconditionally. Lending a laptop is the same exposure as a public machine, only quieter. Clear-by-default with a short keep-list — lastUser (only a trusted device writes it), the trust flag, and the random push device id — so a key added later is forgotten by default rather than by nobody having thought about it. Also runs on a 401.
2. The checkbox asks a question people can answer. "Keep me signed in on this device" defaulted to true, which assumed the answer most costly to get wrong: someone on a library machine got a 30-day cookie unless they noticed a ticked box.
before
after
question
"Keep me signed in on this device"
"This is my own device"
default
ticked
unticked
hint
none
says what each answer does
3. Untrusted means untrusted: session cookie only, nothing written locally, no push subscription, and a 5-minute idle sign-out. Reads are gated as well as writes — a machine trusted once still has residue — and an untrusted sign-in purges it outright.
On the idle timer instead of a reminder
A "remember to sign out" prompt can't work: custom beforeunload text was removed from browsers years ago, and no event fires at all for walking away from a signed-in screen, which is the case that matters. A timer needs nobody's cooperation.
Note on remember
The wire field keeps its name. It is persisted inside SESSION_FILE, so renaming it would invalidate every session file on upgrade for a change of vocabulary. Client-side the concept is isDeviceTrusted(), derived from the session so a reload reaches the same answer without storing it — which an untrusted device could not do anyway.
Verification
Tests: 359 web (+8 new covering both sides of the gate and the timer) and 80 server, typecheck and build clean.
Driven in a real browser against the mock, because a privacy control that only passes unit tests isn't verified:
Sign-out never cleared local storage. It stopped push, flushed settings and removed the subscription — that last one reasoning explicitly that a browser left holding someone's mail becomes somebody else's next — and then left the settings cache and `<accountId>:recent` on disk. **That list is other people's email addresses, and nothing ever removed it.**
## Changes
**1. Sign-out clears local data, unconditionally.** Lending a laptop is the same exposure as a public machine, only quieter. Clear-by-default with a short keep-list — `lastUser` (only a trusted device writes it), the trust flag, and the random push device id — so a key added later is forgotten by default rather than by nobody having thought about it. Also runs on a 401.
**2. The checkbox asks a question people can answer.** "Keep me signed in on this device" defaulted to `true`, which assumed the answer most costly to get wrong: someone on a library machine got a 30-day cookie unless they noticed a ticked box.
| | before | after |
|---|---|---|
| question | "Keep me signed in on this device" | "This is my own device" |
| default | ticked | **unticked** |
| hint | none | says what each answer does |
**3. Untrusted means untrusted:** session cookie only, nothing written locally, no push subscription, and a **5-minute idle sign-out**. Reads are gated as well as writes — a machine trusted once still has residue — and an untrusted sign-in purges it outright.
## On the idle timer instead of a reminder
A "remember to sign out" prompt can't work: custom `beforeunload` text was removed from browsers years ago, and no event fires at all for walking away from a signed-in screen, which is the case that matters. A timer needs nobody's cooperation.
## Note on `remember`
The wire field keeps its name. It is persisted inside `SESSION_FILE`, so renaming it would invalidate every session file on upgrade for a change of vocabulary. Client-side the concept is `isDeviceTrusted()`, derived from the session so a reload reaches the same answer without storing it — which an untrusted device could not do anyway.
## Verification
Tests: **359 web** (+8 new covering both sides of the gate and the timer) and **80 server**, typecheck and build clean.
Driven in a real browser against the mock, because a privacy control that only passes unit tests isn't verified:
| path | result |
|---|---|
| untrusted sign-in, full session, folder expanded | `localStorage` **empty throughout** |
| trusted sign-in | writes `settings`, `a1:recent`, `lastUser` as before |
| sign-out from trusted | `recent` and `settings` gone, `lastUser` kept |
| untrusted sign-in afterwards | clears even `lastUser` |
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_011pJ3S59iaT91dqW8rV3xFa
**Merged** 2026-08-28 as coffey-labs/ihasmail@ddd1bbf9b357
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Sign-out never cleared local storage. It stopped push, flushed settings and removed the subscription — that last one reasoning explicitly that a browser left holding someone's mail becomes somebody else's next — and then left the settings cache and
<accountId>:recenton disk. That list is other people's email addresses, and nothing ever removed it.Changes
1. Sign-out clears local data, unconditionally. Lending a laptop is the same exposure as a public machine, only quieter. Clear-by-default with a short keep-list —
lastUser(only a trusted device writes it), the trust flag, and the random push device id — so a key added later is forgotten by default rather than by nobody having thought about it. Also runs on a 401.2. The checkbox asks a question people can answer. "Keep me signed in on this device" defaulted to
true, which assumed the answer most costly to get wrong: someone on a library machine got a 30-day cookie unless they noticed a ticked box.3. Untrusted means untrusted: session cookie only, nothing written locally, no push subscription, and a 5-minute idle sign-out. Reads are gated as well as writes — a machine trusted once still has residue — and an untrusted sign-in purges it outright.
On the idle timer instead of a reminder
A "remember to sign out" prompt can't work: custom
beforeunloadtext was removed from browsers years ago, and no event fires at all for walking away from a signed-in screen, which is the case that matters. A timer needs nobody's cooperation.Note on
rememberThe wire field keeps its name. It is persisted inside
SESSION_FILE, so renaming it would invalidate every session file on upgrade for a change of vocabulary. Client-side the concept isisDeviceTrusted(), derived from the session so a reload reaches the same answer without storing it — which an untrusted device could not do anyway.Verification
Tests: 359 web (+8 new covering both sides of the gate and the timer) and 80 server, typecheck and build clean.
Driven in a real browser against the mock, because a privacy control that only passes unit tests isn't verified:
localStorageempty throughoutsettings,a1:recent,lastUseras beforerecentandsettingsgone,lastUserkeptlastUser🤖 Generated with Claude Code
https://claude.ai/code/session_011pJ3S59iaT91dqW8rV3xFa
Merged 2026-08-28 as coffey-labs/ihasmail@ddd1bbf9b3
Rebuilt from: git history, session transcript.