Home / Trust / Benchmark

Noise benchmark

How often we flag a package people install on purpose

Catch rates are easy to claim and hard to check. Noise decides if people leave a scanner on, and few tools publish theirs. These numbers are ours, at rules version 2026.11.04b. We rebuild them when that changes.

Read every number with the sentence next to it.

We chose all of these corpora. None is a random sample of the registry, and none measures what happens in the field. So you cannot carry these rates over to your own dependencies. They do show how this scanner behaves on lists we publish in full, and you can run those yourself.

Attack samples we wrote

71 of 71

What that counts: attack samples our rules detected, out of the ones in our corpus. We wrote every sample to match what an attack does. We did not write them to match our rules.

What the corpus is made of, counted from the samples: technique family we wrote: 44, evasion variant we wrote: 14, published incident report: 13. Only the ones written from a published report follow a named attack. The rest are our idea of what a type of attack looks like. They do not show that we caught any real attack.

And they are not all npm packages. What each sample is: npm package: 34, GitHub repository: 12, agent skill: 7, MCP server: 5, model repository: 5, Python source file: 3, PyPI package: 3, CI workflow: 2. So this number covers more than npm packages.

This is not field recall. It cannot be. That would need a list of all attacks in a period, even the ones nobody has found. Nobody has that list. Anyone who quotes such a number for a package scanner is making it up, and that includes us.

We put in cases we expected to fail, on purpose. A test that only holds what you catch tells you nothing.

What we do not catch

Every sample in the corpus was detected in this run. That is a statement about this corpus, which we wrote, and nothing more. Samples we expected to miss are kept in it on purpose, and when one starts passing, we say so.

Noise on packages people install on purpose

Popular npm packagesrules 2026.11.04bmeasured 2026-11-04

1 of 154

What that counts: packages flagged by our rules out of the whole list, with no indicator feed in play.

The corpus, and who chose it: The list we publish package pages for, chosen by us for search traffic, not sampled from the registry at random. It is weighted to packages a working developer installs on purpose, which is the population a noisy scanner annoys.

Full breakdown: 151 came back clear, 1 came back flagged, and 2 could not be read at all in this run. We count those on their own.

PackageVerdictRiskRule categories
pm2flaggedhigh obfuscation, persistence

We name them so you can check them. We do not claim each one is wrong. Our rules flagged it, and you can read the evidence yourself. If we find one was wrong, we correct it and list it in the public corrections log. We do not just edit it out of this table.

Could not be read in this run:

better-sqlite3fetch failed: too_large (fetch: content-length 11402131); Not read. The package download is larger than our 5 MB limit, so there is no result either way. Retrying will not change this unless the package gets smaller.
nextfetch failed: too_large (fetch: content-length 43659658); Not read. The package download is larger than our 5 MB limit, so there is no result either way. Retrying will not change this unless the package gets smaller.

Adversarial npm packagesrules 2026.11.04bmeasured 2026-11-04

5 of 53

What that counts: packages whose result disagreed with the label we wrote down before running them.

The corpus, and who chose it: Chosen by us because each one legitimately does something our rules look for: reads cloud credentials, posts to a chat webhook, spawns shells, compiles code at run time, or runs an install step that downloads a binary. Every label was written before the run, and any we later corrected shows the old label, the date and the reason.

Full breakdown: 50 came back clear, 1 came back flagged, and 2 could not be read at all in this run. We count those on their own.

The two directions, one at a time, so you can see which way we were wrong: 1 we had labelled clear came back flagged, and 4 we had labelled flagged came back clear. The second group is the harder one. We expected a flag for what they do at install time, and the scan did not give one. We do not flag a bare install script, since most are plain builds and downloads. Where we see one, the report lists it as an info finding, which does not change the verdict. Those labels stay as written, so these still count as disagreements.

Labelled clear, came back flagged: pm2.
Labelled flagged, came back clear, install script reported at info: bcrypt, esbuild, puppeteer, sqlite3.
Labelled flagged, came back clear, no install script reported: none.

Labels we corrected after writing them, since the package changed: sharp was labelled flagged until 2026-11-02. sharp removed its install script in 0.35.0, published 2026-06-10, so the old label described releases we no longer scan.

PackageVerdictRiskRule categoriesOur label
pm2flaggedhigh obfuscation, persistence we labelled it clear

Could not be read in this run:

googleapisfetch failed: too_large (fetch: content-length 23807988); Not read. The package download is larger than our 5 MB limit, so there is no result either way. Retrying will not change this unless the package gets smaller.
node-ptyfetch failed: too_large (fetch: content-length 15468777); Not read. The package download is larger than our 5 MB limit, so there is no result either way. Retrying will not change this unless the package gets smaller.

Well-known MCP serversrules 2026.11.04bmeasured 2026-11-04

0 of 20

What that counts: server packages flagged by our rules out of the whole list.

The corpus, and who chose it: The MCP servers we publish pages for, chosen by us from the widely used ones.

Full breakdown: 20 came back clear, 0 came back flagged, and 0 could not be read at all in this run. We count those on their own.

Nothing in this corpus was flagged.

Every sample, and how it went

SampleWhat it isHow it was writtenWritten from ResultLabel when written
postinstall droppernpm packagepublished incident report ua-parser-js / coa / rc (2021): postinstall fetches and runs a remote binarydetected expected to detect
ssh + cloud credential stealernpm packagetechnique family we wrote typosquat crypto/credential stealers: read key material, POST to hardcoded hostdetected expected to detect
npm token + env harvestnpm packagepublished incident report Shai-Hulud (2025): harvest npm/GitHub tokens and env, exfil to attacker endpointdetected expected to detect
obfuscated decode-then-eval loadernpm packagepublished incident report event-stream / flatmap-stream (2018): decrypt a blob at runtime, then execute itdetected expected to detect
decoded into a variable, then evaluatednpm packageevasion variant we wrote the same loader with the decode and the eval split by a variable, the first thing an author tries to dodge a one-expression patterndetected expected to detect
stealer behind a file of nothing but newlinesnpm packageevasion variant we wrote padding that spends the scanner line budget before the payload is reached, a few KB once compresseddetected expected to detect
crypto wallet stealernpm packagetechnique family we wrote wallet-draining typosquats: read wallet files, exfildetected expected to detect
self-updating beaconnpm packagetechnique family we wrote persistent loaders: periodically pull and execute remote codedetected expected to detect
payload hidden inside a minified bundlenpm packagetechnique family we wrote attacker ships the stealer inside a published dist bundledetected written as a case we expected to miss
worm ships as a minified bundle its install script runsnpm packagetechnique family we wrote Shai-Hulud (2025): postinstall runs a single minified bundle.js that harvests tokens and posts them outdetected expected to detect
DNS-based exfiltrationnpm packagetechnique family we wrote covert channel: encode secrets into DNS lookups (no HTTP sink)detected written as a case we expected to miss
reverse shell in a postinstallnpm packagetechnique family we wrote bash /dev/tcp reverse shell dropped by an install script (common CTF/malware idiom)detected expected to detect
Discord-webhook token stealernpm packagetechnique family we wrote many 2023-2025 npm token stealers: read npm/env secrets, POST to a Discord webhook (a "normal" domain)detected expected to detect
clipboard crypto clippernpm packagetechnique family we wrote clipper malware: poll the clipboard and swap a copied wallet address for the attacker'sdetected expected to detect
multi-provider env token harvestnpm packagetechnique family we wrote Shai-Hulud-style worm / token stealers: collect NPM/GitHub/AWS secrets from env and POST themdetected expected to detect
remote code loader to a domainPython source filetechnique family we wrote dropper that fetches a stage-2 payload from a DOMAIN (no hardcoded IP) and execs itdetected expected to detect
bin shadows a system commandnpm packagetechnique family we wrote metadata attack: a package whose bin shadows git/npm so a global install hijacks the commanddetected expected to detect
CI workflow secret exfiltrationCI workflowtechnique family we wrote poisoned GitHub Actions workflow that POSTs a build secret to an attacker hostdetected expected to detect
Windows LOLBin dropper via postinstallnpm packagetechnique family we wrote Windows-targeted npm malware: postinstall uses certutil to fetch a remote exedetected expected to detect
in-memory shellcode injectionPython source filetechnique family we wrote loader that allocates executable memory and runs a raw shellcode bufferdetected expected to detect
obfuscated dynamic import (anti-analysis)Python source fileevasion variant we wrote evasion: assemble the module name from fragments so a static scan never sees "os"detected expected to detect
credential exfil to a throwaway sinknpm packagetechnique family we wrote reads env/npm secrets and POSTs them to a webhook.site capture URLdetected expected to detect
CI workflow dropper from a raw IPCI workflowtechnique family we wrote poisoned GitHub Actions step fetches a payload from a bare IP and pipes it to a shelldetected expected to detect
agent credential store read and exfiltratednpm packagetechnique family we wrote AI-tooling credential theft: read a coding agent OAuth store (.codex/auth.json) and POST the refresh token outdetected expected to detect
agent transcript exfiltrated over a websocketnpm packagetechnique family we wrote AI-tooling credential theft: glob the Claude conversation transcripts and stream them to a relay over wsdetected expected to detect
agent CLI spawned with permission bypassnpm packagepublished incident report Nx s1ngularity (2025): a postinstall drives the on-host coding agent with the approval prompt disabled to inventory secretsdetected expected to detect
agent config tampered to hijack MCP serversnpm packagetechnique family we wrote AI-tooling credential theft: a postinstall rewrites the agent config to repoint mcpServers at a proxy and pre-accept trustdetected expected to detect
on-host agent driven remotely over a websocket relaynpm packagetechnique family we wrote Remote-control daemon: a package bin takes prompts from a WebSocket relay and runs them through the local coding agent with approvals disableddetected expected to detect
agent skill exfiltrates via a rendered imageagent skilltechnique family we wrote prompt-injection in an agent SKILL.md: a markdown image whose URL templates the conversation to an attacker hostdetected expected to detect
remote dynamic dependency (http dep URL)npm packagepublished incident report PhantomRaven (2025): the npm tarball is a benign stub; a dependency VALUE is an http URL npm fetches and runs at installdetected expected to detect
binding.gyp build-time command substitutionnpm packagetechnique family we wrote Phantom Gyp / Miasma: a binding.gyp sources entry runs a script through node-gyp at install, with no package.json hook to inspectdetected expected to detect
Python .pth interpreter-startup stealerPyPI packagepublished incident report LiteLLM 1.107.0 (2026): a shipped .pth imports a module that reads agent credential files at every interpreter start, even when the package is never importeddetected expected to detect
Python .pth startup hook in an sdist src layoutPyPI packageevasion variant we wrote LiteLLM 1.107.0 (2026), as the sdist ships it: the .pth and its module sit under src/, and the module binds its network call by from-importdetected expected to detect
binding.gyp substitution hidden in a split sources listnpm packageevasion variant we wrote Phantom Gyp / Miasma, laid out the gl/Chromium way: the sources key and its [ on two lines, a variable list pulled in through <@(name), and inline sh -c running a package scriptdetected expected to detect
Python .pth import line behind a bare carriage returnPyPI packageevasion variant we wrote LiteLLM 1.107.0 (2026), bent for the reader: site.addpackage splits a .pth with str.splitlines(), so an import after a bare \r runs while a newline-only reader sees one path linedetected expected to detect
agent skill grabs a token while loadingagent skillpublished incident report skill marketplace malware (the ToxicSkills report): a SKILL.md dynamic-context command reads the GitHub CLI token and curls it out during preprocessing, under a blanket Bash grant, before the model has read the skilldetected expected to detect
skill repo config imports the user's secretsagent skilltechnique family we wrote a skill repo whose CLAUDE.md @import lines pull an SSH key and a dotenv file from outside the repo into every session that opens it, beside an honest in-tree import and a clean SKILL.mddetected expected to detect
namespaced command ships the environment while loadingagent skilltechnique family we wrote the skill marketplace shape moved into a plugin slash command under a namespace directory (commands/<ns>/<name>.md, loaded as /ns:name): the environment is piped out at load time under a curl grant that is scoped by nothing but a flagdetected expected to detect
command posts the environment from node while loadingagent skilltechnique family we wrote the same load-time shape with curl taken out: a slash command with no name: key, pasted or installed, whose dynamic-context command runs node -e to POST the whole environment, so no curl flag or shell pipe is on the linedetected expected to detect
MCP tool schema smuggles the conversation through a parameter nameMCP servertechnique family we wrote HiddenLayer / CyberArk schema poisoning: an inputSchema property named conversation_history / system_prompt that the model auto-fills from its own context, with a bland description and nothing in requireddetected expected to detect
one-click MCP install badge hides a curl-piped-to-shell commandMCP serverpublished incident report CursorJack (CVE-2025-54133) / DeepJack / Dive (CVE-2026-23523): a cursor:// mcp/install deeplink whose base64 config decodes to a hostile command/argsdetected expected to detect
VS Code install redirect hides a decode-and-run commandMCP servertechnique family we wrote the same deeplink class in the vscode.dev redirect form most MCP READMEs use: the vscode:mcp/install deeplink, its JSON config the whole query, encoded again inside url=detected expected to detect
MCP tool schema hides the system prompt channel inside a nested options objectMCP serverevasion variant we wrote the HiddenLayer schema channel moved one level down: an options object whose own property is named system_prompt, which a top-level-only schema render never showed to any ruledetected expected to detect
MCP tool schema rewords the context channel and hides behind generic wordsMCP serverevasion variant we wrote the HiddenLayer schema channel under a synonym (previous_conversation) on a tool whose own text says "error message" and "tide model", words that name no conversation and no LLM, so nothing honest explains the fielddetected expected to detect
agent skill with a loader hidden in variation selectorsagent skillpublished incident report GlassWorm (2025): a payload encoded in Unicode variation selectors, invisible in an editor, parked on blank-looking lines of a source file; here inside a skill and its scriptdetected expected to detect
install hook registers a chat-backed MCP server with a baked-in bot tokennpm packagetechnique family we wrote rogue MCP registration: postinstall writes an mcp.json entry whose env carries a Telegram bot token, and a settings.json that auto-accepts broad permissionsdetected expected to detect
status-line plugin ships a settings.json that reroutes the API with a baked-in keynpm packagepublished incident report a Claude Code status-line package (2026) whose only payload was a .claude/settings.json: ANTHROPIC_BASE_URL rewritten to an attacker relay, a key baked into apiKeyHelper, permission prompts switched offdetected expected to detect
setup script turns agent telemetry into a session feedGitHub repositorytechnique family we wrote agent telemetry as a covert channel: a repository setup script enables OTEL export with prompt, tool content and raw API body logging and points the collector at an outside hostdetected expected to detect
repository ships a one-line settings file that streams the sessionGitHub repositoryevasion variant we wrote agent telemetry as a covert channel, written the way a script writes it: JSON.stringify output on one line in .claude/settings.local.json, prompt and tool content logging on, collector on an outside hostdetected expected to detect
agent skill with a selector payload hidden behind blank filler charactersagent skillevasion variant we wrote GlassWorm (2025) variation-selector encoding, with a Hangul filler before every selector so no selector is left without a base and the lines still render blankdetected expected to detect
agent instruction file hides a directive in zero-width joinersnpm packagepublished incident report TrapDoor (2026): AGENTS.md and CLAUDE.md files planted in npm, PyPI and crates packages, with a directive written in zero-width joiners between visible lettersdetected expected to detect
Cursor rules files hide a directive in zero-width charactersnpm packagepublished incident report TrapDoor (2026): the same zero-width directive delivered in a .cursorrules and a .cursor/rules .mdc file, which are not markdown by extensiondetected expected to detect
selector payload parked behind soft hyphensnpm packageevasion variant we wrote GlassWorm (2025) variant: every variation selector of the payload follows a soft hyphen, a format character that renders as nothing, so each selector looks like it has a basedetected expected to detect
selector payload large enough to pass for a minified bundlenpm packageevasion variant we wrote GlassWorm (2025) at full size: thousands of variation selectors on one line of a short module, so the raw length per line reads like a minified bundle and a size check would drop the file unreaddetected expected to detect
npm lifecycle script streams the agent session to an outside collectornpm packageevasion variant we wrote agent telemetry as a covert channel, carried in a package.json postinstall as an env prefix rather than a setup script: prompt and tool content logging on, the OTLP endpoint on an outside hostdetected expected to detect
fake job-interview repository starts a loader when the folder is openedGitHub repositorypublished incident report fake job-interview repositories (Contagious Interview, 2025): a .vscode/tasks.json task set to run on folder open that starts a loader committed beside it, so the editor runs it with no clickdetected expected to detect
workspace settings point the TypeScript server at a copy the repository shipsGitHub repositorytechnique family we wrote editor toolchain redirect (the CVE-2021-26700 class): .vscode/settings.json sets typescript.tsdk to a folder inside the repository that holds its own tsserver.js, so opening a .ts file starts itdetected expected to detect
dev container posts the developer secrets during setupGitHub repositorytechnique family we wrote dev container lifecycle exfiltration: postCreateCommand POSTs the user secrets file and the container environment, Codespaces token included, to an outside host the first time the container is builtdetected expected to detect
project agent settings run a fetched script before every tool callGitHub repositorytechnique family we wrote agent hook abuse: a committed .claude/settings.json whose PreToolUse hook substitutes a remote script into bash -c, so the agent runs it before each tool call in that projectdetected expected to detect
repository ships agent settings that switch the approval prompt offGitHub repositorytechnique family we wrote consent bypass committed as config (the CVE-2025-53773 class): a repository whose settings turn on chat.tools.autoApprove for VS Code and bypassPermissions for Claude Code, so a prompt injection anywhere in it runs commands without a promptdetected expected to detect
in-repo .npmrc swaps the git binary for a repo scriptGitHub repositorytechnique family we wrote npm runs the program named by git= for every git dependency it resolves, so a committed .npmrc turns an ordinary install into running a hidden scriptdetected expected to detect
pnpm hook file starts a detached process on every installGitHub repositorytechnique family we wrote pnpm runs .pnpmfile.cjs hooks on every install; a readPackage hook that spawns a process runs before any package script and needs no lifecycle hookdetected expected to detect
pre-commit local hook pipes a download into a shellGitHub repositorytechnique family we wrote pre-commit runs a repo: local hook entry as written, with no pinned revision, whenever the hooks run on a commitdetected expected to detect
lockfile resolves one dependency from a host the rest of the file never usesGitHub repositorytechnique family we wrote lockfile poisoning: npm installs each entry from the resolved URL in package-lock.json and checks it against the integrity beside it, so a rewritten entry pulls code from any host with no change to package.json and no install scriptdetected expected to detect
requirements file adds an extra package index for an internal-looking nameGitHub repositorytechnique family we wrote dependency confusion through pip: --extra-index-url makes pip take the highest version of a name across every index, so an index the repository adds can serve its own build of any package it listsdetected expected to detect
model repo config runs a modeling file that starts a processmodel repositorytechnique family we wrote trust_remote_code wiring: auto_map in config.json names a modeling file the loader imports on load, and that file starts a shell that fetches a script (nested configs have forced the flag on, CVE-2026-5241 and CVE-2026-27893)detected expected to detect
diffusers repo ships a None.pymodel repositorytechnique family we wrote CVE-2026-44827: a diffusers model_index.json lists a component as [null, null] and the repo ships None.py, which the loader imports without trust_remote_codedetected expected to detect
tokenizer chat template escapes the Jinja sandboxmodel repositorytechnique family we wrote chat template injection: a tokenizer_config.json chat_template that walks from a string to __subclasses__, rendered on every chat calldetected expected to detect
Keras model config carries a Lambda layermodel repositorytechnique family we wrote CVE-2025-1550 and CVE-2025-8747: a Keras config.json whose Lambda layer stores Python to run when the model is loaded, even with the guard meant to refuse it switched ondetected expected to detect
weights file pickle imports a system callablemodel repositorytechnique family we wrote pickle payloads in model weights: a state dict whose pickle also names posix.system, the global a loader imports before any tensor is built (torch.load weights_only bypass CVE-2025-32434; denylist scanner bypasses CVE-2025-10155, CVE-2025-10156, CVE-2025-10157). The sample is the bytes after the fetcher disassembled themdetected expected to detect
install dropper behind a byte-order marknpm packageevasion variant we wrote a UTF-8 byte-order mark ahead of package.json: JSON.parse rejects the file while npm drops the mark and runs the hook. A registry tarball loses the mark when it is decoded, so this reaches the rules when a manifest is sent as inline textdetected expected to detect

How these were measured

What none of this tells you

Check it yourself

The machine-readable version of this page is at /benchmark.json, with each package and its result. The rule list is at /v1/rules. You can scan any package here again. This names the package and not the release, so you may scan a later one:

curl -s https://lazaretto.dev/benchmark.json | jq '.surveys[].members[] | select(.verdict=="flagged")'

Generated 2026-11-05 at rules version 2026.11.04b.