The install was clean
npm stopped running dependency install scripts by default in July. On September 17 Checkmarx published a campaign that does not argue with that: the loader sits inside `BTree.prototype.set`, the method the library exists to be called with, and fires the first time anything stores a key of 100. It ran for eleven weeks on two million weekly downloads, and its command channel is a smart contract on Ethereum Sepolia. A coding agent is the ideal victim, because running the code is the job. Bromure Agentic Coding decides what that code can reach before it runs.
Blocking install scripts was the right call, and the npm ecosystem made it in July. This campaign moved its execution to the first time you use the package, so the install stays clean and your test suite pulls the trigger.
Your agent needs an ordered map. It picks a b-tree package off npm, adds it to
package.json, and installs it. Nothing runs. npm 12 does not execute
dependency install scripts any more, so there is no postinstall, no
preinstall, no build hook, nothing for a lifecycle-script policy to refuse.
The agent writes the code that uses the new dependency, writes a test that
inserts a few hundred rows, and runs it.
The hundredth insert starts a second Node process.
Checkmarx published this campaign on September 17,
researched by Bruno Dias.
BleepingComputer covered it on the 20th
and InfoWorld on the 21st.
The package is indexed-btree, built to be mistaken for the real
sorted-btree, and by the time npm removed it on September 3 it was pulling
close to two million downloads a week. It had been live, in its malicious form,
for about eleven weeks.
The defence everyone had just finished building
In June the npm CLI flipped its install-script default, and
npm 12 shipped that change in July:
preinstall, install and postinstall from a dependency no longer run unless
the project allows them by name, and git and remote-URL dependencies stopped
resolving by default too. JFrog's numbers, quoted in that coverage, put those
three vectors in roughly 53% of the malicious npm packages seen in the preceding
year. Agent harnesses took the same posture: --ignore-scripts, or a proxy that
strips the hooks out of the tarball before the package manager sees them.
Counting back eleven weeks from the removal date puts the first malicious
version of indexed-btree in the middle of that rollout.
So the operators stopped using install scripts. The loader lives in
BTree.prototype.set, the method the library exists to be called with. When the
key passed to it is 100, the code launches an obfuscated first-stage loader as
a detached Node process from extended/sharedLoad.min.js. Checkmarx's summary
is exact about why this is hard to see coming: the loader hides inside the
library's own method, which is, in their words, "a well-built way to sneak past
standard taint-analysis tools."
Look at what arms it: a key with the value 100, in a library whose purpose is holding keys. A leaderboard reaches it, a paginated index reaches it, and a test that inserts rows in a loop reaches it on the hundredth iteration. Use the package the way its README tells you to and you arm it.
Eleven weeks, and a repository that read fine
The rest of the tradecraft targets the checks you would run yourself.
The command channel is a smart contract on Ethereum Sepolia. The loader reads its next address from that contract, generates an X25519 key pair, takes the operator's public key from the contract, derives an AES key by ECDH, and decrypts two ciphertexts stored on-chain to assemble the second stage. Checkmarx is blunt about the payoff: "This technique is more resilient to domain / IP takedown than traditional C2 approaches, since it uses the smart contract as a pointer to a new address whenever the old one gets taken down." Nothing in that gives you a bad domain to blocklist. You get a public RPC endpoint and a contract address, and the operators can repoint the contract without touching a single installed copy of the package.
The project looked real. The operators stood up a GitHub repository with a plausible commit history and a developer account to go with it, and the code in that repository does not contain the malicious part. It is only in the npm tarball. Reading the source on GitHub, which is the advice everyone gives, would have confirmed the package was fine.
Age worked for them too. Eleven weeks makes a package an established dependency
by the time you install it, nothing like the hijacked release that gets yanked
within hours. Checkmarx counted nine sibling packages with the same behaviour,
btree-core alone past 1.9 million downloads and the others in the hundreds of
thousands each, and says the operators control a wallet holding 109 ETH.
Then, after the second stage lands, the loader deletes its own files and strips the trigger back out of the prototype. The package on disk returns to looking like a b-tree library.
Running the code is the job
Every defence above assumes a moment when a person decides to execute something. An agent removes that moment.
A coding agent installs a dependency, writes code against it, runs the test suite, reads the failure, edits, and runs it again, dozens of times an hour, while you are in a meeting, because that loop is the entire value proposition. You are not reading the hundredth insert. The agent also has more reason than you do to produce the kind of data that trips this trigger: it writes fixtures in loops, seeds tables with sequential IDs, and benchmarks with a few thousand rows. If the arming condition is "someone stores the key 100", an agent gets there sooner and more often than a person, in a process holding whatever the session holds.
Install-time defences cover the moment a package arrives. Your agent spends a minute a day there and the rest of it running what arrived.
What the loader needs
Strip the campaign down and it needs two things: somewhere to reach, and something worth taking on the machine it woke up on. You decide both before it runs, and neither decision requires recognising the package.
On a developer laptop, both go the attacker's way. The reconnaissance it sends,
hostname and architecture and CPU and memory and uptime, describes your machine
on your network. The process it spawns runs as you, with your shell, your
~/.git-credentials, your GH_TOKEN, your ~/.aws, your SSH agent. Its three
destinations are slack.com, api.telegram.org and a public Ethereum RPC
provider: two SaaS hosts that half your tooling already talks to, and one
blockchain endpoint that looks like infrastructure. Nothing in that list reads
as malicious in a netflow log.
Bromure Agentic Coding answers both halves on the host, outside the VM the agent runs in.
The credentials come first, because they are the target. Every secret you
configure exists inside the workspace VM as a structure-preserving placeholder
(a GitHub token is ghp_ plus 36 characters, an Anthropic key starts
sk-ant-api03-brm-), derived from the real value with HKDF-SHA256 and a
per-install salt, so gh and git and claude accept it and it never appears
to rotate between sessions. The real values stay encrypted on your Mac, and the
host proxy substitutes them onto the wire after the request has left the VM,
only for the host that credential was minted for. The manual states the
invariant: no file, environment variable, or process inside the VM ever contains
a real API key, OAuth token, AWS secret, or SSH private key. A stealer that
reads the environment collects placeholders.
Carrying a placeholder somewhere it does not belong is itself the alarm. The proxy scans every outbound request for fakes bound for a host they were not minted for. When one appears, the proxy blocks the request with a 451, the destination receives nothing, and Bromure pauses the VM. It force-reattaches a detached session, tints the frame red, and raises an alert naming the token preview, the credential, the host it belonged to and the host it was heading for. You can shut the workspace down, or take Save for Investigation, which exports the system disk, the home directory and every shared folder before stopping. This loader erases its own files after the handshake, so anyone looking later finds a tidy disk. Pausing mid-request gets you the machine before it tidies.
Then the reach. Guardrails carry a per-workspace egress firewall: an ordered pf-style ruleset over every connection the VM opens, by host, CIDR, protocol, port and, for web rules, individual HTTP method, with an Unmatched traffic control set to Allow or Deny. Set it to Deny, list the registries and APIs the work needs, and a loader reaching for a Telegram bot API or an Ethereum RPC provider meets a refusal at the wire. Two layers enforce it, so the guest cannot route around either: the virtual switch matches rules by destination IP and DNS-snooped hostname across TCP and UDP, and the proxy matches them again by TLS server name and method. The switch diverts the VM's port-80 and port-443 flows into the proxy itself, so there is no proxy variable for a payload to unset. Every verdict lands as a Firewall row in the Security Timeline, and rule edits reach running sessions without a reboot.
Where the calendar does not reach
Bromure's age gate, on by default at two days, exists for the hijack pattern: a maintainer account compromised, a poisoned version published, the whole thing caught and yanked within hours. It has carried several of these stories. Eleven weeks passes any cutoff you would set, so the work here falls to other layers.
The Supply Chain pane is a stack for that reason. The layer built for packages that were malicious from the start is Package filtering: socket.dev's compromised-package block covers malware-flagged packages, typosquats, rogue install scripts and obfuscated code, and Depi replaces the npm registry with a pre-vetted mirror. Both run host-side, with their API keys held on your Mac and never exported into the VM, and both sit in front of an install rather than behind it. A block comes back as an HTTP 451 whose plaintext body npm prints word for word, so the agent reads the reason and routes around it instead of hanging.
No reputation service catches everything, and a typosquat that ran clean for eleven weeks is the hard case for all of them, which is where the rest of the stack earns its place. The placeholder credentials, the compromise detector and the egress ruleset decide what a package can reach once it is installed, and none of them has to recognise the package. They cover code nobody has classified yet, including code nobody will classify for another eleven weeks.
Assume it fires on first use
Blocking lifecycle scripts is worth doing and this campaign routes around it. Plan for a payload that wakes up when your test suite calls the library.
The trigger is valid data
A key of 100 in a key-value library. No allowlist of inputs helps, and an agent generates that data faster than you do.
Leave nothing in the process to steal
Recon that describes a VM and tokens that are worth nothing upstream turn a successful execution into a wasted one.
Decide the destinations in advance
Slack, Telegram and a public Ethereum RPC endpoint are not blocklistable by reputation. An allowlist does not need to recognise them.
Blocking install scripts took roughly half the malicious npm packages off the
board. That is why the operators had to build something this elaborate, with a
fake repository, a trigger disguised as arithmetic and a command channel on a
blockchain, to place code that a year ago needed four lines in a
package.json. The defence worked, and the price of the attack went up.
The assumption underneath never moved. You still treat a dependency as safe once it is installed, and running it as a neutral act. For a coding agent, running it is the job, thousands of times a day, with nobody in the room. Decide what that process may hold and where it may connect before it runs, and your agent's hundredth insert ends up as a blocked Firewall row in the Security Timeline.