Supply-chain case study
Threat analysis · 28 Sep 2026
In May 2026, one campaign published 34
packages across npm, PyPI and crates.io whose real payload
wasn't aimed at your runtime at all. It was aimed at your coding
assistant: instructions hidden in .cursorrules and
CLAUDE.md files, written in zero-width Unicode
characters no human reviewer can see. The assistant reads them,
helpfully "runs a security scan" — and exfiltrates SSH keys, cloud
credentials and tokens. Zero CVEs involved, so version-based
scanners report a clean bill of health. Here is how it worked —
and, honestly, which layer actually stops it.
.cursorrules, CLAUDE.md,
AGENTS.md): they have no business being there.
What happened
The packages themselves looked plausible — helpers and SDK shims under fresh publisher accounts, spread deliberately across three ecosystems. The novel part sat in the repository files modern development tooling treats as configuration: assistant rule files. Between the visible characters, the attackers embedded instructions encoded in zero-width Unicode (U+200B, U+200C, U+200D, U+FEFF) — text that renders as nothing in an editor, a diff, or a code review.
An AI coding assistant, told by convention to honour
.cursorrules or CLAUDE.md, parses
exactly those bytes. The hidden instructions told it to run
what was framed as a routine security check — in reality a
staged payload (trap-core.js) that collected SSH
keys, AWS credentials, GitHub personal access tokens, browser
profiles and cookies, cryptocurrency keystores and secrets from
environment variables, and shipped them to attacker
infrastructure hidden behind a GitHub Pages domain. The
developer saw an assistant doing assistant things.
The campaign, attributed by Socket to fresh throwaway publisher profiles, spanned 21 npm packages (335 versions), 7 PyPI packages and 6 crates — one shared payload, one XOR key, one campaign marker across all three registries. And crucially: no CVE existed at any point. There was no vulnerable version to detect, because the malice was the product. A scanner that answers "are any of my versions on an advisory list?" answers, truthfully and uselessly: no.
34
malicious packages in one coordinated campaign, 384 poisoned versions
3
registries at once — npm, PyPI and crates.io, one shared payload
0
CVEs involved — version-based scanning had nothing to match against
U+200B
zero-width characters carried the instructions — invisible in any code review
The OrbitalReg defence
Every TrapDoor package came from a fresh, unknown publisher
account. Behind a curated proxy, a new dependency from an
unvetted publisher doesn't resolve until someone approves it —
and "who is asdxzxc and why does this SDK shim
ship a .cursorrules file?" is a question an
approval step forces before the assistant ever sees the bytes.
All 384 versions were new publishes from young accounts. A hold on new upstream versions buys the window in which researchers like Socket do their work — by the time the hold expires, a campaign this loud is already on the malware advisory feeds.
TrapDoor's cross-registry spread is precisely the case for one
policy plane over all formats: the moment the packages landed
in the OSV/malware feeds, a single policy change returns
403 for every one of
them — npm, PyPI and Cargo alike — instead of three separate
tools with three separate block lists.
Zero-CVE malice makes the response question harder, not easier: there is no "affected versions" list to diff against. The append-only pull log answers "which builds, which machines, which day" in one query — which is your credential-rotation scope for the first hour.
Indicators of compromise
Indicators compiled from Socket's disclosure and Phoenix Security's analysis of 22–25 May 2026. Treat these as a starting point, not a complete list, and cross-check the sources below.
Sources
Your AI assistants pull dependencies too.
Rico, the founder, walks through a curation + quarantine + pull-gate layout across your formats — one policy plane, so a cross-ecosystem campaign meets one door instead of three.