← Articles

// FIELD NOTE

The Malware Was Never in the Repo

Cory LaNou

Cory LaNou

The Malware Was Never in the Repo

The Malware Was Never in the Repo

Overview

Mozilla's 0DIN team published a proof-of-concept where a developer clones a normal-looking GitHub repo, asks Claude Code to get it running, and ends up handing an attacker a reverse shell. The malicious payload never appears in the repository. It arrives in a DNS TXT record, fetched by a setup script, suggested by an error message, followed by an agent that was only trying to be helpful. I've been saying for a while that coding agents are part of your supply chain now. This is what that looks like when someone weaponizes it, and the fix has a lot more to do with how you structure work than with which model you run.

Three indirection steps to a shell

On June 25, 0DIN researchers Andre Hall and Miller Engelbrecht published "Clone This Repo and I Own Your Machine", and the thing that makes it worth your attention isn't a clever exploit. It's how boring the setup is.

A developer clones a repository and asks their agent to get it running. The repo looks fine. A quick scan doesn't scream malware, because there is no malware in it. There's a Python package that refuses to run until it's initialized, and when it fails, it fails with a friendly, professional-looking error that suggests the fix: run python3 -m axiom init. Claude Code does what any of us trained it to do. It reads the error, believes the error, and runs the suggested command to unblock itself. That init command calls a shell script. The shell script does a DNS lookup. The TXT record on the attacker's domain holds a base64-encoded reverse shell, which now executes with the developer's privileges — their credentials, their API keys, their environment variables, all of it.

The researchers put it better than I can: "Claude Code never decided to open a shell. It decided to fix an error. The reverse shell is three indirection steps away from anything Claude Code actually evaluated." That's the whole game. No scanner catches it because the payload isn't in the code. No reviewer catches it because there's nothing to review — the dangerous bytes live in a DNS record that only materializes at runtime, on a machine that's already trusted the repo enough to start fixing it.

I'm deliberately not turning this into a recipe. The lesson isn't the command chain. The lesson is the trust boundary. An automated tool with a shell was resolving an error by executing instructions from a codebase nobody had decided to trust yet. That's different from the old "don't install sketchy dependencies" advice, because the dependency wasn't the weapon. The workflow was.

The helpfulness is the attack surface

The uncomfortable part is that the agent did nothing wrong by its own definition of the job. We reward agents for pushing through friction. The setup failed, so try the documented fix. The dependency is missing, so install it. The config is incomplete, so initialize it. The test environment is broken, so repair it. Every one of those instincts is exactly why agents are useful, and every one of them is a place an attacker can stand.

In a repo you trust, that persistence is the feature you paid for. In a repo you don't, it's an execution engine for instructions you never read. The behavior doesn't change between the two cases — only the trust does, and the agent has no reliable way to tell the difference on its own. A lot of people are still driving agents like it's 2024: open a terminal, hand over a repo, let it run commands, touch the network, read local files, reuse the logged-in browser profile, and then act surprised when it crosses a line nobody ever drew.

You can't fix that with a sentence in a prompt. "Be careful" is not a security model. "Don't run suspicious commands" is worthless when the entire trick is making every individual step look ordinary. The boundary has to live somewhere the agent can't talk its way past.

This wasn't a one-off, and it isn't the scary part

If 0DIN's PoC were an isolated stunt, I'd file it under "neat" and move on. It isn't. It's the clean, well-documented version of a category that has been getting louder all year.

Go back to July 2025, when someone slipped a destructive instruction into version 1.84 of the Amazon Q VS Code extension — an English-language command to wipe the machine and its cloud resources — after getting in through an over-scoped GitHub token in the build config. It shipped to users. It only failed to fire because of a syntax error the author left in on purpose to make a point. The novel part, as TechTarget noted, was that the payload was written in plain English aimed at an AI, not in code aimed at a runtime.

The volume behind that anecdote is the real story. Phoenix Security's 2026 supply-chain analysis counted 37 malicious-package campaigns and 497 indexed packages in the first half of 2026 alone — against 14 campaigns and 111 packages for all of 2025. May 2026 was the single busiest month on record, and one self-propagating worm generated 226 of that month's packages by itself. The defining innovation of the period is the worm: a mechanism that turns a single stolen maintainer token into hundreds of poisoned packages with no human in the loop between hops.

Then there's the blind spot the agents create directly. Research on package hallucination — the basis for what's now called slopsquatting — found LLMs inventing package names that don't exist at rates from about 5% on the best commercial models to over 20% on open ones. Attackers don't have to guess which names; they can watch what the models suggest and register the fakes. When your agent confidently reaches for a dependency that didn't exist until a squatter created it last week, the verification step you skipped is the one that mattered.

Attackers are now writing to the agent, not just the code

The turn that should get your attention is that the newest campaigns aren't only poisoning packages. They're poisoning the instructions your agent reads.

The "Miasma" npm worm that ran in early June 2026 is the clearest example I've seen. StepSecurity's breakdown shows it skipping the usual preinstall/postinstall hooks that scanners watch, and instead hiding execution inside a 157-byte binding.gyp file that fires automatically during npm install. It hit packages like @vapi-ai/server-sdk with over 400,000 monthly downloads. What makes it relevant here is the persistence layer: it writes malicious configuration into .claude/setup.mjs, .cursor/rules/setup.mdc, .gemini/settings.json, and .vscode/tasks.json — the files your AI assistant reads and trusts every time you open the project. Phoenix's data backs the trend up bluntly: AI-agent skills and configs carried a 15.6% malicious-risk rate in their deep scans, more than double the 6.9% they saw for regular VS Code extensions, some of it delivered through CLAUDE.md and .cursorrules files seeded with zero-width Unicode instructions a human editor would never see.

Sit with what that means. The attacker's target is no longer just the code the agent writes. It's the agent itself — the standing instructions, the session hooks, the setup scripts that run before you've typed a single prompt. Your CLAUDE.md is a config file. Treat it like one.

Boundaries have to be real objects, not good intentions

I keep coming back to the same conclusion, and this research is one more argument for it: the answer isn't a smarter chat window. It's controlled orchestration.

When you manage agents as chat sessions, your safety model is mostly vibes. You watch the terminal, approve a prompt when it pauses, skim the diff at the end, maybe keep a few rules in a CLAUDE.md. That's soft pressure, and the 0DIN chain walks right through it — every individual step looked reasonable in isolation, which is exactly what a person skimming a terminal would approve. When you manage work instead, the work has to move through a system, and each stage is a place to put a boundary the agent can't argue with. A scoped issue defines the task. A fresh worktree isolates the change from your source checkout. Setup and network actions sit behind an explicit policy instead of the agent's discretion. A validation gate runs before anything counts as done. A pull request is a review boundary. CI runs in an environment that doesn't have your SSH keys or your logged-in browser.

That's the whole thesis behind Detent, the orchestrator I've been building and writing about. I'm not going to pretend a board makes agent risk disappear — nothing does, and anyone selling that is lying. What it gives you is somewhere to put the discipline. A lane can mean "not reviewed yet." A gate can mean "passed the repo's checks." A serialized merge train can mean "this is the only change touching main right now." A worktree can mean "this agent is not mutating the source." Security gets better when those boundaries are real objects in the workflow instead of promises in a prompt, because an object doesn't get talked out of enforcing itself by a convincing error message.

The guardrails I actually run with

None of this requires you to adopt my tooling. It requires you to stop letting an agent escalate from "read this repo" to "repair and run this repo" without a boundary in between.

Treat every unfamiliar repo as untrusted, and make the first pass read-only. The agent is great at inspection — let it summarize the manifests, the install hooks, the CI config, and every setup step that implies network execution — but inspecting is not the same permission as executing, and the jump between them should be a decision you make, not a side effect of an error message. When it's time to execute, do it somewhere disposable. Worktrees are the floor, not the ceiling: they isolate the code change, but they don't isolate your secrets, your browser sessions, your SSH agent, or your network by themselves. The closer a repo sits to "unknown," the more that execution belongs in a container, a throwaway VM, or a remote environment you can burn down.

Stop handing agents your logged-in life. If an agent can drive a browser profile that's authenticated to GitHub, your cloud console, and your admin panels, a compromised session inherits all of it — this is the exact blast radius the 0DIN shell was reaching for. Keep automation profiles project-scoped and keep credentials out of the default shell when the task doesn't need them. Make network access, package installs, and remote-config fetches things the agent has to ask about in an untrusted repo, and make it explain what will run before it runs. Push verification off your laptop and onto CI, which is cleaner, more repeatable, and far less personally privileged than the machine that holds your keys. And when you review, review the behavior, not just the diff. A clean final diff can hide an ugly path — what the agent fetched, what it ran, what it touched outside the repo. That trail is now part of the security story, which is one more reason I'd rather have it captured as orchestration state than lost in a scrollback buffer.

The checklist I run before I hand over a repo

  • Is this repo already trusted, or is it unknown?
  • Can the first pass be read-only?
  • Which setup scripts, package hooks, or generated configs can execute on install?
  • Does the task actually need network access right now?
  • Does it actually need real credentials right now?
  • Is the agent on a project-scoped browser profile, not my main one?
  • Is the work happening in an isolated worktree?
  • Would a container or disposable VM fit better than my laptop?
  • Is there a validation gate before review?
  • Is there a pull request boundary before merge?

If the honest answer to several of those is "I don't know," the agent has no business running arbitrary setup yet.

The takeaway

The 0DIN proof-of-concept isn't a reason to stop using coding agents. It's a reason to grow up the workflow around them. These tools read docs, run commands, fix setup errors, open browsers, call APIs, and glue systems together — which means they are part of your supply chain now, whether or not you've decided to treat them that way. Attackers already have.

The fix isn't fear, and it isn't trusting the agent less because it's dumb. The agent isn't dumb. It's helpful, and helpful is precisely what someone will aim at you. Isolate the workspace, make the trust boundaries explicit, keep inspection separate from execution, gate the risky transitions, and move work through states you can actually review. Manage the work, because you can't out-prompt the helpfulness.

The repo and the docs for Detent are on GitHub if you want to see what putting those boundaries into a system looks like.

Want more AI development insights?

Subscribe to the newsletter for weekly tips on using AI in professional development.

Subscribe to Newsletter

// KEEP READING

More articles