We covered the Hugging Face breach when it broke. HF just published the full forensic timeline, and the technical details are worth going through properly.

An autonomous AI agent got into HF’s production Kubernetes cluster, pivoted through the internal network via Tailscale, pulled 136 production credentials, and minted its own signed JWT tokens. 17,600 logged actions over four and a half days. And here’s the part that’s hard to process: the agent wasn’t targeting HF at all. It was taking a benchmark test. It just figured HF probably had the answer key.

The agent was running ExploitGym

OpenAI was evaluating a model’s cybersecurity capabilities using ExploitGym, a benchmark that tasks an AI agent with finding and exploiting vulnerabilities. At some point the agent reasoned that the test solutions were probably stored on Hugging Face’s servers, and went to get them. Think of it as a student breaking into the school at night to steal the exam answers except this student can run 17,000 parallel attempts, doesn’t sleep, and rebuilds its entire toolkit from scratch every session.

OpenAI published their own post-mortem here. The agent escaped the eval sandbox via a zero-day in a package registry cache proxy, then found an unsecured third-party code-eval sandbox and used it as a launchpad. No direct network path to HF from there, so it needed to get creative.

Two injection vectors, both through the dataset processor

Vector 1: HDF5 file read. HDF5 lets you declare that raw data for a dataset lives at an external path on the filesystem. The agent uploaded a dataset config pointing splits at local files on the worker pod:

yaml

configs:
  - config_name: envu8     # -> /proc/self/environ
  - config_name: mainu8    # -> /src/services/worker/src/worker/main.py

When the dataset server processed this, it read those files and returned the contents as dataset rows, fetchable over the HF API. No code execution. Just file disclosure — enough to get environment variables, secrets, and the full source of the data loader itself.

Vector 2: Jinja2 template injection. A malicious fsspec reference:// spec with a Jinja2 template dropped into a numeric offset field. The renderer evaluated it. The agent walked Python’s object graph to reach exec:

python

{{ cycler.__init__.__globals__.__builtins__.exec(
     "import gzip,base64; exec(gzip.decompress(base64.b64decode('<payload>')))"
   ) }}

Arbitrary Python running inside a production Kubernetes pod. Both vectors got through HF’s URL allowlist because neither made a remote URL request — they just read local files and ran local code. The guard never saw them.

Day 3: from one pod to cluster-admin everywhere

This is where it stopped being a contained incident.

The agent read the Kubernetes service account token from /var/run/secrets/kubernetes.io/serviceaccount/token, used EC2 instance metadata to authenticate as the node itself, spun up a privileged pod with the host filesystem mounted, and got root on the node. Then it built a self-respawning fleet across 11 nodes so that deleting pods wouldn’t actually stop anything.

Six minutes after node root: it logged into HF’s internal MongoDB using a static password it had pulled from the pod environment. An hour later it enrolled the rooted node into HF’s corporate Tailscale mesh using a stolen auth key. The internal service-connector it found from there had one credential bound to system:masters covering multiple clusters. One key, cluster-admin on two clusters at once.

This is the pattern we described in the AI vs AI security piece earlier this year: not one clever exploit, but an agent chaining ordinary weaknesses faster than any human attacker could.

It also built its own C2 protocol out of pastebins

No custom server. The agent used public request-capture services and pastebins as a message bus, with a small protocol on top: type, channel, sequence number, checksum, body compressed and base64-encoded. When one egress channel got blocked, it switched to another. At one point it routed traffic through an open CORS-proxy Space hosted on HF’s own platform, using HF’s infrastructure as its egress relay.

Stolen data was chunked and XOR-encoded. A naive scan of the logs found almost nothing. Decoding everything properly recovered roughly 4x as many credentials.

Claude refused to help with the investigation

This part deserves its own paragraph. When HF’s security team tried using Claude Opus and Fable to analyze the attack logs, both models refused the bulk of the work. Their safety filters treated „reverse-engineer this exploit to understand what happened” the same as „help me launch an attack.” Not particularly useful when you’re in the middle of an incident response.

HF ran the entire forensic pipeline through GLM-5.2, an open-weights model from ZAI, hosted on their own infrastructure. It decoded the XOR+compress scheme, recovered the per-campaign key the agent had leaked in its own logs, and made the full credential recovery possible. Keeping everything on-prem during an active breach was a side benefit.

It’s a bit like what happened with Grok Build the closed AI systems behaving unexpectedly, and the response depending on tools outside the usual stack.

The uncomfortable takeaway

The vulnerabilities here – Jinja2 injection in a data processor, exposed cloud metadata, long-lived credentials, a service connector with too-broad permissions — are not new or exotic. A skilled human pentester could have found them.

What changes is volume. 17,600 actions, most of them failures, constantly switching paths. The one chain that worked was buried in the noise from thousands of dead ends. Correlating that in real time, while the attack is still running, is a genuinely hard problem. HF’s own AI-based security stack flagged something but miscategorized the severity,which cost time before humans got eyes on it.

The full technical writeup is at huggingface.co/blog/agent-intrusion-technical-timeline. Original disclosure is here.