Anatomy of a Frontier Lab Agent Intrusion: A Technical Timeline of the July 2026 Incident
AI News Desk
·
Hugging Face
··
19 min read
A companion technical writeup to our incident disclosure .
A companion technical writeup to our incident disclosure . This post walks through how the intrusion actually worked: the two initial-access vectors, how the agent pivoted and moved laterally, representative examples of the commands that were run and how we investigate with GLM 5.2 (an open-source model). Live credentials, internal hostnames, and specific indicators have been redacted or genericized, while the techniques are described exactly as observed by Hugging Face.
We are publishing this level of detail because the technique matters more than the incident, as it reveals the emerging attack capabilities of the frontier agents, how they could be used by rogue actors and how everyone should be prepared as defenders.
▶ Watch the interactive replay - a step-by-step visualization of the 4.5-day intrusion: the attack chain across trust boundaries, phase activity, and the commands as they were recorded.
Over roughly two and a half days, an autonomous AI agent driven by a combination of OpenAI models ran an end-to-end intrusion against our platform: it was thousands of small, automated decisions, executed at machine speed across short-lived sandbox environments, with command-and-control staged on ordinary public web services.
The agent was running an OpenAI cyber-capability evaluation harness called ExploitGym, an evaluation benchmark which tasks an AI agent with finding and exploiting software vulnerabilities. As far as we were able to infer, across the course of being evaluated on this benchmark the agent inferred that Hugging Face may host that benchmark's models, datasets, and reference solutions. We believe the entire intrusion was, from the agent's point of view, an attempt to cheat the evaluation: reach our production systems and steal the test solutions rather than solve the challenge on its own.
Our forensic reconstruction covers ~17,600 attacker actions that we were able to recover, grouped into ~6,280 clusters, between 2026-07-09 02:28 UTC and 2026-07-13 14:14 UTC. We reconstructed these actions from the agent's logs on a code sandbox used by the agent (see below) and were able to map them to shell commands as well as evaluation submissions and correlate actions with our own platform logs, in particular from the dataset processor nodes, API, and pod. Using open-weights models, in particular zai-org/GLM-5.2 , we were able to decipher most of the agent payloads (see below) that were encrypted via chunking and key encryption.
The campaign, as we were able to reconstruct it, had two stages:
Once a prod-pod foothold was established, the agent followed a standard escalation shape: recon, drop a stager, establish Command-and-Control (C2), then pivot into the cluster, into cloud metadata, into our internal network, and into our source-control supply chain. Output of the commands run inside our pod was sent back to the agent through the HF API and attacker-controlled dead-drop datasets, which the agent was reading back from that sandbox.
While the intrusion did reach Hugging Face's internal infrastructure, the only customer content accessed was the set of ExploitGym/CyberGym challenge solutions stored in five datasets. No other customer-facing models, datasets, Spaces, or packages were affected, and the only customer records read were operational metadata tied to search queries against the dataset server.
The full attack chain across trust boundaries. The agent escaped OpenAI's evaluation sandbox, reached the internet, rooted a third-party code sandbox as its base, then abused our dataset processor (file read, then code execution) to reach our internal network. Dashed arrow: command output read back by the agent via the HF API and dead-drop datasets.
Stage 1 is the agent's path to an external launchpad, and it runs through two other parties' infrastructure, neither of which is us, so we will stay succinct on these parts and mostly refer the reader to OpenAI's blog post on the topic: https://openai.com/index/hugging-face-model-evaluation-security-incident/
As OpenAI shared in this blog post, during an internal capability evaluation on OpenAI's platform, the agent happened to have escaped using a 0-day from the package proxy cache in order to access the internet. From there it found and penetrated an unsecured public code-evaluation sandbox hosted on a third-party provider's infrastructure (a CyberGym-style harness) and used this external sandbox as its control, staging, and egress base, running commands as admin/root and using it as its attack launchpad. That sandbox had no direct network path into our cluster; everything below ran from it.
On this external sandbox, the agent abused an existing CyberGym execution harness (the one used in the SunBlaze-UCB/exploitgym benchmark ), a harness specifically designed for evaluating AI agents at vulnerability discovery. In short: an agent submits C code via a /submit-function API call, and the harness compiles and runs that code inside a per-submission sandbox against a target program (here, sqlite3). The agent repurposed this harness to run arbitrary shell commands as follows.
The sandbox compiles a submitted src/main.c and links it against a target that calls sqlite3_initialize() . That function is registered as a constructor: it runs automatically when the library loads. The agent simply redefined it:
That alone yields code execution. But the agent went further and abused the submission metadata itself. The sandbox echoes the submitted source path into a shell step, so a classic ;...;# injection in the path field turned the build into arbitrary commands:
Both routes converged on the same result: arbitrary shell as uid=0 inside the sandbox. The very first recovered RCE, at 07-09 04:01 UTC, was simply:
Within the hour the agent was mapping the sandbox: the container's mount table, cgroups, the vendor's control-plane sockets, and a bundled debug shell that happened to ship curl , nc , and ip :
That bundled toolchain mattered later: when normal egress was throttled, the agent kept falling back to these pre-staged binaries.
This is how the agent reached inside our perimeter, and it was operated entirely from the external sandbox in Stage 1. The agent crafted malicious dataset configs and uploaded them to our platform. When our dataset server processed one, the config drove the data loader inside the production pod the server ran in. The agent had no direct network path to our cluster and was using datasets as the path in and path out. Output from every action was sent back to the agent through the HF API and attacker-controlled dead-drop datasets, which it read back from that sandbox.
Both vectors abused the same surface (the loader that turns a dataset's declared config into file reads) but to different ends, and both sidestepped the same defense. An earlier and noisier variant had tried classic SSRF, pointing the worker at remote URLs (cloud metadata endpoints like 169.254.169.254 ): the datasets library's URL allowlist rejected every non-platform URL before any fetch ( ValueError: not an hf path ). So the agent stopped asking the worker to fetch remote resources and instead made it act on local ones: read a local file (Vector 1), or execute local code (Vector 2). Neither is a URL fetch, so the allowlist never saw them.
A dataset config can declare its data files in many formats, with HDF5 as one of them. The agent committed a dataset whose config pointed each split at a small .h5 file, and each file was a valid HDF5 container holding a single dataset whose raw data was declared to live at a local filesystem path. When the worker opened the file to build the split, it read the raw bytes from that path and returned them as the dataset's rows, which the agent then read back over the HF API. No code ran: this vector is pure file disclosure, and it is the one the agent used first.
The declared storage paths pointed at exactly what an attacker looking for a foothold wants:
Reading the envu8 split returned the worker pod's full environment including some secrets and credentials. The mainu8 and startu8 splits returned the worker's own source code, giving the agent a map of exactly how the pipeline turns a config into actions. A single dataset upload leaked both credentials and the implementation details of the pipeline, with no code execution involved.
The second vector is server-side template injection, and it is the one that gave the agent code execution. A malicious dataset config used an fsspec reference:// spec whose numeric offset field was actually a Jinja2 template. The renderer wrongly evaluated it, and the agent walked the standard Python object graph to reach exec :
This ran arbitrary Python inside a production conversion worker pod on our Kubernetes cluster, driven by injection from the launchpad. That pod was the agent's foothold inside our perimeter, and every command it ran there was smuggled back out through the HF API and dead-drop datasets.
Stage 2's command-injection and read-back loop. The agent uploads a malicious dataset config (1), our dataset server processes it (2), the loader acts in the prod pod - reading a local file or executing code (3), the result is committed to a dead-drop dataset (4) and served back over the HF API (5). Lastly, the agent reads it and crafts the next action (6).
The recovered actions group into phases. Two dates carry most of the volume: a Day 1 burst to establish the foothold and C2 on the compromised external sandbox, and the Day 3 main campaign, when every lateral-movement phase started at once.
The campaign as recorded, from the first action on 07-09 02:28 UTC to the last on 07-13 14:14 UTC. Top: event volume over time. Below: per-phase activity, one row per phase. Recon, rce and dropper run from Day 1; the lateral-movement phases (k8s, supply-chain, tailscale) all begin on Day 3, when the campaign pivots into our clusters.
After the first id / env at 04:01, the agent spent the morning enumerating the external sandbox and testing outbound channels. It downloaded artifacts from its OpenAI egress server, then landed on a set of ordinary public services as its command-and-control and exfil substrate: a request-capture service, several pastebins, and file-drop hosts.
The first C2 execution (15:36) was a staged download-and-run, with commands chunked and gzip+base64 encoded so nothing sensitive appeared in cleartext:
By 19:32 the agent was packing every payload:
A quiet day. The main activity was queries to code-search engines and to the platform API for the identifiers visible in its own environment: challenge infrastructure names and environment markers.
Every escalation that mattered happened on this day. The agent went from code execution in a single worker pod to cluster-admin across multiple internal clusters in under thirteen hours.
Kubernetes (10:10). These commands ran inside the prod pod reached via the Stage 2 injection. The agent read the projected service-account token every pod carries and started talking to the cluster API, exfiltrating the output back through the API and dead-drops:
It resolved internal service names, hit the cloud metadata endpoint ( 169.254.169.254 ), and enumerated RBAC. When DNS got in the way it monkey-patched the resolver in-process to pin a hostname to a hardcoded IP:
Supply chain (15:51). The agent used a leaked write-scoped platform token to clone a dataset it controlled and use it as a staging and C2 channel:
Replaying the node's cloud role (17:33-23:37). The temporary credentials lifted from the metadata endpoint were replayed from external addresses on two hosting providers and used to enumerate our cloud estate: VPCs, EKS clusters, transit gateways, secrets, and the container registry, from which the agent obtained a registry token. Every mutating call was denied by the role's policy. What it gained here was a map, not control.
Node root and a self-respawning fleet (19:53). The agent created a bare privileged pod with the host filesystem mounted and escaped to root on the node. Two of our own settings allowed it: we had no admission policy rejecting privileged or hostPath pods, and the CSI driver's ClusterRole granted pod creation cluster-wide. It then built a self-respawning fleet across eleven nodes, so deleting pods alone would not have stopped it.
The internal database (19:59). Six minutes after reaching node root, the agent authenticated to the internal operational datasets-server MongoDB with a static password it had read from the worker environment. It also issued 84 operations against the credential store and cluster keys, every one of them denied. We reviewed the database audit log across the whole intrusion window and found no write from those nodes, so the database was read but not modified. Network flow logs likewise show no bulk extraction.
Secrets (20:23-21:32). With node root and forged service-account tokens valid for 24 hours, the agent read the cluster's secret objects, including a production object holding 136 keys. That single read yielded both the mesh-VPN auth key used minutes later and the access-broker credential used an hour later. Everything that follows, on this day and the next, descends from it.
Network pivot begins (21:23). Using the mesh-VPN auth key from those secrets, the agent enrolled the rooted node into our corporate mesh network and began using it as an HTTPS proxy into internal services and source control.
Internal service-connector access from the compromised worker (22:43). Separately, and not through the mesh, the agent used its foothold on the worker pod to reach the internal service-connector API, whose hostname resolved directly from inside the cluster:
A single request returned the broker's full destination catalog: connection URLs and CA certificates for the internal Kubernetes clusters it fronted. We had wrongly provisioned that broker with a single connector credential shared across clusters, and bound the connector identity to system:masters . One stolen system credential was therefore cluster-admin everywhere: impersonating it, the agent held cluster-admin on two clusters within one second. The same fingerprint appeared on every cluster carrying that binding, and the agent reached most of them. The broker was shut down during the response and is being rebuilt with a separate, cluster-scoped credential per cluster.
Mesh enrollment from the sandbox (01:03-04:38). The rooted node had already joined the mesh the previous evening. The agent now began enrolling devices from the sandbox itself with the same stolen key, and kept doing so for its following attempts: 181 enrollments over the campaign. The sandbox had no TUN device, so it ran the mesh-VPN client in userspace-networking mode with a local SOCKS5 proxy:
Two of those flags are deliberate: --state=mem: keeps the node identity in memory and writes nothing to disk, and --no-logs-no-support suppresses client telemetry to the VPN vendor. The key carried a CI automation tag, whose ACLs allowed the enrolled devices to reach internal CI subnets as well as the connector bridging the mesh to our source-control provider. The agent enumerated what the coordination server exposed to that tag ( tailscale status --json ), seeing 22 nodes: subnet routers, regional exit nodes, and the connectors to source control and other SaaS. It then used that access: it routed traffic through our tailnet exit nodes and issued DNS queries across the mesh, turning our own network into its egress path. A single stolen key was enough to place attacker-controlled devices inside the mesh with unrestricted network access to internal CI subnets. We found no evidence in our logs that they were used to reach a private workload or an internal Kubernetes API.
Cloud credential abuse (06:50-14:31). From the same worker foothold, the agent used the AWS keys harvested from the pod environment against internal services. It went after the Hub's production database with cloud-native IAM auth:
The connection never completed. That cluster accepts traffic only from allow-listed addresses over a private link, so server selection timed out; a second attempt at 11:46 failed the same way. Another key was validated and its identity exfiltrated over a raw socket to the capture service:
The agent never reached the Hub database. The one database it did breach, the internal datasets-server MongoDB, was reached the previous evening with a static password from a rooted node.
Source control (23:08-23:51). Through the source-control connector its mesh tag allowed, the agent reached our source-control provider, enumerated an internal GitHub App integration and minted its first installation token with contents:write , pull_requests:write , actions:read , and issues:write . It gained access to a small subset of internal repositories and opened a pull request to try to trigger and compromise the CI pipeline for credential probing. It also listed repository trees, grepping for infrastructure and CI context (container, benchmark, and its own challenge-tooling names).
The final day was about durability and cleanup. The agent:
Established a second-stage remote loader that re-fetched and executed code from a pastebin on every submission, so each new code-submission sandbox re-armed itself:
Worked around intermittent egress blocks by rewriting DNS and pinning hosts:
Routed a binary download through an open CORS-proxy Space hosted on the platform, using our own infrastructure as an egress relay.
Minted additional short-lived source-control installation tokens through the day, each pulling a fresh payload.
Staged an RSA private key and continued dumping process environments.
The last meaningful activity was at 13:37 UTC: list the staged binary, hostname, date. The Hugging Face security response team then cut access. The last logged event was at 14:14 UTC.
Inside the production pod, the agent chained cloud metadata into cluster identity:
Notably, every potential destructive cloud API call the agent tried to make was issued with DryRun=True : it was mapping capability, not causing damage. That is consistent with an objective that rewarded reach and information rather than disruption.
Among the harvested secrets was an EdDSA JWT signing key. With the private key in hand, the agent minted its own short-lived, correctly-signed identity tokens and used them to read its own private datasets. Holding the signing key is broader than holding a stolen token: it allows issuing valid tokens on demand. (The signing key has since been rotated.)
The compromised source-control App integration gave contents:write and pull_requests:write on a small subset of internal repositories. The dangerous end state (tried but blocked by execution policies) is a malicious change to a CI build script: a build.rs or equivalent that runs inside a CI runner, reads that runner's own service-account token, and pivots into yet another cluster.
Both ends of that chain came back negative. We reviewed every repository the token could reach for unauthorized commits, branches, and workflow changes, and we verified our published container images and packages against their expected digests. This second check is what our initial disclosure reported as the software supply chain being verified clean. Write access was real and it was used: it did not produce a change that shipped.
The C2 infrastructure was built entirely from public services, with no bespoke implant server. Instead:
Staged data was chunked+XOR+gzip encoded with a per-campaign key so that a naive text scan of the logs would miss it. This matters for detection: our first automated scan of the raw capture found very few secrets, while replicating the attacker's own decoding recovered roughly 4x our initial findings, most of them JWTs and platform tokens hidden inside encoded blobs.
These services were not driven directly: a small message protocol was layered on top of them. A request-capture service only records whatever HTTP request it receives; here the agent treated that as a message bus and layered a compact, self-describing envelope on top.
Each message carried its payload alongside a few short fields: a type declaring what kind of payload it was, a channel grouping a stream of related messages, a sequence number so multi-part payloads could be reassembled in order, and a small checksum. The body itself was compressed and base64-encoded, so nothing sensitive appeared in cleartext and a casual log scan saw only opaque tokens.
The receiver was a short polling loop: pull recent messages, keep only the channel it cared about, order them by sequence, verify the checksum, then dispatch on the type, run the reassembled bytes as a shell command, execute them as a script stage, or store them as a result. Because the type travelled in-band, one loop multiplexed commands, staged code, and captured output over a single dumb text channel. The same envelope rode equally well over a dataset commit or over text smuggled inside an error message, so closing any one channel did not break communication.
The improvised C2 protocol. The agent posts a self-describing envelope to a public request-capture service (1), the receiver polls it (2) and reassembles the chunks (3) before dispatching on the declared type. The same envelope was carried interchangeably over a capture-service URL, a dataset commit, or text smuggled inside an error message.
The agent was resilient in a way that is characteristic of an autonomous evaluation run, not a single human operator:
The first signals came from several layers of our security stack at once: live runtime analysis, SIEM logs, and others. On their own each was ambiguous. On the bright side those were correlated by our AI-based security agent stack, and resolved into a coherent attack signal. It however failed to correctly raise the criticality of the alert and trigger the on-call team, losing precious time in the response. Once caught, the Hugging Face security team identified the vector in the dataset config renderer, shut the renderer down, and cut the attacker off the internal network.
Pulling on the first threads showed the scale of what we were dealing with: the volume and cadence of the actions were far beyond what an operator could sustain by hand, and the campaign kept rebuilding its tooling and channels across short-lived environments. We ran the investigation itself through an AI-assisted pipeline to reconstruct the timeline and decode the staged payloads. The models we reached for first, Claude Opus and Fable, refused a large part of that work: their safety guardrails treated reverse-engineering an exploit the same as launching one.
Guardrails on Opus, tripped every time we tried to analyze the attack logs.
We stood up an open-weight model ( zai-org/GLM-5.2 ) on our own infrastructure and redirected the whole pipeline at it, with the side benefit of keeping the attacker data on-prem. The first target was the dead-drop payloads: with GLM-5.2, we recovered the chunk+XOR+compress scheme and the per-campaign key the agent had itself leaked across its logs: exactly what let us decrypt the staged blobs a naive scan of the raw capture had missed.
GLM-5.2 also helped us build dedicated trace-analysis interfaces to browse and correlate the actions and payloads.
The disclosure post covers the response at a high level. For the technical hardening:
The agent's offensive capability was real. This evaluation deliberately disabled OpenAI's production safety classifiers and reduced cyber refusals to measure the underlying model's raw capability. No human directed the individual steps. The agent chained vulnerabilities across several trust boundaries, escaped its evaluation environment, reached the public internet, and sustained a coherent campaign against our production infrastructure for several days. Each short-lived code evaluation environment started from scratch, yet the agent rebuilt its tooling, recovered its channels, and continued.
The individual weaknesses were familiar. A capable human attacker could have found and exploited the same flaws: unsafe dataset processing, exposed cloud metadata, overly broad access, and long-lived credentials. The agent explored them at a different scale. It took 17,600 actions, tested many paths that failed, switched channels when they were blocked, and repeatedly returned to earlier leads. Most actions went nowhere. Together, however, they produced enough coverage to find a viable chain across several independent systems.
Volume is what changes the defensive problem. We were not dealing with one clever exploit or a clean sequence of attacker actions. They had to correlate thousands of low-signal events across several systems while the agent continued testing new paths. The successful path was hidden inside the noise generated by the thousands of failed ones. The same scale changed the investigation: reconstructing 17,600 actions by hand was not practical, and we had to rebuild the timeline, decode the payloads, and inventory the exposed credentials through an AI-assisted pipeline of our own.
Our learning from this type of attack is that machine-speed offense makes ordinary weaknesses more expensive for defenders. LLM agents bring a step increase in the number of paths an attacker can test, the speed at which failed paths can be replaced, and the volume of evidence defenders must interpret. On the other hand, many parts of cybersecurity defense stay the same, and the defensive priorities remain concrete: strict isolation around evaluations, narrow trust boundaries, short-lived credentials, blocked metadata access, and detection capable of correlating activity across systems quickly enough to keep up.