pkg-guardis an alerting layer — a day-zero supply-chain checker for npm + PyPI, distributed as a local MCP server for AI coding agents.
pkg-guardruns locally vianpx. When your AI agent (Claude Code, Cursor, Claude Desktop) is about to install a package, it callscheck_packagefirst and surfaces structured evidence — publish-time anomalies, lifecycle script changes, Scorecard signal, OSV advisories — so the agent (and you) can stop and look before installing.It is not a package-manager enforcement gate, a malware scanner, a sandbox, or a guarantee. See Limitations.
Most supply-chain incidents have a small day-zero window: the malicious version lives for hours before defenses catch up. Confidence-based signals (provenance, signatures) and slow-feedback signals (CVEs, advisory databases) usually arrive too late.
pkg-guard looks for contextual anomalies that don't need to know the specific malware:
- A1 — Version age: package published moments ago
- A2 — New lifecycle scripts /
.pthfiles: install-time code injection - A3 — Size delta: unusual jump in published artifact size
- A4 — Maintainer change: new publisher this version
- A5 — Author change: new author this version
- B1-B4 — Baseline signals: Scorecard, OSV advisories, popularity, age
Signals combine with AND rules (not additive weights). A package with a single anomalous A-signal still gets flagged when paired with a low-popularity baseline. Returns a structured Verdict with level: "ok" | "warn" | "critical", the signal IDs, and the sources consulted.
- Not an enforcement gate — it surfaces evidence; your agent decides
- Not a malware scanner — no static or dynamic analysis of package contents
- Not a sandbox — packages still install with their normal permissions if you proceed
- Not a guarantee — heuristics can produce both false positives and false negatives; treat results as one input, not a verdict
pkg-guard is a stdio MCP server. Your AI client launches it as a subprocess. No hosted service, no OAuth, no account required.
npm install -g @pkg-guard/mcpWhy global install? MCP clients (Claude Code, Cursor, Claude Desktop) spawn the MCP subprocess without inheriting your shell's PATH. If you use
nvmor any non-system Node install, thenpxbinary may not be on the subprocess's PATH, and the MCP connection fails with-32000. A global install putspkg-guard-mcpon a stable path that subprocess-spawned MCP servers can find.You can use
npx -y @pkg-guard/mcpinstead if yournode+npxare at predictable system paths (Homebrew Node, system Node) — see Troubleshooting if your client reports-32000or "Failed to connect."
# After global install
claude mcp add --transport stdio pkg-guard pkg-guard-mcp
# Alternative: via npx (use absolute paths if your client reports -32000)
claude mcp add --transport stdio pkg-guard "$(which node)" "$(which npx)" -y @pkg-guard/mcp{
"mcpServers": {
"pkg-guard": {
"command": "pkg-guard-mcp"
}
}
}{
"mcpServers": {
"pkg-guard": {
"command": "pkg-guard-mcp"
}
}
}Restart your MCP client after editing the config. The check_package tool should appear in the tool list.
| Symptom | Cause | Fix |
|---|---|---|
-32000 / "Failed to connect" |
MCP client can't find npx or node because the subprocess doesn't inherit your shell PATH (common with nvm) |
Use the global-install path above, or pass absolute paths: claude mcp add --transport stdio pkg-guard `which node` `which npx` -y @pkg-guard/mcp |
sh: pkg-guard-mcp: command not found (when calling npx) |
Stale npx cache holding a broken older version | rm -rf ~/.npm/_npx && npm cache clean --force |
Tool list missing check_package |
Bin started but the MCP protocol handshake failed | Test directly: echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1"}},"id":1}' | pkg-guard-mcp should print a JSON-RPC response with serverInfo and tools.listChanged: true |
After install, drop the adoption snippet below into your agent's system prompt (Claude Code's CLAUDE.md, Cursor rules, Claude Desktop system prompt). A typical exchange:
You: install left-pad
Agent: Let me check left-pad first via pkg-guard.
[calls check_package({ ecosystem: "npm", name: "left-pad" })]
Verdict: ok
Signals: []
Sources: registry, deps-dev, osv
The package looks fine. Proceeding with `npm install left-pad`.
When something's wrong:
You: install some-suspicious-package
Agent: [calls check_package, gets level: critical]
pkg-guard returned a CRITICAL signal:
- A1 (version_age): published 2 hours ago
- A2 (new_lifecycle_script): new preinstall script in v1.2.4
I am not going to install this without explicit confirmation.
Please review the evidence and tell me if you want to proceed.
Paste this verbatim into your agent's system prompt (Claude Code's CLAUDE.md, Cursor rules, Claude Desktop system message, or any other MCP-capable AI coding agent). It makes the agent call check_package before any npm or PyPI install, block on warn / critical until you explicitly confirm, and ask before proceeding if pkg-guard is unavailable.
## pkg-guard package install safety
Before installing or adding any npm or PyPI package, call the `check_package`
MCP tool from the pkg-guard server.
This applies before commands including `npm install`, `npm add`, `pnpm add`,
`yarn add`, `bun add`, `pip install`, `pipx install`, `uv pip install`,
`poetry add`, and edits to files that introduce new npm/PyPI dependencies
(`package.json`, `pyproject.toml`, `requirements.txt`, `Pipfile`).
Call `check_package` with:
- `ecosystem: "npm"` for npm/pnpm/yarn/bun packages.
- `ecosystem: "pypi"` for pip/pipx/uv/poetry packages.
- `name`: the package name.
- `version`: the exact version if known. Omit only when the install
command would use the default/latest version.
If pkg-guard returns `level: "ok"`, proceed normally and keep the
explanation brief.
If pkg-guard returns `level: "warn"` or `level: "critical"`, do **not**
install yet. Show the user:
- `level`
- `summary_hint`
- `signal_ids`
- `signals[].evidence`
- `sources_failed`
Then ask the user for explicit confirmation before installing. Do not
infer that they want to proceed.
If the pkg-guard tool is unavailable, errors, or times out (or returns a
non-empty `sources_failed`), do **not** silently proceed. Tell the user
the supply-chain check could not be completed and ask whether they want
to proceed without pkg-guard. Do not run the install command before they
answer.
If `level: "ok"` but `sources_failed` includes `"registry"`, the
requested version probably does not exist on the registry. Treat this
as a phantom-version risk: tell the user the version was not found,
and ask before installing. (v1.0.x has a known gap where this is not
yet a first-class signal; v1.1 will add an explicit `A6:
version_not_found` heuristic.)
Do not reinterpret a `warn` or `critical` result as safe because OSV has
no advisory, because the project is popular, or because the maintainer
is well-known. The pkg-guard `level` is the authoritative alert for this
workflow.Note: MCP tool selection is model-controlled. The snippet strongly instructs the agent but does not enforce — a user who tells the agent to install anyway can still install anyway.
pkg-guardis an alerting layer, not a hard gate.
All optional. Set as environment variables (or via your MCP client's env config).
| Variable | Default | What it does |
|---|---|---|
HMAC_LOG_SECRET |
dev-default | Per-process secret used to HMAC-hash package names in structured logs. Set to a stable random value if you want consistent hashes across npx invocations. |
PKG_GUARD_AUDIT_LOG |
0 (off) |
Set to 1 to append hash-only audit lines to ~/.pkg-guard/audit.jsonl. Useful for analyzing your own check history; default is off so nothing touches disk. |
For each check_package call, pkg-guard:
- Resolves the package and version against the registry (npm or PyPI)
- Fetches in parallel: registry metadata, deps.dev (Scorecard + popularity), OSV.dev (advisories), and for PyPI, the wheel
RECORDfile for.pthfiles - Runs 9 heuristics (A1-A5 acute + B1-B4 baseline)
- Combines signals with AND rules into a
Verdict - Returns:
{ level, signals[], transitive_flagged[], sources_consulted[], sources_failed[], summary_hint }
Failed sources degrade gracefully — the call still returns a Verdict computed from whatever resolved within the timeout budget, and sources_failed[] tells the caller what was missing.
Privacy: structured logs and the optional audit file are hash-only. Package names are HMAC-SHA256'd; raw package names and versions never appear in logs or files.
- Heuristic, not deterministic. Can produce both false positives (legitimate packages with surprising release patterns) and false negatives (carefully-staged attacks that match a normal release pattern).
- Phantom-version gap (v1.0.x). If you query a version that doesn't exist on the registry (e.g.
react@99.0.0), the engine currently returnslevel: "ok"withsources_failed: ["registry"]rather than treating "version not found" as its own signal. Always checksources_failedbefore acting on a level: ok verdict — ifregistryis insources_failed[]for a real-world (not test) query, the version probably doesn't exist and you should not install it. Tracked for v1.1+ as a newA6: version_not_foundheuristic. - English-only signal labels. Signal descriptions are English; integrate via signal IDs (
A1,A2, …) for client UIs that localize. - No package contents inspection. We read metadata and (for PyPI wheels) the RECORD; we don't unpack tarballs or run dependency installers.
- Source availability. When deps.dev or OSV is degraded, signals that depend on them are unavailable.
sources_failed[]reports this. - Stdio only in v1.0. Hosted/multi-tenant mode (HTTP + OAuth) was scoped out for v1; see CONTRIBUTING.md if you want to help bring it back.
See CONTRIBUTING.md for dev setup, test requirements, and how to add a new heuristic or MCP tool. Issues and PRs welcome.
Vulnerability reports: see SECURITY.md. Use GitHub Security Advisories for private disclosure.
MIT. Copyright 2026 Filipo Ardenghi.