Back to all posts
Published on · by Renaud Deraison

The repo really was Microsoft's

On June 5–6, 2026, the Miasma worm pushed credential-stealing code into 73 repositories across four of Microsoft's own GitHub organizations — Azure, Azure-Samples, microsoft, MicrosoftDocs — including Azure/functions-action, the official deploy Action, and durabletask, a repo that had already been cleaned once in May. This time the payload did not wait for npm install. It fired the moment a developer opened the repository in Claude Code, Cursor, Gemini CLI, or VS Code. Here is why the trust signal — 'it's a Microsoft repo' — was again the attack surface, and what changes when the agent that opens it lives in a per-profile Bromure VM, behind a credential broker, a read-write guardrail, and a package cooldown.

A week ago the worm rode in on Red Hat's npm scope and fired through a preinstall hook. This week it rode in on Microsoft's GitHub and did not need an install at all. Miasma planted project-scoped configuration into 73 Microsoft-owned repositories — Azure/functions-action, the official deploy Action, among them — and the payload executed the moment a developer opened the repo in Claude Code, Cursor, Gemini CLI, or VS Code. Cloning was not the trigger. Opening it in your agent was.

A developer who clones Azure/functions-action to debug a failing deploy does not hesitate, and neither does the agent they point at it. It is a first-party repository from Microsoft's own Azure organization — the canonical source of the GitHub Action half the ecosystem references as Azure/functions-action@v1. There is no maintainer to vet, because the maintainer is Microsoft. There is no name to squint at, because the name is exactly what it should be. So the repo gets opened, and the agent reads the project's configuration the way every modern coding tool does on folder-open — and one of those config files points at a command, and the command is a roughly 4.3-megabyte blob that starts reading the filesystem for keys.

We wrote up the npm half of this exact campaign seven days ago. Miasma is the same worm — a variant of the "Mini Shai-Hulud" code TeamPCP released publicly in mid-May — and the uncomfortable fact it demonstrates is the same one, turned one more notch: reputable source is not a security control. It feels like one. Most supply-chain advice leans on it. And on June 5 it bought exactly nothing, twice over: the namespace was Microsoft's, and the execution did not come from a package you chose to install. It came from opening a folder.

What Miasma did to Microsoft's repositories.

On June 5–6, 2026, GitHub disabled 73 repositories across four Microsoft GitHub organizations after malicious commits were pushed into them, per The Hacker News and a detailed teardown from StepSecurity. Redmond Magazine covered it on June 8. The breakdown:

  • Azure — 49 repositories, including Azure/functions-action (the official Functions deployment Action) and the language workers for .NET, Python, Java, Go, and PowerShell.
  • microsoft — 10 repositories.
  • Azure-Samples — 13 repositories.
  • MicrosoftDocs — 1 repository.

The shape, stripped to its mechanics:

  • The entry point was a previously compromised contributor account with commit access, used to push malicious commits directly into the repositories — tagged [skip ci] so the changes slid past the CI/CD checks that would otherwise have run.
  • The commit planted project-scoped configuration — the kind of file a coding agent or IDE reads and acts on automatically when you open the folder: an editor task, an agent hook, a project-defined MCP server. This is the same class of trust boundary Adversa AI's TrustFall demonstrated across Claude Code, Cursor CLI, Gemini CLI, and Copilot CLI — all four execute project-defined configuration right after the folder-trust prompt.
  • The payload — roughly 4.3 MB of obfuscated code — executed when the repository was opened in Claude Code, Gemini CLI, Cursor, or VS Code, or run through an npm test script. Not on clone alone. The act of pointing your agent at the cloned tree is what ran it.
  • On execution it swept the host for GitHub tokens, AWS keys, Azure service principals, GCP credentials, npm and PyPI publish tokens, SSH keys, and .env files, then used the stolen access to commit itself onward — which is what makes it a worm rather than a one-shot.

One detail is worth dwelling on: Azure/durabletask was among the repositories hit — and it had already been compromised in May in the TeamPCP campaign and cleaned. A repo that was remediated once was re-poisoned five weeks later. Cleanup is not a state you reach and keep; it is a state you fall back out of the moment another credential in the chain is taken.

It is worth being equally precise about what did not happen. Microsoft's corporate network was not breached. Azure the cloud service was not breached. No customer data and no production system was touched. This was an attack on source-code repositories — and its widest-felt consequence had nothing to do with the malware at all: the instant GitHub disabled Azure/functions-action, every pipeline on earth that referenced Azure/functions-action@v1 stopped resolving. Microsoft was the highest-profile carrier. The people actually owned were the developers who opened the poisoned repos in an agent between June 3 and 5, and had their credentials swept off their own machines.

DEVELOPER LAPTOP — host secrets visible to whatever the agent runs on folder-openMICROSOFT'S OWN GITHUBcompromised contributoraccount → push [skip ci]Azure/functions-actionAzure/durabletask (again)+ project config plantedDEVELOPER CLONESgit clone …/functions-actionnothing runs yetfirst-party repo ⇒no reason to hesitateOPENED IN THE CODING AGENT — payload firesClaude Code · Cursor · Gemini CLI · VS Codefolder-open → reads project configagent hook / task / MCP server↳ executes 4.3 MB payload(clone alone did NOT run it)HOST FILESYSTEM & ENV — all real, all readable by the payload$GITHUB_TOKEN, gh hosts.ymlpush access (real)~/.aws, Azure service principalscloud keys (real)~/.npmrc, PyPI tokenpublish here (real)~/.ssh/id_ed25519, .envkeys + CI secrets (real)tar | encrypt | exfiltrate→ harvested off your machineSELF-PROPAGATIONstolen GitHub accesscommit to next repoplant the same config73 repos, 4 orgsdurabletask: hit twice
Miasma on a developer machine that opens an affected Microsoft repo in a coding agent. A previously compromised contributor account pushes a [skip ci] commit that plants project-scoped configuration — an agent hook, an editor task, an MCP server definition. The developer clones the repo (nothing runs) and opens it in Claude Code, Cursor, Gemini CLI, or VS Code. On folder-open the agent reads and acts on that configuration, executing a 4.3 MB payload that sweeps the host for GitHub tokens, AWS keys, Azure service principals, npm/PyPI tokens, SSH keys, and .env files, encrypts them, ships them out, and uses the stolen GitHub access to commit itself into the next repo. No typosquat, no fake maintainer, no npm install. The trust signal is the Microsoft org name, and the execution comes from opening a folder.

The same repo, opened inside Bromure Agentic Coding.

Bromure Agentic Coding runs your coding agent inside a per-profile Linux VM — its own kernel, its own filesystem, its own network stack, on Apple's Virtualization framework. A profile is a coherent scope of work: this client, this internal service, this open-source repo you cloned to debug a deploy. You clone Azure/functions-action into that profile and you open it with your agent in there. The folder-open trigger fires exactly as designed. The payload runs. And then it goes looking for keys on a host it cannot reach.

Because the credentials are not in the profile. The VM ships with stubs — fake tokens that look real to git, gh, aws, kubectl, npm, and anything else expecting an Authorization header. A proxy on your Mac sits in front of every connection leaving the sandbox, recognizes the stub, and swaps it for the real secret at the wire as the request leaves (the sandbox that held the key walks through the mechanism). The real GitHub PAT, the real AWS key, the real Azure principal — none of them touch a file, an environment variable, or a page of memory the VM can read. SSH keys never leave the macOS Keychain at all; only the ssh-agent socket is forwarded in, the way OpenSSH always intended.

So walk Miasma's sweep through that boundary. The payload reads the environment for $GITHUB_TOKEN and finds a stub. It reads ~/.aws and finds nothing. It reads ~/.npmrc and finds no publish token. It reads ~/.ssh and finds no key file — there is a forwarded socket, not a private key on disk. The 4.3 MB blob runs to completion exactly as written. It just exfiltrates a box that was never holding your keys, on the wrong side of a hardware-enforced boundary from everything that matters.

PER-PROFILE BROMURE VMagent opens functions-actionproject config → payload firesruns, but inside the guest onlyWHAT THE PAYLOAD SEES$GITHUB_TOKENstub_7f3a…~/.aws, ~/.npmrcstub / absent~/.ssh key filesocket onlypropagate: git push (self)needs a write → asks the proxyexfil host keys: nothing to takeneeds a dependency?npm install → fetch leaves VMthrough Bromure's proxyPROXY · YOUR MACCREDENTIAL BROKERreal PAT / keys held herestub → real, at the wirevalue never enters the VMGUARDRAIL: READ/WRITEpush = mutate → PROMPTnames verb + targetSUPPLY CHAINOSV + socket.dev scancooldown: < 2 days heldinstall scripts strippedOUTCOMEhost keys:stubs harvested,real ones untouchedpush to propagate:paused, you say nofresh bad package:never reaches the VMblast radius = 1 profile
Three layers, in the order Miasma hits them. (1) Supply Chain: the proxy scans every package fetch against OSV and socket.dev and quarantines releases younger than the cooldown — so the agent can't even pull a fresh malicious dependency while the ecosystem is still catching up. (2) Credential brokering: the VM holds only stubs; the real secrets sit on the host behind the proxy, swapped in at the wire, so the payload's host sweep finds placeholders. (3) Guardrails: the worm's propagation step — a git push to commit itself onward — is a state-changing call, and a read-write guardrail stops it at the wire and asks, naming the verb and the target. Each layer is enforced below the agent, at the VM boundary the agent cannot route around.

The propagation step is a write, and writes get a prompt.

Stealing keys is only half of Miasma. The other half is spread: it used stolen GitHub access to commit itself into the next repository, and that is what turned a handful of poisoned repos into 73. Even in a profile that legitimately has push access — say you cloned functions-action precisely because you intend to open a PR against it — the worm's propagation step still has to go out through the proxy, and that is where Guardrails meets it.

Guardrails reads the operation, not just the connection — it tells a read from a write. A git fetch is a read; a git push is a write. Set a profile's GitHub credential to ask on write, and the moment the agent reaches for a state-changing call — the git-receive-pack the worm needs to commit its config back, a DELETE against an API, a Terminate* on EC2 — Bromure stops it at the wire and surfaces a prompt on your Mac that names the verb, the target, and the profile. The grant you give is time-boxed: fifteen minutes for a release, single-use for the scary ones, never if the ask makes no sense. Reads never interrupt you; the agent fetches and greps and reads all day. It is the mutation that pauses.

This is the difference between "the agent has a token" and "the agent can do whatever it wants with the token." Miasma's whole spreading mechanism is a write the agent never told you it was making — and a write the agent never told you it was making is exactly what the read-write prompt is built to catch. The push that propagates the worm becomes a dialog box you click Don't allow on, the same way "the agent deleted the production database" stops being a postmortem and becomes a prompt you declined.

The version was hours old, and Bromure makes packages age.

There is a second way Miasma — and the broader Mini Shai-Hulud lineage — reaches a developer: not through a repo you open, but through a freshly poisoned package the agent installs while doing its work. The Red Hat half of this campaign was precisely that, a preinstall hook on 32 packages in a trusted scope. And the brutal detail of those incidents is timing: a compromised version typically gets caught and yanked within hours — but those are exactly the hours during which an autonomous agent, running unattended, might pull it.

Bromure's Supply Chain layer turns the same boundary proxy into a scanning checkpoint, and it does the two things that actually matter against a same-day compromise:

  • It force-scans every fetch against socket.dev as well as OSV. OSV catches known CVEs above the severity threshold you set. socket.dev catches what the vulnerability databases have not caught up to yet — rogue install scripts, behavioral malware, typosquats, the just-published compromise. A flagged release is blocked before the tarball ever lands in the VM. Crucially the scan runs below the agent, at the proxy: however the agent rewrites its own config to route around you, the fetch still leaves through the boundary it cannot cross.
  • It enforces a cooldown. Bromure quarantines any release published in the last two days — tunable — so a version uploaded an hour ago is simply not installable in that profile while the ecosystem catches up. Against a worm whose entire window of opportunity is the gap between publish and yank, a cooldown is not a heuristic about whether a package looks bad. It is a refusal to be the first one to find out. Combine it with the install-script stripping Bromure does on the fly — pulling postinstall hooks out of the tarball and fixing the metadata hash so the install still verifies — and the package that does land lands inert.

For Miasma specifically, the repo-open vector is the headline. But the same campaign spreads through packages too, and the cooldown is the control that would have starved the npm side of it: a fresh @redhat-cloud-services release, or a freshly poisoned transitive dependency pulled while debugging that Microsoft repo, sits in quarantine through the exact hours it is dangerous.

Where this does not save you.

A push you approve is a push that happens.

The read-write guardrail catches the write the agent didn't tell you about. It does not read the diff. If you are legitimately pushing to functions-action and you approve the prompt, Bromure forwards the push — including, in principle, a poisoned workflow you didn't notice in the diff. Read what you approve. The session trace tells you which diffs to read.

The cooldown is a window, not a wall.

Two days is tuned to the observed publish-to-yank gap, but a patient attacker can sit on a compromised version longer than the cooldown and still be installable on day three. The cooldown starves same-day worms; it does not vouch for a package that has merely gotten old. socket.dev and OSV still have to do their part.

The profile is long-lived, so persistence persists.

A Bromure profile is not a disposable disk. A payload that writes itself into a startup path inside the profile can survive into the next session in that profile. What it wakes up to is a guest with no host keys and a broker that only speaks short-lived, prompted, scope-limited tokens — presence in a box with nothing in it — but presence all the same.

Scope the broker on purpose.

If a profile is provisioned to push to a repo today and that profile runs Miasma today, an approved write goes through. Broker grants work because they are narrow. A profile that only needs to read a repo should not be able to write it; a profile that never publishes should hold no publish token. Isolation contains the blast; scoping decides how big it could ever have been.

The next trusted repo is already cloned somewhere.

The lesson of the TanStack worm was that the lockfile and the signature are not defenses. The lesson of the Red Hat scope was that neither is the publisher. Microsoft adds the next corollary: neither is the repository, and the trigger does not even have to be an install you chose — it can be a folder your agent opened. The Azure/functions-action repo did nothing wrong by being trusted. Being trusted is the whole point of a canonical first-party Action, and it is exactly what made it worth poisoning — twice, in the case of durabletask.

You cannot fix that by trusting more carefully, because the trust was never misplaced. You fix it by arranging things so that "which repo is this" and "which scope published this" stop being the questions your keychain depends on. Bromure Agentic Coding is the configuration where the agent opens the repo inside a per-profile VM, the real credentials stay on the host behind a broker, every write the agent makes has to get past a prompt, and a package can't be installed until it has survived a cooldown. The worst a poisoned folder-open can do is exfiltrate a box that was never holding your keys. It is free, open-source, and shipped today.