slopsquat-guard
v0.1.0
Published
Defend against slopsquatting — malicious npm packages registered under names AI coding agents commonly hallucinate.
Downloads
25
Maintainers
Readme
slopsquat-guard
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-guardRequires 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.txtandpyproject.toml→ pip. Anything else (or a non-standard filename) needs an explicit--ecosystem npm|pip. - For pip manifests,
pyproject.tomlpackages are read from the[project.dependencies](PEP 621) and[tool.poetry.dependencies](Poetry) sections; the Poetrypythonkey 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 # CRITICALcheck 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-domBehavior:
- Package specs are parsed out of the arguments (
name@version, scoped names, etc.) and each bare name is evaluated via the registry gate. - If any name is CRITICAL, the real install never runs — a clear
message explains the failure and the command exits
1. - WARNINGs are reported but never block an install.
- 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 --fastGuard 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 runningnpm 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 apackage.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.jsYou 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-hooksThis 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-commitalready exists, you're prompted before it gets overwritten (pass--forceto 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 hookWhat 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>/jsonCRITICAL — 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
searchmethod 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 shipssrc/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.
--fastskips 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-hooksinstalls it; it scans stagedpackage.jsonchanges 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) exposescheck_packageandscan_manifestso coding agents can call the guard inside their own tool-use loop before installs. - [x] pip/PyPI support —
scan/check --ecosystem pipevaluate requirements.txt and pyproject.toml dependencies against PyPI. - [ ] Guarded
pip install— wrappip install <name>the same wayslopsquat-guard installwraps npm/pnpm/yarn. - [ ] PyPI pre-commit hook — extend
init-hooksto scan stagedrequirements.txt/pyproject.tomlin addition topackage.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
