Two People, One Collaborative Terminal Session in the Browser: How shell.online Decides Who Types During a GhostAction Cleanup
GitGuardian published its write-up of the returning GhostAction campaign on 7 October 2026, and the cleanup it describes is not a one-person job. Gaetan Ferry and Guillaume Valadon counted 772 public repositories hit between 31 August and 30 September, with injected workflows that hardcoded secret names copied from the victims' own workflows and posted the values to a bare IP address over plain HTTP. Their remediation advice has two halves: rotate every secret the workflow could reach, and find and revoke the GitHub credential that pushed the workflow in the first place. That is a long session of gh commands, provider consoles and second opinions, which is exactly when teams reach for a collaborative terminal session in the browser instead of pasting output into a chat thread.
This post is about one narrow piece of that: what happens in shell.online when two or three people have the same terminal open and more than one of them wants to type. The answer is in the relay and host source, so every number below comes from the code on main in the shell.online repository, not from a feature page.
Why rotation work is where shared terminals go wrong
Truffle Security's scan of a 224-million-repository snapshot, reported by BleepingComputer on 30 September, found 543,699 unique credentials that still authenticated, with a median exposure of 784 days. Only one of 101,886 committed npm tokens still worked. The difference is who revokes: npm acts on leaked tokens itself, while a database connection string has no central issuer to do that. For every credential like that, a person has to do the rotation.
GitHub's own secure use reference for Actions says to rotate secrets periodically and keep the GITHUB_TOKEN to the minimum permissions. In practice the rotation after an incident looks like one terminal running commands such as these, with a colleague reading over the output:
gh run list --repo acme/api --limit 50
gh secret set DEPLOY_SSH_KEY --repo acme/api < ./new_deploy_key
gh secret set AZURE_CREDENTIALS --repo acme/api
The last line, with no --body, opens an interactive prompt for the value. If a second person's keystrokes land in the middle of that prompt, the stored secret is wrong and nobody notices until a deploy fails. Interleaved input in a shared terminal is a nuisance in a dev server. In a rotation it produces a broken credential.
Starting the shared session
The person doing the rotation starts a named shell on their own machine:
shell --name "GhostAction cleanup" bash
It prints a URL, a password and a QR code. A teammate opens the link in a browser, enters the password, and sees the same live terminal. There is no SSH account to create and no VPN. The process and its PTY stay on the machine that started it, and the browser is a view onto it. The command reference lists the rest of the local controls: shell list to find the session ID, shell attach <ID> to rejoin it from another local terminal, Ctrl-X then D to detach without stopping it.
Up to 16 browser viewers can join one session; the relay closes the seventeenth with "session is full". Each one appears as a numbered guest avatar, and controller agents connected through an MCP grant show up separately as an "Agent: <label>" chip, so everyone can see who is in the room.
How the typing lock works in a collaborative terminal session
Every input frame from a browser goes through the relay, and the relay decides whether to forward it to the host. The rule is a short lease, 1,800 milliseconds long:
- If someone is typing on the host machine itself (the foreground terminal or a local shell attach), every browser input frame is dropped until that person has been quiet for 1.8 seconds. The host sends a local_typing signal at most every 650 ms while keys are pressed.
- Among browsers, the most recent active typist holds the lease. Input from any other browser is dropped, not queued, while that lease is live.
- An MCP controller agent comes last. The relay comments spell out the order: local host, then browser human, then one MCP writer at a time. A blocked agent write returns busy; it is not queued and its operation ID is not consumed.
On the viewer side, the page shows "Local owner is typing…" or "Guest 2 is typing…" and disables input while someone else holds it. Since v0.13.0 the browser also clears any keystrokes it had queued behind encryption or backpressure when another collaborator takes the lock, so a delayed burst cannot arrive in the middle of someone else's turn.
On the host, all input (foreground stdin, a local attach, browser frames, MCP sends) goes through one arbiter before it reaches the PTY. The relay does not have to read keystrokes to arbitrate. It decides from which socket sent an input frame and when, and with end-to-end encryption on (the default) the frame contents stay sealed between the browser and the host.
What the lock does not do
It is a lease on bursts of typing, not a floor you hold. After 1.8 seconds of silence anyone else with an interactive link can type. If you stop halfway through a command to read something, and a colleague starts typing, their characters land on your half-written command line in the shell. The lock prevents two streams of keys from mixing at the same instant. It does not give you a private turn.
So for a rotation session the practical arrangement is one driver and several watchers, and the cleanest way to get that is to give the watchers a link that cannot type at all:
shell --read-only --name "GhostAction cleanup (watch)" bash
The security model page states that read-only input is rejected by both the relay and the CLI, so a modified browser cannot turn a watch link into an interactive one. In the relay source, a typing event from a read-only session is ignored before it can claim the lease. The tradeoff is that read-only is chosen when the session is created. A watcher who later needs to type needs a new interactive share.
The link is itself a credential, so treat it like one
The share URL and password together are a bearer credential. Anyone holding both can view, and on an interactive link type, with the permissions of the wrapped process. During a secret rotation that process is a shell holding a GitHub token with admin rights on your repositories.
That makes the shared session another secret in the incident. Keep the URL and password out of the ticket and out of any channel with integrations that log message bodies. The QR code carries the password too.
Three controls help here, all documented in the CLI reference:
- --auto-close 2h sets a deadline for the share. The task exiting still closes it earlier.
- shell password rotate <ID> changes the password, salt and cipher generation, disconnects current viewers and revokes MCP grants without restarting the shell. Use it when someone leaves the call.
- shell kill -- <ID> stops the process and closes its share when the work is done.
Rotation does not undo anything. Commands a viewer already typed have already run, and anything they already saw or copied stays with them. The security page says this directly, and it applies to the session password the same way it applies to the GitHub secrets you are rotating.
What the relay can and cannot see
By default the CLI encrypts terminal frames before they reach the relay and the browser decrypts them, so the relay forwards ciphertext. It sees timing, frame sizes, which viewers are connected and who held the input lease; it does not see the gh output or the secret you pasted. The encryption details page covers the key derivation. Running with --no-e2ee drops that and leaves only HTTPS and WSS transport encryption, which would put rotation output in reach of the relay. Do not use it for this.
Two other limits matter on a long cleanup. The host has to stay awake and online, because nothing runs in the cloud. And a session that drops off the network reconnects without killing the local process, but a reboot of the host ends the shell, whatever link you saved.
Where this sits next to Pilot
shell.online is developed by the team behind Pilot Protocol, and the same team maintains it in the open. If you want the basics of the tool before using it on an incident, the introduction to the live terminal browser link walks through the first session. The arbitration code discussed here lives in worker/index.ts (claimInputLease) and cmd/shell/session_unix.go in the MIT-licensed source on GitHub, so the 1.8-second figure is something you can read and argue with.
Pair on a terminal without SSH
Install the CLI, run shell --read-only bash, and send the link and password to the one person who needs to watch. The process stays on your machine.
Try shell.online