GreyNoise published its write-up of the PaperCut NG/MF campaign this week. One operator built a working exploit in a lab, then handed the rest of the job to hundreds of agents running on OpenAI's Codex harness with a DeepSeek model behind it. The tally in the GreyNoise report: at least 440 PaperCut instances at 395 organizations in 48 countries, credentials harvested from 280 of them, and a 28-country avoid-list that the agents ignored in places. GreyNoise's own phrase for that last part is "agents gone wild".
The number worth sitting with is 280. Each of those credentials now belongs to somebody else too, and the only remedy the owner has is to make it stop working from this point forward. Nothing revokes the past.
That is the right frame for what we shipped in shell.online 0.12.2 today, because the feature has the same shape and the same limit.
What shell password rotate does, in order
Every encrypted shell.online session has a browser password and a share URL whose fragment carries a 16-byte salt. The key is PBKDF2-SHA256 at 600,000 iterations, and frames are sealed with AES-256-GCM. The default password is six random bytes in base64url, which is where the eight characters come from. Until today the only way to change any of that was to restart the process. For a coding agent an hour into a task, that is not a real option.
The rotation path lives in cmd/shell/session_unix.go and runs like this:
- Generate fresh material: a new salt, a new derived key, a new cipher.
SHELL_ONLINE_E2EE_PASSWORDpicks the replacement password if set, otherwise one is generated. - If the session runs with
--persistent, write the new fragment, key and password into the owner's state file first, so a restart comes back with the new credentials rather than the old ones. - Update the owner-only local record. This is the record that
shell password <ID>prints. - Swap the host cipher. From this instant the host seals output and opens input under the new key only. A frame sealed with the old key fails authentication.
- Send a
credentials_rotatecontrol message to the relay and wait up to three seconds for an acknowledgement. No ack means every step above is rolled back: cipher, local record, state file.
The ordering of steps 4 and 5 is the point. The comment in the code says it plainly: reject old-key input before asking the relay to disconnect its holders, because that removes the acknowledgement-window race. A viewer holding the old password cannot get a single keystroke through in the gap between the host deciding to rotate and the relay dropping them.
What the relay does with it
The relay's side is a few lines in worker/index.ts. On credentials_rotate it sends a credentials_rotated notice to each viewer socket, closes each one with code 4003, and acks the host. The comment above that block is the security claim in full: the relay never receives either credential. It only creates a hard boundary. A viewer reconnecting with an old password may reach the socket, but cannot authenticate one encrypted frame.
So the relay learns that a rotation happened. It does not learn what changed. That is the same position it was already in: connection metadata, frame sizes, timing, lifecycle events, and nothing readable.
There is a negotiation step that matters if you self-host. The hosted relay advertises credentialRotation: true on its terminal_size message, and the CLI only enables rotation after seeing that flag. Without it the command refuses with "the connected relay does not support live password rotation". The same release ships a standalone single-node relay for ordinary Docker hosts, documented in docs/self-hosting.md: Node.js, WebSockets, Caddy for TLS, no Cloudflare account. That relay does not advertise the flag yet. Rotate against it today and you get the refusal, not a silent no-op, which is the failure mode we wanted.
What the accounts service does
If the machine is linked with shell login, the CLI re-registers the session with credential_rotation: true. The handler in app/server/app.ts checks that the session exists in the caller's organization, that the caller owns it, that it is encrypted, and that the new share URL differs from the old one and ends in a 22-character salt. Then, in one transaction, it updates the share URL, deletes every sealed password copy for that session, and inserts only the owner's fresh copy sealed to their vault.
Teammates who had the session lose their copy in that transaction. The CLI's last line of output says so: existing viewers were disconnected, share the new link and password.
The limit, stated in the help text
Removing someone from a team already deletes every session password copy sealed to their account, across all of the organization's sessions. That was not enough on its own, and the changelog says why: owners must still rotate active sessions to revoke passwords a former member may already have seen. The shell help password text carries the harder version of the same sentence. Rotation cannot erase terminal output somebody already saw.
Which brings it back to the 280 harvested credentials. Revocation is a forward-only operation everywhere, and a feature that pretends otherwise is the dangerous kind. The companion command, shell password <ID>, is equally blunt about the other direction: it prints from the owner-only local record, an unlocked account vault can recover copies sealed to it, and if neither copy exists there is deliberately no recovery backdoor.
One more fix in the same release
The release also carries a fix from a new contributor, @monperrus. shell help reference listed today as an accepted value for --auto-close, but the bare keyword resolved to midnight, which has always already passed by the time any command runs. The documented form could not succeed at any hour of any day. It now means 23:59:59 local, and the unquoted form reports the real reason a date was refused instead of calling it a grammar error. Small fix, but a doc that lists an option which can never work is the sort of thing that erodes trust in the rest of the doc.
shell.online is built at Pilot Protocol. The source, including everything cited above, is in the TeoSlayer/shell.online repository.
Top comments (0)