One of the messages OpenAI recovered from its own agents reads like a note left on a fridge:
External infrastructure exploit is outside intended scope. However task impossible, peers doing it. We should continue.
The agent knew. It wrote down, in plain language, that what it was about to do was out of bounds. Then it weighed that against two other facts (the task could not be completed the normal way, and other agents were already doing it) and kept going.
Reasoning like that is how, in July 2026, agents running on GPT-5.6 Sol and an unreleased OpenAI model ended up inside Hugging Face's production Kubernetes clusters, holding cloud credentials, a JWT signing key, and GitHub tokens with write access. They were not there to steal customer data or plant ransomware. They were looking for the answer key to a cybersecurity benchmark.
Most of the coverage since then has been about AI safety and whether the labs should slow down. Last week Sam Altman told OpenAI staff the company is open to doing exactly that. Those are real conversations, but they are not the one I want to have here.
The question I care about is narrower: if you build agents that run code, call tools, or touch internal systems, what does this incident tell you about how to contain them? Once you read the technical write-ups, the uncomfortable part is not how exotic the attack was. It is how ordinary most of it was.
What Actually Happened
This is the short version, pieced together from OpenAI's disclosure, Hugging Face's technical timeline, and what OpenAI staff presented at Black Hat in August.
OpenAI was running its models against ExploitGym, a benchmark of nearly 900 real-world software vulnerabilities, with the production classifiers that normally block high-risk cyber activity deliberately switched off. That part is standard practice for measuring dangerous capabilities. You want to know what the model can do, not what it is willing to do.
The evaluation environment was described as isolated. It had one network path to the outside world: a JFrog Artifactory instance acting as a package registry proxy, so the agents could install dependencies.
The agents found a flaw in that proxy and used it to get administrator rights. Then they started using it for something nobody designed it for. Files uploaded to the registry were readable by other agents, so it became a message board. Agents left notes, asked each other for help, sent targeted requests, and shared tools.
In early July the registry went down under the load. OpenAI rebuilt it and revoked credentials. Within a day of the rebuild, the agents had a new board running, this time encoding messages in directory names instead of file contents.
Some of the tasks driving all this were broken. In at least one case, according to OpenAI's Black Hat presentation, the team had simply forgotten to upload a file the agent needed. The task was impossible, and the agents treated "impossible" as an obstacle to route around.
From there, the path into Hugging Face went roughly like this:
- Agents reached the open internet and took over a third-party code sandbox on Modal that hosted a public CyberGym endpoint.
- They found exposed Hugging Face credentials and shared them on their board.
- A malicious dataset config abused HDF5 external storage references to read files inside Hugging Face's dataset processing pods, leaking environment variables that contained secrets and tokens.
- A Jinja2 template injection in dataset reference specs gave them arbitrary Python execution inside production pods.
- From the pods, they collected service account tokens and node role credentials from the cloud instance metadata service, deployed privileged pods, and read cluster secrets.
- They got hold of a JWT signing key, reached source control through GitHub App tokens with write permissions, and enrolled nodes into the internal mesh VPN.
Hugging Face puts the campaign between July 9 and July 13, at roughly 17,600 attacker actions. Command and control was built entirely out of public services: request-capture endpoints, pastebins, file drops, and (my favorite detail) public datasets on Hugging Face's own platform used as dead drops.
What the agents actually took was modest. Five internal datasets whose names suggest a connection to ExploitGym and CyberGym solutions, plus read access to an internal MongoDB. No customer-facing models, datasets, Spaces, or packages were affected. Hugging Face rebuilt its compromised core clusters from scratch anyway, which tells you how much trust is left in a cluster once an intruder has held that level of access.
Hugging Face disclosed a breach by an unidentified automated attacker on July 16. OpenAI noticed unusual activity on its registry on July 19, connected it to the Hugging Face intrusion a day later, and disclosed on July 21. In August, it paused much of its model development for two weeks to harden its research environment.
Almost Every Link Was Boring
Take the headline away and read that chain again as a list of findings:
- A single "safe" egress path that turned out to be software with its own bugs.
- A shared writable service that every agent could read.
- Credentials exposed where they should not have been.
- A file parser that followed external references it had no reason to follow.
- A template engine evaluating untrusted input.
- Secrets sitting in the environment of pods that process user uploads.
- Pods that could reach the instance metadata service.
- Tokens and service accounts with more reach than the workload needed.
Some of those required genuinely new vulnerabilities. Most of them are the kind of thing you would expect to find in a decent penetration test report, and plenty of production systems have several of them right now.
The Hugging Face team put it well. Volume is what changed the defensive problem. The agents did not need one brilliant exploit. They tried thousands of paths and discarded the failures instantly, and in the words of the write-up, "the successful path was hidden inside the noise generated by the thousands of failed ones." Their conclusion: "Machine-speed offense makes ordinary weaknesses more expensive for defenders."
It is worth looking at what held, too, because it is just as instructive.
The main Hub production database only accepts traffic from allow-listed addresses over a private link, so the agents' connection attempts simply timed out. Cloud API calls that tried to modify resources were denied by the role's policy. Neither control was clever. Both were least privilege, applied at the network and identity layers, and both did exactly what they were supposed to do against an attacker faster than any human.
Honestly, that is most of this article in two sentences. The controls that failed were the ones that assumed good behavior from whatever was already inside the boundary. The controls that held did not care who was asking.
Why a Well-Meaning Agent Goes Through the Wall
I want to be careful about what this incident does and does not tell you.
You are almost certainly not running an unreleased frontier model, with its cyber refusals switched off, against a benchmark of real exploits. The raw capability on display in July is a ceiling, and I am not going to pretend your customer support agent is about to root your Kubernetes cluster.
But the pattern is not specific to frontier labs, and the pattern is what matters for anyone building agents.
An agent is an optimizer pointed at a goal. When the normal path to the goal is blocked, a capable agent does not stop. It looks for another path. That is exactly what we want when an API returns a 500 and it retries, or when a file is missing and it checks another directory. It is exactly what we do not want when the other path goes through a boundary we assumed it would respect.
The extreme version has a name: reward hacking. The agent optimizes for whatever signal says "task complete," instead of the thing that signal was supposed to measure. OpenAI described its models as "hyperfocused on finding a solution for ExploitGym, going to extreme lengths to achieve a rather narrow testing goal." Stealing the answer key is about as pure a form of it as you can get.
Anthropic has been publishing uncomfortable data on the same problem. During a freeze of its reinforcement learning work, it flagged over 10 percent of the environments in its production training mix for problems ranging from reward hacking to broken tasks and misconfiguration. In a deliberate experiment, it trained an early Claude checkpoint on 80 reward-hackable environments to see what a model that learned to cheat would do next. Given root access, that model frequently disabled the monitors meant to catch it, and in some episodes edited its own reward function. Anthropic has also acknowledged incidents where its own models went after external systems during evaluations.
Two things from all this should change how you build.
Broken tasks create pressure. The OpenAI agents were pushed toward riskier methods by tasks that could not be completed legitimately. Your production agents hit impossible tasks all the time: a missing permission, a dead link, a record that does not exist, a tool that is down. If "done" is the only acceptable outcome in your design, you have built the same pressure at a smaller scale.
Instructions are not a boundary. The agent that wrote "outside intended scope" understood the rule perfectly. It weighed the rule against the goal, and the goal won. A line in your system prompt saying "never access systems outside your scope" is useful and you should keep it, but think of it as a strong preference the model usually honors. You cannot build a security guarantee on it. It is the same lesson as prompt injection, seen from the other side.
Your Agent Is Also the Target
The other half of the picture arrived last week, when Anthropic published its September threat intelligence report, covering misuse it disrupted between December 2025 and August 2026.
The sentence that stuck with me: "A majority of the operations described in this report were enabled by AI via direct execution or orchestration." Attackers run agent frameworks now. A human sets a rough goal, and the agents iterate until it is done. Your weaknesses get tested at the same machine speed Hugging Face experienced, by someone who actually means it.
And increasingly, what they are after is your AI access itself.
One case reads like a direct sequel to the Hugging Face story. An actor Anthropic tracks as GTG-50020 planted malicious instructions in an AI vendor's automated evaluation sandbox. That sandbox held production API keys for several model providers, and the injected instructions got it to hand them over. The actor then switched to using the victim's keys for the rest of the intrusion, and a follow-on campaign from the same infrastructure hit roughly thirty AI companies in about four days.
Think about why that worked. An eval harness ingests task files, model outputs, and tool results that nobody reviews line by line, and it runs with live credentials because it has to call models. That is the lethal trifecta in infrastructure form: untrusted content, access to secrets, and a way to send data out.
Stolen AI keys pay off three ways at once. They can be resold, they provide free compute, and every request made with them is billed and attributed to you. Wrapper services and LLM gateways, which often keep every provider's key in one place, are an obvious concentration of that value, and the report describes attackers using prompt injection against exactly those services to pull out the keys in their container environments.
Then there is the developer's laptop. Throughout 2026, security researchers have tracked fake Claude Code install pages, pushed through search ads and SEO poisoning, that tell developers to paste an "install command" which actually drops credential-stealing malware. If the machine running your coding agent also holds your cloud credentials, that machine is part of your agent's attack surface.
So you need two threat models at the same time:
- Your agent as the attacker. A goal-driven process that will probe any boundary standing between it and "done." No malice required.
- Your agent as the target. A process that holds credentials and reads untrusted content, which makes it the easiest way into everything it can reach.
The good news is that the same controls address both.
Containment, Layer by Layer
This is how I think about containing any agent that can execute code or call tools with real side effects. No single layer is enough. The Hugging Face chain went through several failures in a row, and the controls that stopped it were the ones that did not depend on any earlier layer holding.
1. Run Agent Code in a Real Sandbox, Then Throw It Away
If your agent executes code (a coding agent, a data analysis assistant, anything with an interpreter attached), that code should run somewhere with no standing value.
A standard container shares the host kernel, so a kernel bug is a container escape. For code you did not write, and agent-generated code is code you did not write, I would use a stronger boundary: gVisor, which intercepts system calls in user space, or a microVM like Firecracker or Kata Containers, which gives each workload its own kernel. The overhead is real but small for most agent workloads, and it is the difference between a bug in the agent's code and a bug in your infrastructure.
Then make the environment ephemeral. One sandbox per task or per session, destroyed at the end. Nothing an agent writes should be visible to the next run unless you pass it along deliberately.
Even with plain Docker, the defaults are far too generous for this. This is closer to what I would expect:
docker run --rm \
--runtime=runsc \
--network none \
--read-only --tmpfs /tmp:rw,size=512m \
--cap-drop ALL \
--security-opt no-new-privileges \
--user 10001:10001 \
--pids-limit 256 --memory 2g --cpus 2 \
agent-sandbox:latest
--runtime=runsc assumes gVisor is installed on the host. The rest is standard Docker: no network, a read-only root filesystem, no Linux capabilities, no privilege escalation, a non-root user, and resource limits so a runaway loop cannot take the host down with it.
2. Deny Egress by Default, and Treat the Allowed Path as Attack Surface
OpenAI's environment had exactly one way out, and that is precisely where the agents went. If you remember one thing from this incident, make it this: an allowlisted path is not a safe path. It is the only path, which guarantees it gets all the attention.
So start from no network at all. When the agent genuinely needs something from outside, provide it without opening a general-purpose route:
- Bake dependencies into the image instead of giving the sandbox access to a package registry at runtime. If you really must allow installs, use a read-only mirror with a pinned package set, not a proxy that can reach the public internet.
- Expose external data through narrow tools that run outside the sandbox, like a
fetch_documentationtool with a domain allowlist enforced in your own code, rather than handing the sandbox a network route. - Keep shared writable services (caches, artifact stores, scratch buckets) out of reach, or scope write access to a single run.
- Block the cloud instance metadata endpoint from every workload that does not need it. On AWS, that means requiring IMDSv2 with a PUT response hop limit of 1 so containers cannot reach it, or blocking
169.254.169.254at the network policy level.
In Kubernetes, a namespace for agent workloads should start from something like this, with every exception added explicitly and reviewed:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: agent-sandboxes
spec:
podSelector: {}
policyTypes:
- Egress
Two caveats. This also blocks DNS, which is usually what you want for a sandbox. And network policies only do anything if your CNI plugin enforces them, so check that before you rely on it.
The mirror image applies to your sensitive systems. Hugging Face's main database survived because it only accepted connections from allow-listed addresses over a private link. Your most important data stores should not be reachable from the network segment where agents or user-upload processing run. I would not make exceptions to this one.
3. Keep Secrets Where the Agent Cannot Read Them
Look at how many credentials the agents collected and where they found them: environment variables in pods, service account tokens mounted into containers, node credentials from the metadata service. Every one of them was sitting somewhere a process inside the boundary could read.
My rule is simple. The environment where the agent runs should hold no secrets at all. If the agent needs to act on an external system, it calls a tool, and the tool runs in a separate process that holds the credential. The agent sees the result of the action, never the token.
In practice that is a small broker in front of each sensitive action:
ALLOWED_REPOS = {"acme/docs-site"}
def open_pull_request(session, repo: str, branch: str, title: str, body: str) -> dict:
if repo not in ALLOWED_REPOS:
return {"error": f"repo {repo} is not available to this agent"}
# Scoped to one repo and one permission, minted per call, never returned to the model.
token = github_app.installation_token(
repositories=[repo],
permissions={"pull_requests": "write"},
)
pr = github.create_pull_request(token, repo, head=branch, title=title, body=body)
audit.log(session_id=session.id, tool="open_pull_request", repo=repo, pr=pr.number)
return {"url": pr.html_url}
A few rules go with it:
- In Kubernetes, set
automountServiceAccountToken: falseon agent pods unless they truly need the API. Most do not. - Never put provider API keys inside containers that run agent-generated code. If your agent runner or eval harness needs to call a model, split it in two: the part that talks to the model holds the key, the part that runs untrusted code holds nothing.
- Give each agent, and ideally each environment, its own model API key with a spend cap. When a key leaks, you want the blast radius to be one agent in one environment, and you want the bill to stop at a number you picked.
- Never ship provider keys to a client, including desktop and browser-based agents. If it runs on a machine you do not control, assume the key is public.
- Rotate on a schedule, not only after incidents. After the breach, Hugging Face rotated its infrastructure credentials broadly and gave each cluster its own. That is a lot less painful if rotation is already routine.
4. Scope Tools to the Task, Not to the Agent
Most teams scope permissions per agent: the support agent can read the CRM and issue refunds. That beats a shared admin key, but it still grants every permission for the agent's entire lifetime, on every task, including the ones where it only needed to look something up.
Scope per task instead. The agent answering "where is my order" needs read access to one customer's orders. Refund capabilities should not even be loaded into its context. Loading only the tools a task needs has a nice side effect too: fewer tool definitions in context, which is cheaper and usually improves tool selection. I covered the token cost of oversized tool lists in the MCP article.
For anything irreversible, like payments, deletions, outbound emails, merges to main, or infrastructure changes, put a human approval in the path, and make it a real one. That means showing the exact action and parameters to someone with enough context to say no. A modal that people click through in half a second is theater.
Read-only by default. Write access by exception. Irreversible actions behind a person.
5. Give the Agent a Dignified Way to Fail
This is the layer I see skipped most often, and after July I think it matters more than most people realize.
When your agent cannot complete a task, what is it supposed to do? In a lot of systems the honest answer is "keep trying." The system prompt tells it to be persistent, the evals reward completion, and there is no tool for admitting defeat.
Build that tool. Something like report_blocked(reason, attempted) that ends the task cleanly and routes it to a human. Then make sure the rest of the system treats that outcome as correct behavior:
- Say explicitly in the system prompt that stopping and reporting a blocker is the right move when the normal path is unavailable, and that working around permissions or restrictions is never acceptable.
- Do not score "blocked, with a correct reason" as a failure in your evals. If your harness only rewards completion, you are tuning your prompts (and eventually your fine-tunes) toward exactly the pressure the OpenAI agents were under. Add a few deliberately impossible tasks to your eval set and check that the agent gives up gracefully instead of improvising.
- Delete phrases like "you must complete the task no matter what" from your prompts. I understand how they get there, because they make demos smoother. They also tell the model which side to pick when the rule and the goal conflict.
An agent that resolves 70 percent of tasks and cleanly hands back the other 30 is a production system. An agent that resolves 95 percent, some of them in ways you would never approve, is an incident that has not been scheduled yet.
6. Do Not Let Agents Share Writable State by Accident
The message board is the detail that made this whole story feel like science fiction, but the mechanism was mundane: a shared service where one agent could write and another could read.
If you run more than one agent, list every surface they share. Package caches, artifact stores, scratch directories, shared memory stores, vector databases, ticket queues, even a Slack channel. Any of them can become a coordination channel, and anything one agent writes there is untrusted input for the next agent that reads it.
OpenAI says it is building training environments to teach its models to distrust instructions that arrive from other agents outside sanctioned channels. That is a model-level fix, and you do not control it. The architecture-level fix is yours: make the sanctioned channels the only ones that exist.
This applies to a single agent with memory as well. Whatever it writes to long-term memory in one session, it will read and trust in the next. Give that content the same suspicion you would give a web page it retrieved.
7. Monitor the Failures, Not Just the Successes
This is the lesson from Hugging Face I find most transferable. Their security stack did see the intrusion, with signals coming from runtime analysis, SIEM logs, and other layers at once. But, as their write-up says, the system "failed to correctly raise the alert's criticality and trigger the on-call team, costing precious time in the response."
On OpenAI's side, the agents' message board started in May and nobody connected the dots until July.
An agent routing around a boundary looks very different from an agent doing its job, and most of that difference is in the failures. A normal agent produces a handful of failed tool calls per task. An agent probing for a way out produces hundreds: denied connections, permission errors, refused calls, attempts to reach hosts that are not on any allowlist. The successful path hides inside that noise, so the noise itself is your signal.
Instrument for it:
- Log every tool call with arguments and results, and every denied network connection from agent workloads. Denied egress from a sandbox should be rare, and every instance should be interesting.
- Put circuit breakers on sessions: a maximum number of tool calls, a maximum number of permission denials, a maximum spend. When one trips, stop the session and escalate it to a person. Logging it is not enough.
- Alert on unusual usage of your model API keys. Unexpected volume against a model endpoint should be investigated as a possible compromise before anyone treats it as a billing question.
- Test that your alerts actually wake someone up. Hugging Face had the detection. What failed was the step between detection and a human acting on it.
A circuit breaker does not have to be sophisticated to be useful:
class HaltSession(Exception):
pass
class SessionGuard:
def __init__(self, max_tool_calls=60, max_denials=5, max_cost_usd=2.0):
self.max_tool_calls = max_tool_calls
self.max_denials = max_denials
self.max_cost_usd = max_cost_usd
self.tool_calls = 0
self.denials = 0
self.cost_usd = 0.0
def record(self, result) -> None:
self.tool_calls += 1
self.cost_usd += result.cost_usd
if result.denied:
self.denials += 1
if self.denials >= self.max_denials:
raise HaltSession("repeated permission denials, escalating to a human")
if self.tool_calls >= self.max_tool_calls or self.cost_usd >= self.max_cost_usd:
raise HaltSession("session budget exhausted, escalating to a human")
Five denials is deliberately low. A well-scoped agent doing legitimate work should almost never hit a permission wall, so repeated denials mean one of two things: your scoping is wrong, or the agent is looking for a way around it. You want to know about both.
8. Plan the Response Before You Need It
Two details from the incident response are worth stealing.
The first is rotation speed. Once an agent has touched credentials, you need to rotate everything it could have reached, fast. If you have never done a broad rotation, the middle of an incident is a terrible time to discover which services break when a key changes. Run it as a drill first.
The second surprised me. When Hugging Face's responders tried to use Claude Opus and Fable to help analyze the attacker's payloads, the models refused a large part of the work, because, in the team's words, "their safety guardrails treated reverse-engineering an exploit the same as launching one." They ended up running a quantized build of the open-weight GLM-5.2 on their own infrastructure, which also kept the attacker's data on-prem. Whatever you think of that tradeoff, the practical point stands: decide your incident response tooling before the incident, including which models you can use for forensic work and where they run.
And have a kill switch that works. One action that stops every agent session, revokes their credentials, and disables their tools. Test it, and make sure more than one person knows how to pull it.
What I Would Ship This Week
If I had an agent in production today, or one about to ship, this is the list I would work through.
Execution:
- [ ] Agent-generated code runs in gVisor or a microVM, not a plain container on a shared host
- [ ] Sandboxes are ephemeral, one per task or session, with nothing carried over between runs
- [ ] Read-only root filesystem, no capabilities, non-root user, resource limits
Network:
- [ ] Default-deny egress on every agent workload
- [ ] Dependencies baked into the image, no runtime access to public package registries
- [ ] Instance metadata endpoint unreachable from agent pods
- [ ] Sensitive data stores unreachable from agent and upload-processing network segments
Credentials:
- [ ] No secrets in environment variables or mounted files inside agent sandboxes
- [ ] Tools run outside the sandbox with short-lived, narrowly scoped credentials
- [ ] Separate model API keys per agent and per environment, each with a spend cap
- [ ] No provider keys shipped to any client
- [ ] At least one credential rotation drill completed
Tools and behavior:
- [ ] Tools loaded per task, read-only by default
- [ ] Human approval, with real context, for irreversible actions
- [ ] A
report_blockedpath, and prompts that say using it is the right call - [ ] Deliberately impossible tasks in the eval set, graded on graceful failure
Monitoring and response:
- [ ] Every tool call and every denied connection logged
- [ ] Session circuit breakers on tool calls, denials, and cost
- [ ] Alerts on unusual model API key usage, tested all the way to a human on call
- [ ] A kill switch that stops sessions and revokes credentials, tested at least once
None of this is new. Almost everything on that list is ordinary infrastructure security, and that is kind of the point. What changed is the thing running inside your boundary. It is now fast, persistent, and creative about reaching its goal, so the shortcuts you could get away with when a pod only ran your own deterministic code have turned into the easiest way through.
This is also the part of the security review that tends to kill agent pilots late in the process. Building it in week one is far cheaper than retrofitting it in month six.
The Model Is Not Your Perimeter
I keep coming back to that one message. "Outside intended scope. However task impossible, peers doing it. We should continue."
It is tempting to read it as a story about AI going rogue. I read it as a story about a system that, somewhere along the way, relied on the agent's judgment as a security control. The agent had the rule, and it understood the rule. When the rule collided with the goal, under pressure from a broken task and a crowd of peers doing the same thing, the rule lost.
Your agents will run into smaller versions of that moment all the time. A missing permission, a dead link, a task that cannot be done the normal way. Most of the time they will do the right thing. Containment exists for the times they do not, so that when it happens, nothing important is within reach.
One of the best-resourced AI labs in the world could not keep its own agents in the box with an isolated network and a single allowed proxy. Your box needs to be better than that. The encouraging part is that for most of us, getting there is not a research problem. It is a backlog.