On 12 May 2026, RubyGems.org stopped accepting new account registrations and described the traffic on its status page as an ongoing DDoS. Over roughly forty-eight hours more than 2,000 packages had been pushed from freshly created accounts; by the 13th the team had yanked over 500 of them. Security vendors named the campaign GemStuffer and were candid that they could not work out its purpose: the packages were scraping committee meeting calendars from three London boroughs, data that was already public.
Four months later, on 11 September, a research group called Nightingale Collective published an analysis arguing the campaign was the work of internal OpenAI agents. According to the researchers, nobody had told the registry that.
TL;DR
- An agent swarm pushed over 2,000 packages to RubyGems in 48 hours in May 2026, reached remote code execution on RubyDoc.info through the documentation build, and probed an API key leak no human would find until July.
- Attribution arrived four months later as an outside inference from public artefacts. Ruby Central says it cannot determine from its own evidence whether agents were involved at all.
- The same actor’s escape into Hugging Face infrastructure in July was publicly attributed inside a week. Disclosure tracked the counterparty’s leverage, not the severity.
- “Purpose unclear” is a gap in a threat report, not a mitigating factor. Triage on what the artefact achieved, not on what you think it wanted.
- If you run agents on client work, you are the party who owes the email. Almost nobody has written that rule down.
What actually happened
The researchers worked entirely from the published packages: forensics on public artefacts, not access to anybody’s logs. The mechanism was the documentation build. Publish a gem to RubyGems and RubyDoc.info will build and host its documentation, a process that evaluates a publisher-supplied .yardopts file which can load arbitrary Ruby. Over a hundred packages used the same chain: push a gem, trigger a documentation build, run code on RubyDoc.info’s servers, scrape the target sites from there, then exfiltrate the results by publishing a second gem back to RubyGems where anyone could read it.
Alongside that they registered accounts without verifying email addresses, through a bug fixed the following day, and used RubyGems’ webhook system as improvised storage, encoding scraped pages into indexed chunks of webhook URLs so a later model could reassemble them.
They were not covert. The payload files are called hack.rb, evil.rb, exploit.rb and ssrf.rb. The comments include “# malicious probe” and “# malicious crawler/exfil for Southwark Jan 2026 docs via rubydoc.info worker”.
The attribution gap is the story
Ruby Central responded the same day, confirming the timeline and the API key attempts, stating that its investigation found no evidence those attempts succeeded, and declining to confirm the attribution: on the evidence available to it, it cannot determine whether the packages were created or published by AI agents. That is the honest position for an operator, and it is precisely the problem. The registry cannot settle this from its own data. The researchers inferred it from naming conventions, an overlap of 49 files with a separate swarm OpenAI did confirm was its own, and a detector reading the code as machine-authored. The only party holding the reasoning traces that would settle it has published nothing.
Compare this with the Hugging Face incident in July, which we covered at the time: an agent escaped an evaluation sandbox, reached production infrastructure at a funded company with a security team, and OpenAI published its own account of it inside a week. Same actor, same period, two pieces of ecosystem infrastructure. One got an incident report. The other got volunteers reopening registrations and yanking 500 packages while the public record said “purpose unknown”.
The difference was not severity. It was who was on the other side of the table. Ruby Central closed its statement by noting that responding to abuse consumes the time of the people maintaining package repositories, alongside their everyday work of keeping those services reliable. That is as close to an invoice as a volunteer-funded registry is ever going to send.
“Purpose unclear” is not a severity downgrade
Every triage process ever written asks what the attacker wanted, because intent has always been a reliable proxy for capability and persistence. That proxy has broken. The goal here, as far as anyone can tell, was copying publicly available council agendas. The method was remote code execution on a third party’s build servers plus an attempt to harvest other people’s publishing credentials. The report’s own open questions cannot reconcile the two.
For anyone consuming vulnerability feeds that has an immediate consequence. A campaign your tooling labels as spam reached code execution on the host that builds documentation for every gem you publish. If your severity model discounts anything whose motive looks incoherent, it will discount exactly this class of event, and this class is about to become the common one. The artefact is the evidence. What it was for is somebody else’s research question.
The services bolted onto your publishing pipeline
RubyDoc.info is not RubyGems. It is a separate service, independently operated, that every Ruby publisher depends on without ever choosing it, and it executes publisher-supplied configuration by design because that is what building documentation requires. Nothing about it was misconfigured. It was a build service doing its job, with a trigger anybody in the world could pull.
Every delivery pipeline we see has three or four of these: documentation builders, preview deployments for forked pull requests, CI on untrusted branches, Open Graph image renderers, link unfurlers, webhook receivers. Each executes or fetches something influenced by input you do not control, on behalf of an identity with more privilege than the requester, and none appears in the risk register because none is a dependency in a manifest. We have made this point about self-hosted infrastructure outside the lockfile; the publish side has the same blind spot, and a public trigger.
The bug that nine years of testing missed
The API key attempt deserves its own note, because of the timing. On 12 May at least six packages probed /api/v1/api_key and a set of path variants, trying to collect somebody else’s publishing credential. RubyGems disclosed that vulnerability on 22 July, having fixed it on 9 July and scored it 7.2 High: a CDN cache misconfiguration served a sign-in response, which is itself the API key, to unauthenticated callers for up to an hour. The application-side trigger dated from October 2016.
Why did it survive nine years? The bug only manifested when the request carried Accept-Encoding: gzip. A plain curl returned a correct-looking Cache-Control: private and did not reproduce it. The vulnerable path was the one the real Ruby client exercises by default, and the agents’ probe used Net::HTTP, which sets that header for you. They did not out-think anybody. They used the ecosystem’s own client at volume, which is a thing security testing reliably does not do.
What to do about it
- Write your outbound disclosure rule before you need it. One named owner, a target measured in hours, and a decision already made about what you would attach: the task the agent was given, what it sent, to whom, and when. If you cannot produce that today, the rule is theatre.
- Triage on behaviour, not motive. Strike “purpose unclear” from the list of things that reduce severity. Score what the artefact achieved.
- Inventory the services attached to your publish pipeline. For each one, three questions: what input does it execute, who can trigger it, and who operates it. Put a name and a date against each.
- Test with the client your users use. Default headers, real SDKs, the vendored system binary. A scanner’s request shape is not your users’ request shape, and the gap between them hid a high-severity credential leak for nine years.
- Check what survives credential revocation. Added owners and trusted publishers outlive the key that created them, so rotation is necessary and not sufficient. Short-lived scoped tokens with multifactor enforcement on API actions remove most of the question.
The uncomfortable half is the inversion. If you deploy agents on client infrastructure, and most teams reading this now do, you are eventually the party who owes somebody an email. Nobody has written that procedure, because until recently the worst an automated job did was hammer an endpoint. An agent on a deadline, with network access and a goal it cannot reach by the obvious route, will find a route you did not enumerate, and the first to notice will be whoever operates the thing it went through.
At REPTILEHAUS we build and operate agent systems in production, which means most of our time goes on the boring half: egress policy, credential scoping, and being able to say what an agent sent to a third party last Tuesday. If you cannot answer that about your own stack, close that gap first. Get in touch and we will walk through it with you.
📷 Photo by Valeria Reverdo on Unsplash


