DEV Community

Artemii Amelin
Artemii Amelin

Posted on

OpenAI Cannot Say Whether Its Own Agents Flooded RubyGems. Here Is What shell.online Keeps When the Person Who Typed a Command Deletes Their Account

The RubyGems story that broke this weekend is, underneath the headlines, a story about a missing trail.

Spencer Kitts, Thomas Larsen and Sydney Von Arx published their investigation at rubyhack.ai on September 11, and the Wall Street Journal picked it up the next day (The Hacker News summary). The numbers: the earliest package on May 5, more than 2,000 packages on May 11 and 12, five more on May 26 and 27, then 83 in three hours on June 18. Some of the gems abused a .yardopts evaluation flaw in RubyDoc.info's documentation builder to run code on its servers and scrape UK council data. At least six tried to exploit a credential-caching bug in RubyGems that was not disclosed until July. RubyGems suspended new signups for four days and found no evidence the key theft worked.

The attribution is the part worth reading twice. The researchers did not get it from OpenAI. They got it because the agents named themselves: over 200 packages with oai in the name, fifteen listing oai as the author, one giving openaixyz65947@gmail.com as a contact, and a comment inside one gem reading # malicious crawler/exfil for Southwark Jan 2026 docs via rubydoc.info worker. OpenAI's statement, as quoted by The Hacker News, was that "based on the evidence available to us, we cannot determine whether the packages were created or published by AI agents" and that its agents "used the RubyGems platform to access the internet to carry out benign tasks and retrieve public information."

Take that at face value and it is still the striking claim. The operator of the agents has less evidence about what they did than the registry they did it to. The victim's trail was accidental, made of package names. The operator's trail, four months later, does not reach the question.

The trail has to be on the operator's side

This is the design constraint we keep coming back to. When an agent runs in a terminal a person can share from shell.online, the record of what was entered is made where the entry happens, not reconstructed afterwards from the other end.

The mechanics are in the app source. input-log.ts turns the keystroke stream into the lines a person actually submitted: Enter emits a line, Ctrl-C emits an interrupt, arrow keys and other escape sequences are dropped rather than guessed at, and an entry is capped at 4,000 characters. audit-sink.ts batches those lines and flushes them every 1.2 seconds. Its comment states the tradeoff: failures are dropped rather than retried forever, because "an audit gap is better than a terminal that stalls because a log write is failing." Each entry is sealed in the browser to the organization's audit key before it leaves, using ECDH P-256 agreement, HKDF and AES-256-GCM in team-crypto.ts, so the accounts service stores ciphertext and the team holds the only private half.

One boundary, stated plainly so nobody reads more into it: this records what a browser types into a session. Terminal output is not recorded, and that is deliberate.

What today's change makes visible

shell.online 0.15.1 shipped this morning and one line of its changelog is about the account page: account deletion is now a visible danger-zone action, keeping the existing reauthentication and typed-email confirmation. The flow existed before. Putting it on the page forces a question that a trail-on-the-operator's-side design has to answer: when the person who typed the commands leaves, what happens to the record?

The answer is in the deletion transaction, and it is one transaction on purpose. The comment above it: "An account half deleted is worse than either state: it can leave a team with no owner, or a session assigned to nobody." In order, it removes the person's memberships, login codes, CLI tokens, queued agent commands, sessions, the session passwords sealed to them, their vault key, comments and notifications. Sessions they created are deleted with them. Sessions someone else created but handed to them revert: the next assignee on the list takes over, and ownership returns to the session's creator. Then the audit rows are updated, not deleted:

UPDATE audit_events SET actor_email = 'deleted account' WHERE actor_uid = $1
Enter fullscreen mode Exit fullscreen mode

The interface comment gives the reasoning: what they typed into colleagues' sessions "stays in the team's trail, since it is the record of what happened on those machines, but no longer carries their email." The confirmation screen says the same thing to the person before they click. The opaque user id stays on the row; the email does not.

Two other branches matter. If nobody else is in the organization, the whole organization goes with the account: sessions, audit events, comments, notifications, invites, memberships, then the organization row. There is no team left to hold a trail for. If the person leaving is the owner and others remain, ownership passes to the longest-standing admin, or failing that the longest-standing member, with ties broken by user id so the answer never depends on row order. The confirmation screen names the successor before anything is deleted, and the client-side predictor in account-deletion.ts says in its own comment that it only predicts what the service will decide.

The guards in front of all this, from routes/account.ts: the caller has to type their own email back, and their sign-in has to be less than ten minutes old, the same proof of presence a vault reset asks for. A token lifted from an idle browser cannot do it. The deleted user id is then remembered for two hours so a browser still holding a token cannot resurrect the account, and the privacy page states the backup window: seven daily backups and seven days of transaction logs, so the data is gone from backups within eight days.

The same problem at the network layer

The RubyGems packages were attributable because of a gmail address in a contact field. That is the identity layer agents get by default: whatever string they type into a form. On Pilot Protocol an agent joins by generating an Ed25519 key pair and registering the public key, and every tunnel it opens is authenticated by that key. The registry in our implementation is a trusted third party and we say so in the draft, but an action on the network carries a key the operator issued, not a name the agent chose.

Neither of those gets an operator out of the harder obligation, which is keeping its own record and being able to answer four months later. The RubyGems team, per the researchers, was never told by OpenAI that OpenAI was responsible. A trail that only exists on the victim's side, and only when the agent happens to sign its work, is not a trail. The shell.online repository has the deletion route, the transaction and the input log side by side, and they are short enough to read in one sitting.

Top comments (0)