npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

slopsquat-guard

v0.1.0

Published

Defend against slopsquatting — malicious npm packages registered under names AI coding agents commonly hallucinate.

Downloads

25

Readme

slopsquat-guard

Tests

Defend against slopsquatting — the practice of registering malicious packages under names that AI coding agents commonly hallucinate. The guard covers the npm and pip/PyPI ecosystems.

When an agent (or a tired human) invents a plausible-looking package name, npm install (or pip install) happily installs whatever is out there under that name — which may be empty, abandoned, or worse: a malicious package an attacker registered days earlier to catch that exact install. Research on hallucinated package names finds that ~43% of the names recur across repeated prompts with the same agent, which is why attackers can pre-register them in bulk. A name that doesn't exist on the registry is the single strongest signal that someone may be about to get slopsquatted.

Install

npm install -g slopsquat-guard

Requires Node.js 18+. Runtime dependencies: commander (CLI), @modelcontextprotocol/sdk + zod (MCP server), @iarna/toml (pyproject.toml parsing).

Usage

Scan a manifest (package.json, requirements.txt, or pyproject.toml)

The file type is auto-detected from the file name/extension:

slopsquat-guard scan                         # scans ./package.json (npm)
slopsquat-guard scan path/to/package.json    # npm
slopsquat-guard scan requirements.txt        # pip
slopsquat-guard scan pyproject.toml          # pip
slopsquat-guard scan --ecosystem pip my-deps.txt  # override detection
slopsquat-guard scan --json                  # machine-readable output for CI
slopsquat-guard scan --fail-on-warning
slopsquat-guard scan --fast                  # skip the expensive mashup heuristic
  • Auto-detection: package.json → npm; requirements.txt and pyproject.toml → pip. Anything else (or a non-standard filename) needs an explicit --ecosystem npm|pip.
  • For pip manifests, pyproject.toml packages are read from the [project.dependencies] (PEP 621) and [tool.poetry.dependencies] (Poetry) sections; the Poetry python key is ignored (it's the interpreter version, not a package).

Exit codes:

  • 0 — no CRITICAL results (WARNINGs are allowed unless --fail-on-warning)
  • 1 — at least one CRITICAL result, or a WARNING with --fail-on-warning

Check a single package name

This is the shape a coding agent would call before npm install <name> or pip install <name>:

slopsquat-guard check left-pad
slopsquat-guard check imaginary-http-framework   # CRITICAL: doesn't exist on npm
slopsquat-guard check react-codeshift --json
slopsquat-guard check requests --ecosystem pip
slopsquat-guard check totally-fake-pypi-pkg --ecosystem pip  # CRITICAL

check defaults to the npm ecosystem; pass --ecosystem pip to check a package against PyPI.

Exit code 1 when the package is CRITICAL, 0 otherwise.

Guarded install (install)

Wrap npm install / pnpm add / yarn add so the guard checks every package name before the real installer runs:

slopsquat-guard install npm left-pad express
slopsquat-guard install npm install express@^4.19.2 --save-dev
slopsquat-guard install pnpm add @scope/[email protected]
slopsquat-guard install yarn add react react-dom

Behavior:

  1. Package specs are parsed out of the arguments (name@version, scoped names, etc.) and each bare name is evaluated via the registry gate.
  2. If any name is CRITICAL, the real install never runs — a clear message explains the failure and the command exits 1.
  3. WARNINGs are reported but never block an install.
  4. All other arguments (flags, version ranges) are passed through verbatim to the real package manager, which runs as a child process with inherited stdio.

To consciously override a CRITICAL block (e.g. a confirmed false positive), pass --force (or -f) — it is consumed by the guard and not forwarded:

slopsquat-guard install npm some-hallucinated-name --force

--fast is also consumed by the guard (like scan --fast, it skips the expensive mashup-name heuristic) and is never passed through to the real installer:

slopsquat-guard install npm express --fast

Guard your agent's own tool calls (MCP server)

This is the core differentiator versus CI-only defenses: it puts the check inside the agent's tool-use loop, before an install ever executes.

When a coding agent builds up its own dependency list and then runs npm install <name> to satisfy a task, there is often no human watching to catch a hallucinated name before it executes. Traditional slopsquatting defenses live in CI, which only sees the lockfile after the install — and by then a malicious package may already have run its postinstall script on every machine that pulled it.

The MCP server closes that gap by exposing the same evaluation logic as two tools an agent can call as part of deciding what to do:

  • check_package — evaluate a single npm package name. Call this before running npm install <name> for any package the user didn't explicitly tell you to install, especially if you're not fully certain the name is correct. If the result is CRITICAL (the name doesn't exist on the registry), do not install it — surface the finding and ask the user for confirmation.
  • scan_manifest — scan every dependency in a package.json. Call this after generating or modifying a manifest and before committing it, to catch any hallucinated dependency before it reaches a lockfile or a reviewer's eyes.

The server speaks MCP over stdio and is published as the slopsquat-guard-mcp binary.

Setup: register the server with Cline

In Cline, open the MCP server settings and add the following entry (or add it to your client's equivalent MCP configuration file):

{
  "mcpServers": {
    "slopsquat-guard": {
      "command": "npx",
      "args": ["slopsquat-guard-mcp"]
    }
  }
}

Make sure slopsquat-guard is installed somewhere on the system running the agent (npm install -g slopsquat-guard), since npx resolves the binary from the local npm cache or the global install.

The same setup works for any MCP-capable editor/CLI client (Claude Code, Cursor, VS Code via a MCP client extension, etc.) — the server is a standard MCP stdio server, so no slopsquat-guard-specific integration is needed.

Verifying the connection

After registering the server, restart the client and confirm the two tools (check_package, scan_manifest) appear in its tool list. You can also verify the server manually from a terminal — it starts, waits for an MCP client on stdin/stdout, and exits if stdin closes without having connected:

node bin/mcp.js

You should see no output and the process should stay alive (press Ctrl-C to stop it). A full manual round-trip requires a real MCP client handshake; the test suite (npm test) covers the full tools/list and tools/call request path over an in-memory transport.

Install the pre-commit hook (init-hooks)

Stop bad packages from even getting into your lockfile:

git init                 # if you don't have a repo yet
slopsquat-guard init-hooks

This copies hooks/pre-commit into .git/hooks/pre-commit and makes it executable. From then on, whenever package.json is part of the commit's staged changes, the hook runs slopsquat-guard scan package.json and blocks the commit (non-zero exit) on any CRITICAL result. Commits that don't touch package.json skip the scan entirely, so unrelated commits stay fast.

Details:

  • If .git/hooks/pre-commit already exists, you're prompted before it gets overwritten (pass --force to skip the prompt: slopsquat-guard init-hooks --force).
  • If the current directory isn't a git repository, the command tells you and exits 1.
  • To bypass the hook for a single commit, use git commit --no-verify.

Programmatic API

import { scanPackageJsonFile, evaluatePackage } from 'slopsquat-guard';
import { guardInstall } from 'slopsquat-guard/src/install.js';
import { installHook } from 'slopsquat-guard/src/hooks.js';

await evaluatePackage('left-pad');              // { name, severity, reasons }
await scanPackageJsonFile('package.json');      // { path, deps, results }
await guardInstall('npm', ['install', 'left-pad']);
await installHook();                            // installs pre-commit hook

What the checks do (and why)

Each dependency is evaluated in two phases, in the same way for both ecosystems:

1. The hard gate: does the package exist? (deterministic)

  • npm: GET https://registry.npmjs.org/<name>

  • pip: GET https://pypi.org/pypi/<name>/json

  • CRITICAL — doesn't exist. This is the core slopsquatting signal. The name may have been hallucinated, and an attacker may have already registered it (or could, at any moment) to catch the next install. The check is a hard 404 from the registry itself — no heuristics involved.

  • CRITICAL — lookup failed. Network error, 5xx, timeout. We can't verify the package, so we refuse to bless it.

  • Lookups are cached in-process; a name is never queried twice per run.

If the package exists, the secondary heuristics below run.

2. Secondary suspicion scoring (heuristic, only for existing packages)

| Heuristic | npm | pip/PyPI | What it flags | |---|---|---|---| | Recency | time.created from the registry doc | earliest upload_time across release files | Package registered within the last 30 days | | Downloads | https://api.npmjs.org/downloads/point/last-week/<name> | https://pypistats.org/api/packages/<name>/recent (third-party mirror) | Under 50 weekly downloads, or a 404/error from the downloads/stats API (common for brand-new packages) | | Mashup name | npm search (/-/v1/search?text=<segment>&size=5&popularity=1.0, one call per segment) | bundled static list of ~200 popular PyPI packages (offline — see below) | At least two distinct name segments each echo a different popular real package (e.g. react-codeshift echoes both react and jscodeshift; mashup-numpy-requests echoes both numpy and requests) |

Notes:

  • The mashup check splits names on kebab-case, snake_case, and camelCase, and matches segments against popular real package names via exact substring or Levenshtein distance ≤ 2.
  • The mashup check is the most expensive (multiple network calls on npm), so it only runs when the package already looks new or unpopular.
  • PyPI has no official JSON search API — the XML-RPC search method was deprecated in 2022 and removed in 2023. Two officially recommended alternatives exist: downloading/indexing the ~600k-name simple-index JSONL catalog snapshot (far too heavy per scan), or querying the public BigQuery dataset (needs credentials). The guard instead ships src/pypi-popular.js, a bundled list of ~200 mainstream PyPI packages, and matches name segments against it locally — zero network calls, never rate-limited, works offline. Tradeoff: the list is static and only covers mainstream packages (which are exactly the ones agents most often echo).
  • The pypistats.org download count is a third-party stats mirror, not an official PyPI API; failures there never abort a scan — they degrade to a "near-zero downloads" WARNING.
  • --fast skips the mashup heuristic entirely.

If any heuristic fires, the package is WARNING with the reasons listed. If none fire, it's OK.

Roadmap

  • [x] Pre-commit hook — init-hooks installs it; it scans staged package.json changes and blocks on CRITICAL results.
  • [x] Guarded install — slopsquat-guard install npm|pnpm|yarn ... evaluates names before the real installer runs.
  • [x] MCP server mode — an MCP stdio server (slopsquat-guard-mcp) exposes check_package and scan_manifest so coding agents can call the guard inside their own tool-use loop before installs.
  • [x] pip/PyPI support — scan/check --ecosystem pip evaluate requirements.txt and pyproject.toml dependencies against PyPI.
  • [ ] Guarded pip install — wrap pip install <name> the same way slopsquat-guard install wraps npm/pnpm/yarn.
  • [ ] PyPI pre-commit hook — extend init-hooks to scan staged requirements.txt / pyproject.toml in addition to package.json.

Development

npm install
npm test          # fully offline — global.fetch is mocked
node bin/cli.js scan examples/package.json    # npm end-to-end demo
node bin/cli.js scan examples/requirements.txt  # pip end-to-end demo
node bin/cli.js scan examples/pyproject.toml    # pip (pyproject) demo
node bin/cli.js check requests --ecosystem pip  # single PyPI name check
node bin/cli.js install npm express           # guarded install demo
node bin/mcp.js                               # start MCP server (stdio)

License

MIT