Anthropic published its threat intelligence report for December through August this week. The section on the ShinyHunters affiliates has one sentence worth reading twice: "New credentials and durable access are minted so the operation outlives rotation: cloud API keys in victim accounts, platform developer keys, forged sessions and 2FA codes, network backdoors." The same section reports one compromise that went "from a single stolen developer token to full administrative control of a victim's cloud environment in roughly three hours."
GreyNoise's PaperCut writeup from Wednesday is the same story at a different scale. Hundreds of agents on OpenAI's Codex harness and a DeepSeek model, at least 440 PaperCut instances at 395 organizations in 48 countries, and credential harvesting on 280 of the victims. Only 12 organizations reached domain admin. The harvested credentials from the rest keep working until every secret in them changes.
That is the whole problem with revocation. Deleting the record of who was given a secret does nothing to the secret. You have to change the thing it unlocks.
We shipped three shell.online releases today (0.12.1, 0.12.2 and 0.13.0), and the piece that matters for that problem is a new command:
shell password rotate <session-id-or-prefix>
What rotate does, in the order it does it
A shell.online session is a browser link to a process running on your machine. The link carries a salt in its URL fragment, the person opening it types a password, and the two together derive the AES-256-GCM key that the browser and the CLI share. The relay in between forwards ciphertext and never gets the password. Before 0.12.2 there was no way to change that password on a running session. Cutting someone off meant killing the process and starting over.
The rotation sequence in session_unix.go runs on the host, inside the process that already owns the terminal:
- Generate a fresh password and a fresh URL salt. Generated passwords are now ten Base64URL characters, each one an independent six-bit symbol, so 60 bits of entropy.
- For a persistent session, write the new fragment, key and password to the owner-only state file first, so a restart comes back with the new credentials rather than the old ones.
- Swap the frame cipher. From this instant, input encrypted under the old key fails authentication and is dropped.
- Only then send the relay a control message that says
credentials_rotateand nothing else. No password, no salt, no key. - Wait up to three seconds for the relay's acknowledgement. No ack means a full rollback: old cipher, old state file, old credentials.
The comment above step 3 explains why the cipher swaps before the relay is told: "Reject old-key input before asking the relay to disconnect its holders. This removes the acknowledgement-window race." If the order were reversed, a viewer holding the old password would have a window between the relay's message and the host's swap where their input still decrypted.
On the relay side, the handler in worker/index.ts closes every viewer socket with code 4003 and the reason "session credentials rotated", then acks. The comment there is the design in one line: "The relay never receives either credential. It only creates a hard boundary."
The last step is the account registry, if you are signed in. The CLI re-registers the session with a credential_rotation flag, and the server runs one transaction: update the stored share URL, delete every sealed password copy for that session, insert the owner's new copy sealed to their vault key. Teammates who had a copy no longer do, until you share again. The registry stores ciphertext it cannot open, before and after.
What removing a teammate deletes, and what it cannot
Removing someone from a team now deletes every session-password copy sealed to their account, across all of the organization's sessions, in the same transaction that drops their membership. That query is in store-postgres.ts under removeMember.
The changelog states the limit rather than hiding it: "Owners must still rotate active sessions to revoke passwords a former member may already have seen." A sealed copy in the registry is bookkeeping. The password itself is a bearer secret, and if it was ever revealed on a session page or unlocked in a browser, the person may still have it. Removal cleans up our side. Rotation is what changes the lock. And, quoting the CLI help text: "Rotation cannot erase terminal output somebody already saw."
This is the same distinction the Anthropic report draws for cloud credentials. Machine identities have a cleaner answer. Agents on Pilot Protocol authenticate with Ed25519 identities and per-tunnel X25519 key agreement, so the credential is a keypair rather than a string that gets typed and copied. A terminal share is different because a human types the password into a phone. The honest position is that a shared password is revocable only by replacing it, so the replacement had better be one command and not a restart.
The self-hosted relay does not do this yet
0.12.2 also added a standalone relay for an ordinary Docker host: Node.js, WebSockets, a metadata state file, and Caddy for TLS. The self-hosting docs cover it, and the Compose file runs the relay container read-only with every capability dropped. The state volume holds session metadata and host-token hashes, not terminal contents, keys or passwords.
Rotation is not wired into it. The host control handler in standalone/server.ts accepts local_typing, local_attached and exit, and closes the socket with code 4002 for anything else. So shell password rotate against a standalone relay hits that close and rolls back after the three-second wait. The Cloudflare Worker relay has the branch, the standalone one does not. That gap is ours to close. The CLI side already rolls back cleanly when a relay does not understand the message, so the failure is a clear error rather than a half-rotated session.
The rest of 0.13.0, briefly
The account vault is now optional. When you do set one up, it unlocks with a password, a WebAuthn passkey that supports the PRF extension, or the recovery key. The passkey path derives the unwrapping secret from the authenticator's PRF output, so a phone or hardware key can open the vault without a typed password.
Session lists in the accounts app now ask the relay whether a session is actually alive, instead of treating every unclosed database row as online. The check is rooted at the operator's configured relay origin, with a 2.5-second timeout, a 30-second cache on success, and a cap of 30 checks per minute. The code comment on that function is a small security decision in itself: "A registry row is caller-supplied data and must never become a server-side fetch target."
One fix came from outside the company. A first-time contributor found that --auto-close today had never worked, because a bare day keyword resolved to midnight, which had always already passed. It now means the last second of the local day.
Top comments (0)