Analysis · September 16, 2026
The ring said Sent
A dead man's switch told its owner the delivery had gone out, during the window in which he could still have stopped it. Nothing had been sent. The backend was right the whole time, and the screen could have caused the event it was falsely reporting.
On 2026-09-02 at 18:12:16 Eastern, a dead man’s switch I run on my own account expired. I had let it run down on purpose, because I wanted to see what the grace period looked like from the owner’s chair. The countdown ring on the dashboard turned and said, in two lines, “Sent” and “delivered.”
Nothing had been sent. I checked in three minutes and forty-five seconds later, the switch reset, and the audit trail for that account shows five escalating reminders, one expiry, one reset, and zero dispatch rows. The last real delivery on the account was two months earlier.
If you build anything with a countdown and a consequence, this is the failure to be afraid of. A switch that fires early is a bug. A switch that tells the owner it already fired, while the cancel window is still open, is a bug that removes the one person who could have stopped it. The screen can cause the event it is falsely reporting.
Here is what a dead man’s switch has to get right, learned by getting it wrong.
The clock has one owner, and it is not the phone
Etergis is a switch with a vault behind it. You check in on a schedule. Miss the deadline and a grace period opens, one hour by default, longer if you set it. Check in during grace and nothing happens. Let grace close and your recipients get what you left for them. A kitchen timer with a snooze button, where the thing on the stove is your estate.
The ring had a parameter for the grace window. It was added on 2026-07-26 in a commit titled “Show the cancel countdown during grace, not ‘Sent / delivered’.” The parameter’s own doc comment predicted what would happen if a caller left it out. Nine places in the client draw that ring. The commit wired one of them. The primary ring on the dashboard, the one an owner looks at, was not it.
With the parameter absent the widget had nothing to compute a window from, so its “in grace” test was false from the instant the deadline passed, and it fell through to the branch for a delivery that had already happened. Six weeks in the field.
A test existed. It proved the ring renders grace correctly when told the window. Its last case omitted the parameter, asserted that the ring says “Sent,” and was named “no graceSeconds keeps the old behavior.” The suite did not miss the defect. It enforced it.
The fix had the same bug in a different costume
Auditing the first bug turned up a second one, and it was inside the fix.
The client’s fallback for the grace window was arithmetic: deadline plus grace seconds. The server does not measure grace that way. It measures from expired_at, the moment the sweep observed the expiry, precisely so that an outage in the sweeper cannot eat the owner’s window. That rule dates from a 2026-08-08 incident where a 20-minute sweeper gap meant the first tick after recovery found the grace already elapsed, sent a reminder reading “0s remaining,” and dispatched in the same breath.
So a client that computes deadline plus grace lands earlier than the server every time the sweep lagged, and prints “Sent” while the server still has minutes left in which the owner can cancel. Same lie, different road. And expired_at was not on any API response, so no client could have got it right.
The correction is dull, which is the point. The server now publishes grace_ends_at on the heartbeat and on every ring response, computed beside the code that enforces it. Clients prefer it. The local arithmetic stays as a fallback only because it errs early, and early is the safe direction for a cancel window: telling an owner they have less time than they do costs a check-in, and telling them they have none costs the delivery.
Two checks that could not pass
Neither fix is interesting. The checks around them are.
A widget test cannot catch a defect that lives at the call site, so the client grew a test that reads the call sites. It walks the source tree, finds every ring with a deadline, and fails with a file and line number if the grace parameters are missing. It is crude, it took fifteen minutes, and it would have caught the six weeks.
The status vocabulary got the same treatment. The server now declares the set of ring statuses it can write, next to the code that writes them, and a contract test checks three directions: nothing assigns an undeclared status, no client compares against a status the server cannot produce, and no server status goes unhandled by every client. On its first run it found the clients testing for a status called “grace” that the server has never written, at four sites across the Dart, Swift, and Kotlin code, while ignoring “warning,” which the server writes every time a reminder fires. The negative control did not have to be manufactured.
The test refuses to skip when it runs in CI, because a green run that checked nothing is the failure mode it exists to prevent. 663 API tests and 124 client tests ran green with it, and the fixes shipped in 1.1.22 on both stores on 2026-09-07.
What sits in the queue for years is the other half
A dead man’s switch has a second property that a countdown does not: the payload waits. Not for a session. For as long as the owner keeps checking in, which is meant to be years. That is the harvest-now-decrypt-later surface in its purest form, a ciphertext whose whole job is to sit still until a date nobody has picked yet.
So the copy of each secret’s key that belongs to the owner, the one that sits at rest for the life of the switch, is wrapped with a hybrid of X25519 and ML-KEM-768. Both shared secrets are concatenated and fed into one HKDF-SHA256 derivation, and the wrap key falls out of that. An attacker has to break both. A defect in the newer primitive degrades to the classical baseline, never below it.
Three things about how it went in, because the how is where these migrations get lied about:
Nothing was forced. The old envelope format is marked by a null version column and stays valid indefinitely. An owner who cannot log in, which is the whole premise of the product, must never lose a secret to a format change. Owners who can log in get upgraded opportunistically: on any passphrase-gated decrypt, the client re-wraps the owner’s old envelopes to the hybrid format, unwraps the result, compares it to the original key with a constant-time check, and refuses to persist if they differ.
Delivery is not post-quantum, by design. When the switch fires, the recipient’s copy is protected by an Argon2id passphrase, not by a key-encapsulation mechanism. Its quantum exposure is Grover’s algorithm, which is quadratic and blunted by the work factor, rather than Shor’s, which is the one that ends X25519. Making delivery hybrid would require recipients to hold long-term keys before they know they are recipients. That is a different product, and it is deferred, not unfinished. The accurate sentence is: stored information is hybrid post-quantum, delivery is Argon2id. “End-to-end post-quantum” would be false, and I would rather you read the true version here than the false one on a landing page.
The client code is not constant-time. The pure-Dart X25519 and ML-KEM implementations carry secret-dependent timing, the same class as KyberSlash. Decapsulation runs on the owner’s own device with no remote oracle to time against, and the hybrid construction bounds the damage at the X25519 baseline, so the audit verdict was adopt with mitigations. It is a caveat, and it belongs in the open.
What I would do
If you are building a countdown with a consequence, put the clock on the server and ship the answer, not the ingredients. A client that derives a deadline from parts will get it wrong the first time the parts disagree, and the disagreement will land on the side that hurts.
When a widget gains a parameter it cannot function safely without, write the check that reads the call sites the same day. The type system was happy with all nine of ours.
Never let a test document an unsafe fallback without saying so in its name. “Keeps the old behavior” read as approval for six weeks. The case is now named for what it does: a caller that omits the window gets the unsafe legacy render, with the incident date in a comment.
And name the surface, not the product. Which copy of which key, protected by what, for how long. The dull sentence is the one that survives an audit.
If you want to see the switch itself, it is at etergis.com, and the offline decryptor that opens an export without the service existing is public at etergis-recover. That second link is the one I would test first.
Provenance
Observed in production (Verified). The 2026-09-02 timeline comes from the audit trail of my own account: reminders, expiry at 18:12:16 Eastern, reset at 18:16:01, no dispatch, release-token, or release-email rows. The one-of-nine wiring, the test that asserted “Sent,” the 2026-07-26 commit, and the six-week exposure are read from the repository history. Test counts are from the CI run that shipped the fix.
Read from the code (Verified). The grace rule measured from expired_at, the grace_ends_at field on the wire, the declared status vocabulary and its contract test, the one-hour default grace window, the null-version-means-old-format rule, the concatenation of both KEM shared secrets into HKDF-SHA256, and the constant-time self-check before a re-wrap persists were each traced in the source while writing this. The repository is private; the offline decryptor that consumes the same envelope formats is public and MIT-licensed.
Cited, not re-derived (Reported). The Grover-versus-Shor exposure argument is standard and I have not re-derived it. The timing caveat rests on an independent audit of the Dart ML-KEM library from 2026-07-06 whose verdict was adopt with mitigations; the KyberSlash comparison is the audit’s, not mine.
Designed, not built (Proposed). A hybrid delivery path in which recipients hold long-term keys is designed and deferred. Nothing above should be read as saying it exists.
If something here is wrong, email the studio and I will correct it in place with a note saying what changed.