Back to all posts
Published on · by Renaud Deraison

One session, a hundred repositories

Mandiant's 2026 AI Risk and Resilience report describes an attacker who took over a live AI coding-assistant session at a SaaS provider, got a poisoned PyPI package installed through it, stole GitHub OAuth tokens, and spread the Shai-Hulud worm across about 100 internal repositories. The report does not say how the session was taken. In a Bromure Agentic Coding workspace it does not need to: the token in that session is a `ghp_` decoy, the attempt to mail it out pauses the VM, and the pushes that turn one machine into a hundred repositories stop at the host proxy.

Someone took over a coding session that was running, authenticated and trusted, on a developer's own machine. Everything that followed, up to a hundred repositories, came out of what that machine was holding at the time.

A developer at a software-as-a-service company asked their coding assistant which package to use for some ordinary problem. The assistant named one. The developer said yes, which is the entire point of having an assistant, and pip install did the rest.

An attacker had the session by then, and typed that recommendation.

Mandiant describes the case in its AI Risk and Resilience Report 2026, published this month by Google Cloud's incident-response arm; The Hacker News wrote it up on September 16. The attacker hijacked an active coding-assistant session on a developer's workstation. Five more steps carried them to roughly a hundred internal code repositories and the company's own package namespace, where a second employee pulled the poisoned version.

The chain, hop by hop

One workstation, six stepswhat the step spends1an attacker takes over a live coding-assistant sessionmethod not disclosed in the public case studythe session2the assistant recommends poisoned softwarethe developer accepts it, as designedthe developer's yes3a compromised PyPI package installs an infostealerordinary dependency traffic, ordinary installregistry trust4GitHub OAuth tokens are read off the workstationthe pivot: everything below needs this to workstored tokens5Shai-Hulud spreads to about 100 internal repositoriesrepository secrets and product source code takenwrite access6a package in the company's own namespace is poisoneda second employee pulls that version and is infectedpublishing rights
The six steps Mandiant describes, and the credential each one spends. Mandiant discloses no method for step 1. In each step after it, the attacker uses an authorization the developer's machine was holding already: the session's own trust, then the package manager's, then GitHub's, then the company's publishing rights.

Mandiant withholds the part you would most want. The public case study does not say when the intrusion happened, and it does not say how the attacker got control of a running session. Stolen browser cookies, a malicious extension, a compromised CLI on a shared box: the report picks none of them. Anyone reading for the patch will leave without one.

Steps 2 through 6 contain no exploit at all. A package install works the way package installs work, and code running as the developer reads the tokens sitting on the developer's disk. A git push carrying a valid token pushes, and a publish carrying valid publishing rights publishes. One step was an intrusion; the other five were spending.

The multiplier is credentials, and it keeps growing

Shai-Hulud is the self-replicating supply-chain worm that has been eating maintainer accounts since 2025, and it turns up as the payload in a story like this one because step 5 is what its authors built it for. GitGuardian took apart a recent variant in August and found it sweeping 469 credential locations, up from 189 in earlier builds: developer environments, CI/CD tooling, cloud configuration, AI tool configs, package-manager config files, shell history, .env files, IDE settings, CLI caches. The worm reads a filing cabinet rather than hunting for a bug.

GitGuardian's write-up puts the category in one sentence:

"Attackers have stopped trying to break trust relationships and started using the credentials that already make those relationships work."

One workstation became a hundred repositories because it was holding a hundred repositories' worth of authorization. That number came out of an inventory, and you decide what goes into the inventory.

The session was holding decoys

Bromure Agentic Coding runs the agent inside a Linux virtual machine on your Mac, and that machine holds no real secrets. You switch nothing on to get this. It is how a workspace stores a credential in the first place.

Your real GitHub token stays encrypted on the host. The VM receives a structure-preserving fake: ghp_ followed by 36 characters, 40 in total, so gh's own prefix-and-length checks pass without complaint. Bromure exports it as GH_TOKEN and writes it into ~/.git-credentials and the gh configuration, and it is the only GitHub credential anywhere in that machine. A proxy on the host substitutes the real value onto the wire after the request has left the VM, and only when the destination matches the host the credential was minted for. Each fake is deterministic, derived from the real value plus a per-install 32-byte salt through HKDF-SHA256, so a client that fingerprints its own key sees no apparent rotation between sessions.

Run step 4 against that. The infostealer arrives, sweeps the 469 locations, finds the environment variable, finds ~/.git-credentials, finds the gh config, and ships what it found. Each read succeeds, and each file sits where the worm expected it. The attacker ends up with a forty-character string that authenticates nowhere, except by passing back through one specific Mac.

Steps 5 and 6 both need step 4 to have produced a working token, and step 4 produced a decoy.

The theft is also the alarm

A fake token has one legitimate destination. No innocent request carries a ghp_ decoy minted for github.com to some other host, so the proxy scans each outbound request, headers and body, for any fake leaving its own scope. It runs an Aho-Corasick automaton, cheap enough to point at all of the traffic.

When the proxy finds one, it does more than log:

  • it refuses the request with HTTP 451, and no byte of it reaches the destination;
  • Bromure pauses the VM on the spot;
  • an alert offers you Shut down, Save for Investigation (export the disk, home and shared folders for forensics first), or Continue at your own risk;
  • the Security Timeline gains a red Credential brokering row;
  • Bromure marks the workspace compromised, so the next launch wipes the disk image and the persistent home. Your tokens, SSH keys and workspace settings survive that wipe.

You enable none of this; the detector is always on. The exfiltration attempt in step 4 is what stops the session, so the incident ends with a paused VM and a timeline entry instead of a hundred repositories and a case study.

A hundred repositories is a hundred pushes

Suppose you want the belt as well as the braces. Step 5 is a worm writing to repositories, and the host classifies writes.

A workspace's guardrails carry a write policy per service. For GitHub, that policy reads git-over-HTTPS as well as the REST API: git push arrives as git-receive-pack and counts as a write, while git fetch arrives as git-upload-pack and counts as a read. Read-only refuses the pushes. Prompt before write, the default for new workspaces, holds each one for a host-side dialog naming the operation. Fetches pass under both, so the agent keeps working.

The proxy makes that call on the host, outside the VM. An agent whose session belongs to someone else cannot switch the policy off or route around it, because the policy does not run on the agent's side of the line. A worm that wants a hundred repositories collects a hundred refusals, or asks you a hundred times.

And the package still had to arrive

Step 3 is where Mandiant gives concrete advice: validate AI-recommended third-party dependencies against cryptographic checksums and allowlists, and route dependency traffic through repositories you control.

In a workspace, each package fetch passes through the host already, since the proxy is the VM's only route to the network. Bromure intercepts PyPI at pypi.org and files.pythonhosted.org and applies the host's policy there. The in-VM pip.conf can tighten what the proxy served; it cannot loosen it. Three layers speak for Python packages:

  • the age gate, which is on by default with a two-day minimum and refuses versions younger than the cutoff, on the theory that a freshly published release is the one most likely to have just been hijacked. PEP 503 indexes carry no timestamps, so Bromure looks the publish time up on demand;
  • the OSV check against api.osv.dev, free and keyless, which blocks any version carrying an advisory at or above the severity you choose;
  • socket.dev's compromised-package filter, which fires on malware, known malware, typosquatting and rogue install scripts.

A block comes back as an HTTP 451 whose body begins Bromure Supply-Chain Security blocked this request: followed by the reason, and pip prints that verbatim. You see why the install failed, and so does the agent, which can often pin an older version by itself. That older version is the one the age gate was steering it toward.

An ordinary workstationstep 3: the package installsthe registry serves the poisoned versionnothing on the path judges provenancestep 4: the tokens are readreal OAuth tokens in env, config, historyexfiltration ships a working credentialstep 5: the worm pushesa valid token pushes, so it pushesabout 100 repositories, plus the namespaceInside a Bromure workspacestep 3: the fetch is judged firstage gate, OSV, socket.dev, on the host451 before a byte reaches the VMstep 4: the same read succeedsall 469 locations hold ghp_ decoyssending one out: 451, and the VM pausesstep 5: the push is classifiedgit-receive-pack refused or promptedgit fetch still passes; work continues
The same three load-bearing steps, on two machines. On the left, an ordinary workstation: real tokens on disk, a theft that works, and pushes that go through. On the right, a Bromure Agentic Coding workspace: the host refuses the poisoned version before it reaches the VM, the same theft returns a decoy and pauses the machine, and the host classifies the pushes, where a hijacked session gets no vote.

Mandiant's three controls, and where they live

The report closes the case study with three recommendations. Each one names a place where a Bromure workspace already puts the boundary.

Validate AI-recommended dependencies

Mandiant asks for checksums and allowlists on anything the assistant suggests. Your workspace applies its policy to the registry response before the VM sees it. The proxy strips too-fresh versions out of the version listing, so from the agent's point of view they do not exist yet, and it refuses a pinned too-fresh version with the package's real age quoted in the error. Reputation keys stay on the host, outside the machine that runs the install.

Keep raw keys and long-lived tokens away

That is the wire boundary written as advice. No file, environment variable or process inside the VM holds a real API key, OAuth token, AWS secret or SSH private key. Bromure keeps subscription OAuth tokens encrypted on the host and refreshes them there about five minutes before expiry, so the guest carries no refresh token. The channel between the two runs one way: the host writes fakes in, and the VM has no call it can make to ask for a real token back.

Route dependency traffic through something you control

The proxy is the VM's only route out. Each HTTPS request travels over a virtual socket to the host, which terminates the TLS, inspects the request, and re-emits it through macOS's own TLS stack. That single path is what makes the other two controls enforceable instead of advisory. No package fetch can skip the checks, and no credential can leave unswapped.

And keep the blast radius small on purpose

A hundred repositories fell because one machine carried authorization for a hundred repositories. Cut workspaces one per project, or one per credential boundary. Each maps to a single VM, no two share one, and each carries the credentials that workspace's work needs. You set the inventory on any one machine in a settings pane.

The part you cannot patch

Mandiant frames the category in one sentence that describes the shape rather than the incident: "a poisoned data source, model dependency, or extension hook can turn a trusted agent into a channel for internal reconnaissance, lateral movement, or autonomous escape from a sandbox."

Three different entry points, one sentence, and it holds for all of them. The entry point sets how an attacker arrives; the contents of one developer's machine set how far they get. This attacker came in through a hijacked session. The next one comes in through something else, and the report names no door for you to lock.

Build for the step after the entry. Assume the session belongs to someone else already, which is the assumption the case study forces on you by never explaining step 1, then ask what that session can reach.

One version of this incident ends at step 3, with a 451 in the terminal and a line in the Security Log. Another ends at step 4, with a paused VM and a red row in the timeline recording that a credential tried to leave. Neither version reaches step 5, and step 5 is where the hundred repositories were.

Put the agent on a machine that holds decoys. Install Bromure Agentic Coding, and let someone take the session.