pkgproof
v0.0.1
Published
Block hallucinated & slopsquatted npm packages before they install — the pkgproof CLI.
Maintainers
Readme
pkgproof
Blocks hallucinated and slopsquatted npm & PyPI packages before they install — an explainable, agent-native guard.
⚠️ Pre-alpha (
v0.0.1) — not for production usepkgproof is under active development. Verdicts, APIs, and the bundled blocklist will change without notice. This early release reserves the name and shares progress — please don't depend on it yet.
AI coding agents invent package names, and attackers pre-register the ones models predictably hallucinate — a supply-chain attack called slopsquatting. pkgproof sits inside the install loop and returns a calibrated, explainable ALLOW | WARN | BLOCK verdict from deterministic signals — offline-first, in well under a millisecond, and it never installs or executes the package it judges.
It's not a theoretical guard. pkgproof caught [email protected] — a package carrying a postinstall infostealer dropper (behavior consistent with a DPRK campaign, by static analysis) that sat live and installable on npm for ~278 days, carrying no GHSA/CVE/OSV advisory at catch time, a prefix squat of pino (the node- prefix is a 5-character insertion, not a typo) — surfaced pre-install by the semantic judge, because its content didn't match its name.
TL;DR — quick start
Pre-alpha on npm. Published under the
alphatag asv0.0.1: install withnpm install -g pkgproof@alpha. A barenpm install -g pkgproofintentionally won't resolve yet — this is early and under active change; you can also just build from source below.
# Build from source (the real path today):
git clone https://github.com/EastVillageTech/pkgproof
cd pkgproof
pnpm install
pnpm --filter pkgproof build
# Run it:
node packages/cli/dist/cli.js check express
# Pre-alpha, published under the alpha tag:
npm install -g pkgproof@alphaA sample verdict — one PASS, one risk-signal FLAG:
# PASS — a known, popular package clears:
✓ ALLOW express — known popular package
# FLAG — a heuristic risk signal (unverified, NOT a malware claim):
┌─────────────────────────────────────────────────────────────────────────────┐
│ ⚠ WARN expres │
│ why: "expres" looks like a typo of "express" — unverified, possible squat │
│ fix: if you meant "express", install that instead │
│ ref: verify with the user before installing │
└─────────────────────────────────────────────────────────────────────────────┘A WARN is a heuristic risk signal — "this looks wrong, a human should confirm" — never an accusation. A BLOCK is reserved for confirmed-evidence catches (a name on the confirmed-malicious blocklist, or a content-verified squat like node-pino).
Why pkgproof
Commercial scanners already do basic hallucination and typosquat detection. pkgproof does not claim to out-detect them — it publishes its own failed novel-detection experiments in the open. The honest differentiation is where and how it acts:
- Pre-install, not post-install.
npm auditreports CVEs after resolution; most scanners alert on a dashboard after the fact. pkgproof blocks at the install boundary via a PATH-shim, so a maliciouspostinstallnever runs. - Agent-native and deterministic. A red-team drove real AI agents at a confirmed-BLOCK package: a weak agent installed it anyway — sometimes even after seeing an advisory. The deterministic
pkgproof guardhalted it regardless — the boundary holds no matter what the agent decides. - Explainable and open. Every verdict states WHAT, WHY, and the FIX in plain text. Apache-2.0, no cloud account, no opaque score.
- Slopsquat-focused. The net-new AI threat: an LLM hallucinates a name an attacker pre-registered. pkgproof ships a readable blocklist of 37,953 npm names — 37,650 from the OSSF malicious-packages advisory DB, plus 362 mirrored npm security-seizures, plus 2 seeded compromised-version ranges (sources overlap; 37,953 unique) — and 11,398 PyPI names. Of the npm set, pkgproof independently surfaced 12 novel live squats — 1 (
node-pino) payload-confirmed, 11 live-squat-shape awaiting deeper analysis.
See the honest comparison and threat model.
The four surfaces
One engine (@pkgproof/verify), one verdict vocabulary, four shipped, user-facing surfaces (CLI, install-guard, MCP, GitHub Action). Two more — the Cloudflare Worker and web /api/verify — are built but not deployed (see the roadmap).
1. CLI check — verify a name by hand
node packages/cli/dist/cli.js check langchain-community-helpers lodahs react╔══════════════════════════════════════════════════════════════════════════╗
║ ⛔ BLOCK lodahs ║
║ what: npm seized this name for malware — a neutralized stub ║
║ why: "lodahs" is on the confirmed-malicious blocklist — do not install ║
║ fix: install "lodash" instead — the real package ║
║ ref: pkgproof check lodahs · override (human only): --force ║
╚══════════════════════════════════════════════════════════════════════════╝
✓ ALLOW react — known popular packageExit 2 on any BLOCK. CLI reference →
2. Guard shim — the real boundary in your install loop
pkgproof guard <pm> <args...> verifies first, then runs the real package manager only if nothing blocks. Install the PATH-shim (pkgproof init shell) and even a bare npm install — including an agent's — is guarded.
node packages/cli/dist/cli.js guard npm install node-pino╔═════════════════════════════════════════════════════════════════════════════╗
║ ⛔ BLOCK node-pino ║
║ why: "node-pino" is on the confirmed-malicious blocklist — do not install ║
╚═════════════════════════════════════════════════════════════════════════════╝
⛔ pkgproof BLOCKED this install: node-pino. 'npm' was NOT run — nothing was written.Works for npm · pnpm · yarn · bun · pip · poetry · uv. Guard reference →
3. MCP — a verify tool for your agent
Register the verify_package tool (Claude Code / Cursor / VS Code) so the agent checks a name before it installs. Structured output an agent can reason over:
{
"verdict": "BLOCK",
"package": "node-pino",
"reason": "\"node-pino\" is on the confirmed-malicious blocklist — do not install",
"confidence": 0.99,
"suggestion": "pino",
"blocklist_loaded": true,
"gate_ok": true
}gate_ok: false means signature coverage is degraded — an ALLOW is then not malware-verified. The measured lesson: the MCP hint informs the agent; the guard enforces. Use both. MCP reference →
4. GitHub Action — gate your CI
A composite action scans your lockfile (pkgproof scan --lockfile --json), emits SARIF 2.1.0 plus a step summary and inline annotations, and exits the scan contract code (BLOCK → 2) so a bad dependency fails the build.
- uses: EastVillageTech/pkgproof@v1
with:
paths: .
strict: true
sarif: trueInputs: paths · working-directory · strict · sarif · sarif-file. Action reference →
What it checks
| Detector | What it flags | How |
|---|---|---|
| Existence | A name that 404s on the registry — a pure hallucination | Live registry lookup; newness is a gate, never a conviction |
| Confusion | Typosquats & look-alikes (lodahs → lodash) | Damerau-Levenshtein distance + homoglyph / NFKC folding against 297 npm / 138 PyPI anchor names |
| Signature blocklist | Confirmed-malicious names, exact + version-aware | 37,953 npm (37,650 OSSF advisory + 362 mirrored seizures + 2 seeded ranges; overlap → 37,953 unique) + 11,398 PyPI names; download-continuity checks |
| Living corpus | Names today's models actually hallucinate | 410 catches and counting, mined by an always-running miner |
| Semantic judge | Aged look-alikes whose content betrays the name | LLM name↔content coherence (what caught node-pino) — downgrade-only, opt-in, needs a local claude CLI |
Rules run in a fixed order (R0..R3 hard rules, then a 5-signal soft band), so the same name always gets the same verdict.
How it decides — and what it can't do
pkgproof is deliberate about the line between a proven catch and a guess:
- BLOCK is evidence-backed: an exact hit on the confirmed-malicious blocklist, or a content-verified squat with a cited reason. These are the only verdicts framed as malware.
- WARN is a heuristic risk signal — "this looks like a typo / is suspiciously new / thin" — explicitly unverified. It asks a human to confirm; it is never an accusation.
- ALLOW means "nothing tripped," not "audited safe." A brand-new legitimate package is never blocked just for being new.
What it can't do: it is not a full malware scanner or a sandbox detonator. It does not execute or install anything it judges (a hard invariant). Deterministic verdicts (blocklist + heuristics) are fully offline, with no account. The optional semantic judge — the aged-look-alike edge that surfaced node-pino — requires a local claude CLI (account + network); it is downgrade-only and off by default, so CI stays fully offline and deterministic. It reasons over registry metadata, names, and published content — not runtime behavior. Treat it as a fast, explainable first gate in front of your package manager, layered with, not replacing, your other supply-chain controls.
Proof: the node-pino catch
pkgproof caught [email protected] on npm — a package that was clean by every metadata signal and malicious in its content, a prefix squat of the popular pino logger (node- prepended — a 5-character insertion, not a typo).
- Caught pre-install on 2026-06-22 by the semantic judge, not typo-distance: its content didn't match its name —
mainpointed at./lib/winston.js, its dependencies were winston's, and it shipped apostinstalldropper. The judge scored it 0.95. - Analyzed statically and offline (decrypted with
openssl, never executed): apostinstallspawns a detached child that AES-256-CBC-decrypts a bundledlicense.listandeval()s a ~105 KB obfuscated infostealer targeting crypto wallets, the macOS Keychain, and browser credential stores. - Consistent with the DPRK "Contagious Interview" / BeaverTail campaign (C2 port
:1224, dropper structure, targeting) — stated as consistent with, never a definitive attribution. No C2 host IP was reconstructed. - Dwell time: ~278 days (9.3 months) live and installable until pkgproof surfaced it.
[email protected]specifically carried no GHSA/CVE/OSV advisory at catch time (now recorded in OSSF via our PR #1340, merged 2026-07-05). A sibling variant,pino-node, from the same campaign was already flagged on Snyk — pkgproof caught a different, still-unlisted variant of a known campaign. - Reported to npm 2026-07-02 (malware-removal report); the OSSF
malicious-packagesPR (#1340) is now merged. As of last check, npm has restricted the package page (403) but the tarball is still live and installable — takedown is npm's action and is not yet confirmed. Do not install it.
Full write-up with IOCs (no runnable payload): case study →.
Config & exit codes
Exit codes (the agent / CI contract):
| Code | Meaning |
|---|---|
| 0 | all ALLOW (or WARN without --strict) |
| 1 | --strict and a WARN was seen (or a usage / internal error) |
| 2 | a BLOCK was seen — installs MUST stop |
--force overrides a BLOCK (exit 0) and logs it to stderr — explicit and auditable. --json emits machine-readable output. A per-project .pkgproofrc.json can downgrade a matching heuristic WARN/BLOCK to ALLOW, but never a confirmed-malware BLOCK.
Useful commands: pkgproof status (blocklist freshness + count) · pkgproof demo (a hallucination, a typosquat, a real package) · pkgproof init shell (install the guard PATH-shim).
Links
- Repository & full docs: https://github.com/EastVillageTech/pkgproof
- CLI reference: https://github.com/EastVillageTech/pkgproof/blob/main/docs/cli.md
- Install guard: https://github.com/EastVillageTech/pkgproof/blob/main/docs/install-guard.md
- MCP integration: https://github.com/EastVillageTech/pkgproof/blob/main/docs/mcp.md
- GitHub Action: https://github.com/EastVillageTech/pkgproof/blob/main/docs/github-action.md
- How detection works: https://github.com/EastVillageTech/pkgproof/blob/main/docs/how-detection-works.md
- Threat model: https://github.com/EastVillageTech/pkgproof/blob/main/docs/threat-model.md
- node-pino case study: https://github.com/EastVillageTech/pkgproof/blob/main/docs/case-study-node-pino.md
- Security policy: https://github.com/EastVillageTech/pkgproof/blob/main/SECURITY.md
Apache-2.0 · pkgproof never installs or executes the package it judges.
