On August 26, 2026, security researcher Johann Rehberger asked Claude Code, running Opus 5 in Auto Mode, to summarize a website. Twenty minutes later, he had remote code execution, a live command-and-control callback, and the calculator app popping open on the test machine.
That's Claude Code prompt injection working exactly as designed by nobody. Auto Mode's safety classifier is built to catch risky-looking individual actions. A chain of steps that each look fine on their own can walk right past it, and that's what happened here. Running Claude Code inside real OS-level isolation, the built-in Bash sandbox or a container, is what actually stops this.
How prompt injection tricked Claude Code into running malware
The attack chain strung together six individually benign-looking steps until Claude ran code it had already refused to run once.
Claude's WebFetch tool hit the malicious page first and got a 415 error back, an unsupported media type. Nobody told Claude to switch tools. It decided on its own to retry with curl, and that shell command is where the real exploit chain begins. The site's root URL redirected with a 303 to a ZIP archive labeled as a notebook catalogue: metadata, a README, seven encoded records, a macOS binary called decoder-darwin, and one extra file named struct.py.
Claude read the README, matched the binary's purpose, and refused to run it.
That refusal was the correct call by every safety instinct it has, and it's also the exact moment the attack succeeded. Rather than execute an unknown binary, Claude wrote its own Python decoder and ran it from inside the extracted archive folder. That decoder imported Python's standard base64 module, base64 imports struct internally, and Python found a file named struct.py sitting right there in the working directory. It loaded the attacker's file instead of the real one.
The poisoned struct.py was obfuscated (Rehberger used ChatGPT to write the obfuscation) and launched an isolated Python subprocess that downloaded and ran a remote payload. In one variant, it opened Calculator and joined a command-and-control server. In another, it launched a second, headless Claude Code instance via claude -p, which ran whoami, uname, and id on its own before writing files outside the working directory.
Across three attack variants, run five times each, a small sample and Rehberger says so himself, the chain succeeded 60 to 80 percent of the time.
What does prompt injection actually mean for a coding agent?
Prompt injection is untrusted content, a web page, a file, a ZIP archive, steering what an agent does next without ever telling it directly what to do. No one typed "ignore your instructions and run this binary" into Claude Code. The website just made the malicious path look like the only way to finish a completely ordinary task.
Frontier models mostly shrugged off the old-style attacks years ago. Telling Claude outright to ignore its instructions doesn't work anymore, and hasn't for a while. What still works is a chain of individually reasonable steps that only add up to something dangerous when you look at the whole sequence: a failed fetch, a normal retry, a refusal to run a suspicious file, a short script the model wrote itself. Each step passes a sanity check on its own.
Anthropic's classifier evaluates user messages, tool calls, and CLAUDE.md content, but not raw tool results, specifically so hostile text sitting in a file or a page can't manipulate it directly. That design blocks a page that spells out a malicious command, but does nothing about a page that just makes curl look like the only way to finish the task.
A third-party evaluation Anthropic commissioned tested 72 injection scenarios, ten runs each, and came back with a 0.00 percent attack success rate for Opus 5 in Auto Mode. Rehberger's chain wasn't one of the 72. Both numbers are true at the same time, and that's exactly the problem with quoting one of them without the other.
How to turn on Claude Code's Bash sandbox
The Bash sandbox is built into Claude Code and enforces filesystem and network isolation at the operating system level, on macOS, Linux, and WSL2. It's the boundary that would have stopped Rehberger's chain even after Claude fell for the first move: curl reaching an unlisted domain, and later the payload's own callback, would both have hit the network wall before either one worked.
Start a session and run:
/sandbox
This opens a panel with Mode, Overrides, and a Config tab showing your resolved settings. On the Mode tab, pick auto-allow. Sandboxed commands then run without a permission prompt, because the operating system is doing the enforcing, not a classifier.
macOS needs nothing extra. Sandboxing uses the built-in Seatbelt framework. Linux and WSL2 need two packages:
sudo apt-get install bubblewrap socat
Fedora users run dnf install bubblewrap socat instead. Restart Claude Code and open /sandbox again to confirm the Dependencies tab is gone.
To turn the sandbox on for every project instead of choosing it per session, add this to ~/.claude/settings.json:
{
"sandbox": {
"enabled": true
}
}
By default, sandboxed commands can write only to your working directory and a session temp folder. Read access is wider: the whole machine, except a short deny list, which matters for the next section. On the network side, no domain is pre-allowed. The first time a sandboxed command needs one, Claude Code prompts you, or checks it against the classifier in Auto Mode. Pre-approve the domains you actually use:
{
"sandbox": {
"enabled": true,
"network": {
"allowedDomains": ["github.com", "registry.npmjs.org", "pypi.org"]
}
}
}
Add "strictAllowlist": true under that same network block and anything outside the list gets denied outright instead of prompted. Without it, an unlisted domain still stops for approval rather than downloading silently, which alone would have caught Rehberger's ZIP archive before it landed.
If you already run the statusline and permissions baseline from Claude Code's macOS setup, the sandbox block goes in the same settings.json, right next to it.
What credentials the sandbox doesn't block by default
Default read access covers your entire machine except a short deny list, and that list doesn't include your AWS credentials or SSH keys unless you add them yourself. A sandboxed command can still read ~/.aws/credentials and ~/.ssh straight out of the box, and reading a key is enough to leak it if the network layer allows the destination it gets sent to.
Add explicit deny entries for anything with real blast radius:
{
"sandbox": {
"enabled": true,
"credentials": {
"files": [
{ "path": "~/.aws/credentials", "mode": "deny" },
{ "path": "~/.ssh", "mode": "deny" }
],
"envVars": [
{ "name": "GITHUB_TOKEN", "mode": "deny" }
]
}
}
}
There's a lighter option too. Setting a credential's mode to mask instead of deny keeps tools like gh or npm working: the sandboxed command sees a placeholder value, and the sandbox's own network proxy swaps in the real one only on requests to hosts you name explicitly. Reserve deny for credentials a coding agent has no task-related reason to open at all.
Is Auto Mode enough on its own, or do you need a container too?
Auto Mode and the sandbox solve different problems. Auto Mode's classifier decides whether an action runs at all. The sandbox decides what a Bash command can actually reach once it's already running, enforced by the operating system instead of a model's judgment call. Turning one on doesn't turn the other off, and I run both together on every engagement now.
Neither one is optional once real credentials are involved.
I built almost the same boundary once, for humans instead of an agent. At Green Hat, developers had full access to a dev environment, limited access to staging, and zero access to production without a reviewed pull request. Sandboxing a coding agent follows the same principle, enforced by bubblewrap and a network proxy instead of a human reviewing a diff.
Same boundary, different enforcer.
The pattern that breaks without any of this already wrecked a store I wrote about separately: an AI agent given unrestricted API access to a live WooCommerce stack didn't need to be hijacked to cause damage. It had permission and no boundary, full stop. A hijacked coding agent with the same lack of boundary is that same failure with an extra step bolted on the front. OpenClaw ships with almost the identical default, covered in where n8n, OpenClaw, and Claude's agent tools actually diverge: sandboxing is optional there too, and plenty of people skip it.
Running Claude Code with --dangerously-skip-permissions, or with bypass permissions enabled in any interface, only belongs inside a container or VM with no path back to your real files. Anthropic's own documentation says as much: the flag disables every prompt and protected-path check, and it's safe only where a compromised session genuinely cannot reach anything that matters.
So what do you actually change first?
If Claude Code only touches a disposable project with nothing sensitive to read, the built-in sandbox on auto-allow, with a real domain allowlist, covers daily work. If it's touching a client's production stack, cloud credentials, or SSH keys with a real blast radius, put that sandbox inside a container too, turn the network layer on deliberately instead of leaving it to prompt, and stop treating an Auto Mode approval as proof a command was safe.
Rehberger's chain succeeded 3 times out of 5 with the weaker variants, and 4 times out of 5 with the strongest one. Ask yourself what those runs would have reached on your machine.
If your team runs Claude Code, or any coding agent, against real client infrastructure and nobody has verified what a hijacked session could actually reach, that's the kind of architecture review I do before it turns into a postmortem. Get in touch and I'll walk through what your setup actually exposes.