Terminal Sharing Security When Infostealers Go After Coding Agents: What shell.online Leaves on Disk, and What to Revoke
On September 8, Gen Digital's Jan Rubín published a report on infostealers that now collect AI agent data. Remus goes after Claude, Cursor and OpenCode. The macOS stealer Djinn is associated with Claude, Codex, Gemini, Cline, OpenCode and Kilo. CallbackBeaver alone showed more than 5,000 samples in a recent 30-day window. The targets are cached access and refresh tokens, MCP configs, and prompt histories. Gen closes with advice for the people who build these tools: keep less on disk, keep auth material apart from history, use the OS credential store, and issue short-lived, narrowly scoped tokens. That advice applies to terminal sharing security as much as to coding agents, so we ran the same audit on our own tool.
shell.online wraps exactly the programs on that list. You run shell claude or shell opencode, and it prints a browser link to that running terminal, an eight-character password and a QR code. The process stays on your machine, and the relay forwards only encrypted frames. (The introductory post on the browser link for any terminal covers the basics.) The question here is narrower: if a stealer runs as you on that machine, what does it find, and what should you revoke afterwards? Everything below was read from the shell.online source on GitHub at the current main branch. The latest CLI release is v0.25.0.
The account link: one JSON file, only if you ran shell auth
Without an account, this file does not exist. If you link a machine with shell auth, the CLI does an OAuth authorization code exchange with PKCE and a loopback redirect, the pattern in RFC 8252, OAuth 2.0 for native apps. The service rejects any redirect URI that is not a loopback callback and accepts only the S256 challenge method. The resulting tokens go to credentials.json in a shell-online folder under your OS config directory: ~/.config/shell-online/ on most Linux setups, ~/Library/Application Support/shell-online/ on macOS. The SHELL_ONLINE_CONFIG variable overrides the location.
The file is plain JSON, written with mode 0600 inside a 0700 directory. It holds an access token, a refresh token, the expiry, your account email and name, and the remote-start choice. It is not in the macOS Keychain. Gen's report recommends the keychain, and it is a fair comparison: Claude Code's authentication docs say it stores credentials in the encrypted macOS Keychain and falls back to a 0600 file on Linux. File permissions stop other users on the box. They do nothing against code running as you, which is how a stealer runs.
So what does a stolen copy of that file get someone? From the server code:
- Access tokens last one hour. The service stores only hashes of the tokens and checks revocation on every request, so a revoked token stops working on its next call, not when it expires.
- The refresh token is not rotated on use. Refreshing returns a new access token and keeps the same refresh token, so a copied refresh token keeps working until someone revokes it. This is the weakest point in the design.
- The token can publish sessions and list the sessions you started. Before that list is returned, the server strips the sealed password copies kept for organization members. Share URLs stored by the service keep at most a #salt= fragment, and a raw #key= fragment is removed before publishing. The salt is not a secret: no key derives without the eight-character password.
In short, the account token tells a thief which sessions you run, on which machine, and when. It does not give them a password or any terminal content.
The more valuable file is the live session record
This one matters more, and it exists whether or not you have an account. On Unix hosts, each active session has a record at /tmp/shell-online-<uid>/<id>.json. The directory is 0700, and the CLI will not read sessions from it if another user owns it or its permissions are looser. The file is 0600. It contains the share URL and the session password, because that is what shell list and shell password <ID> read when you ask for them.
A URL plus its password is a complete bearer credential. Anyone holding both can view the session and, unless it is read-only, type with the permissions of the wrapped process. If an infostealer grabs that directory while shell claude is running, it has not stolen a token for a service. It has a live terminal on your machine. The shell.online security page states the general rule plainly: end-to-end encryption does not protect against malicious code on either endpoint. Our encryption design keeps the relay from reading frames. It cannot keep a process running as you from reading a file you can read.
Two more files fall into the same category. If you use --persistent <state-file> so a link survives restarts, that state file is what brings back the same URL and password, so treat it as a credential too. And a --files share exposes a directory on purpose, which a thief with the password reaches like anyone else.
Terminal sharing security after a suspected infection: the cleanup order
Gen recommends starting from a clean device, and that applies here. If the laptop may still be infected, any new password you generate on it lands in the same readable record. The order we would follow:
- From another device, unlink the machine. The Machines page in the shell.online web app revokes that machine's token. Because revocation is checked on every request, the stolen refresh token and any access token derived from it stop working right away. This works even if the laptop is offline.
- Decide whether the host is clean. If it is not, stop the sessions instead of rotating them. Run shell list, then shell kill -- <ID> for each one. A killed process cannot be reached through a stolen password.
- If the host is clean and the work must keep running, rotate. shell password rotate <ID> disconnects existing viewers, changes the password and salt, and revokes MCP grants. The wrapped process keeps running. Hand the new link and password only to the people who need them.
- Revoke agent access explicitly. Rotation already revokes grants, but shell mcp revoke-all <ID> is the direct command when you only need that.
- Re-link deliberately, or not at all. shell logout stops the background daemon first, attempts the remote revoke, and deletes credentials.json even if the service is unreachable. When you link again, consider shell auth --no-remote-start on a laptop you never need to start sessions on from a browser.
The full flag list is on the shell.online command reference.
None of these steps can undo what already happened. Rotation and revocation cut off future access. They cannot recall commands someone typed into a session, or output they already read. If a thief had a live interactive link, treat everything that process could touch as exposed: the repository, the credentials in its environment, and the agent's own login.
Remote start widens what a linked machine accepts
Remote start is opt-in for a reason. With it on, your signed-in browser can ask a linked, online machine to start a process, and the machine runs it. When the browser picks a session password, it seals it to a key the machine published, so the accounts service relays an envelope it cannot open. That keeps the service out of your sessions. It does not make remote start a small permission: the docs call it remote execution authority. A machine that only ever runs shell locally does not need it, and shell daemon status shows whether the listener is running.
What we would still change
Measured against Gen's checklist, two gaps are ours. Linked-machine tokens live in a file, not the OS credential store, and the refresh token does not rotate on refresh. Both appear in the open code, in internal/account/credentials.go and the service's token module, so anyone can verify whatever we change next. The short access-token lifetime, per-request revocation checks and per-machine unlinking are what keep the current design manageable.
The session record is a different kind of trade-off. Recovering a password on the host is a feature people use, and anything the CLI can read back, a process running as the same user can read too. If that is the wrong trade for your threat model, keep sessions short with --auto-close, use --read-only when nobody needs to type, and kill sessions you are not watching.
Check what your own host is holding
Run shell list on any machine where you share terminals, then compare it with the Machines page in the app. Anything you don't recognize is something to kill, rotate or unlink.
Try shell.online