pnpm-shield
v1.0.1
Published
Supply chain attack protection audit tool for pnpm projects
Maintainers
Readme
🛡️ pnpm-shield
Supply chain attack protection audit tool for pnpm projects.
pnpm-shield audits your pnpm project and developer environment against the most common supply chain attack vectors — postinstall script injection, dependency confusion, phantom dependencies, and accidental npm usage. It runs 13 checks, explains every finding with attack vectors and remediation steps linked to official docs, and can auto-fix all of them interactively.
Why this matters
The npm registry is the largest software registry in the world — and one of the most targeted. Attackers abuse postinstall scripts, typosquatting, and account takeovers to execute arbitrary code on every machine that runs npm install or pnpm install. The attacks below all share one trait: they would have been stopped by a correctly configured pnpm environment.
| Incident | Year | Attack vector | Source |
|---|---|---|---|
| event-stream backdoor | 2018 | Malicious postinstall injected after maintainer handover | Snyk |
| eslint-scope credential theft | 2018 | Stolen npm credentials → postinstall exfiltrated .npmrc tokens | ESLint |
| ua-parser-js takeover | 2021 | npm account hijacked → cryptominer + RAT via postinstall | GitHub Advisory |
| dependency confusion | 2021 | Public package shadows internal name, executes on install | Alex Birsan |
| node-ipc sabotage | 2022 | Maintainer added destructive postinstall targeting Russian IPs | Socket.dev |
| colors + faker protest | 2022 | Maintainer corrupted own packages, breaking thousands of projects | Snyk |
| xz-utils backdoor | 2024 | 2-year social engineering → malicious build script in release tarball | Openwall |
| polyfill.io CDN hijack | 2024 | Domain acquired → CDN injected malicious JS into 100k+ sites | Sansec |
| nx package compromise | Aug 2025 | Malicious nx versions published → credential theft + filesystem scan | Arctic Wolf |
| Shai-Hulud worm | Sep 2025 | Self-replicating npm worm stole cloud tokens and re-published infected packages | Wiz |
| axios maintainer compromise | Mar 2026 | Hijacked maintainer account → postinstall RAT in axios v1.14.1 | Arctic Wolf |
| TanStack / TeamPCP campaign | Apr 2026 | Poisoned CI/CD cache → malicious publishes across TanStack ecosystem | Cybernews |
💡 Every
postinstallattack in this list is blocked byignore-scripts=true. The dependency confusion attacks are mitigated by a strictpnpm-lock.yamland thepackageManagerfield enforced by Corepack.pnpm-shieldchecks for all of these protections.
Installation
# Run directly without installing (recommended for one-off audits):
pnpm dlx pnpm-shield
# Install globally:
pnpm add -g pnpm-shield
# Add as a dev dependency in your project:
pnpm add -D pnpm-shieldZero production dependencies. Everything uses Node.js built-ins.
Usage
# Run in the root of your project:
pnpm-shield
# Same command, shorter alias:
pnpm-check
# CI mode — non-interactive, exits with code 1 on failures:
pnpm-shield --ci
# Show help:
pnpm-shield --help
# Show version:
pnpm-shield --versionInteractive menu
After the audit runs, an interactive prompt lets you explore and fix findings without leaving the terminal.
Commands
| Command | Action |
|---------|--------|
| ? | Open an arrow-key browser across all 13 checks — navigate with ↑↓, press Enter to read documentation |
| ?N | Read docs for check N directly, e.g. ?3 |
| fix | Open a visual multi-selector for fixes — navigate with ↑↓, toggle with Space, confirm with Enter |
| all | Apply all auto-fixable items at once |
| q | Quit |
Documentation panel
Every check has an integrated documentation panel showing:
- Why it matters — the security rationale
- Attack vector — a concrete attack scenario
- How to fix — step-by-step remediation commands
- Official references — links to pnpm docs, Node.js docs, and security post-mortems
What it checks
pnpm-shield runs 13 security checks across three categories. All non-passing checks support auto-fix.
🖥 Environment
| # | Check | Severity | Auto-fix |
|---|-------|----------|---------|
| 1 | pnpm is installed and in PATH | CRITICAL | — |
| 2 | Shell alias npm → pnpm | HIGH | ✅ Adds alias to shell config |
| 3 | Corepack enabled and managing pnpm | HIGH | ✅ Runs corepack enable pnpm |
| 4 | No foreign lockfiles (package-lock.json, yarn.lock, bun.lockb) | CRITICAL | ✅ Deletes foreign lockfiles |
⚙️ pnpm / npm Configuration
| # | Check | Severity | Auto-fix |
|---|-------|----------|---------|
| 5 | Global ignore-scripts = true | CRITICAL | ✅ pnpm config set ignore-scripts true |
| 6 | Local .npmrc: ignore-scripts=true | HIGH | ✅ Appends to .npmrc |
| 7 | Local .npmrc: save-exact=true | MEDIUM | ✅ Appends to .npmrc |
| 8 | Local .npmrc: shamefully-hoist=false | LOW | ✅ Appends to .npmrc |
| 9 | Local .npmrc: engine-strict=true | MEDIUM | ✅ Appends to .npmrc |
📦 package.json Hardening
| # | Check | Severity | Auto-fix |
|---|-------|----------|---------|
| 10 | pnpm.onlyBuiltDependencies whitelist | HIGH | ✅ Adds [] to package.json |
| 11 | packageManager field pinned to [email protected] | HIGH | ✅ Sets current pnpm version |
| 12 | engines.node range specified | MEDIUM | ✅ Sets >= current Node major |
| 13 | pnpm-lock.yaml present | HIGH | ✅ Runs pnpm install |
Grading
After the audit, your project receives a security grade:
| Grade | Score | Meaning | |-------|-------|---------| | A+ | ≥ 92% | Fortress — all critical paths hardened | | A | ≥ 84% | Excellent — minor optional improvements available | | B | ≥ 76% | Good — a few medium-risk items to address | | C | ≥ 60% | Fair — notable gaps that should be closed | | D | < 60% | Needs attention — critical or multiple high failures |
Score = passed checks + 0.5 × warnings.
Check details
1. pnpm installed — CRITICAL
pnpm is the only mainstream package manager with onlyBuiltDependencies whitelisting, per-project ignore-scripts, and a content-addressable store with integrity verification.
corepack enable pnpm
# or:
curl -fsSL https://get.pnpm.io/install.sh | sh2. Shell alias npm → pnpm — HIGH ✅
Even with pnpm fully configured, typing npm install by muscle memory invokes the real npm binary. npm ignores your .npmrc, your pnpm-lock.yaml, and your onlyBuiltDependencies whitelist.
echo 'alias npm=pnpm' >> ~/.zshrc && source ~/.zshrc3. Corepack managing pnpm — HIGH ✅
Corepack (built into Node.js ≥ 16.9) reads "packageManager" in package.json and blocks npm and yarn project-wide. It also ensures every developer uses the exact same pnpm version.
corepack enable
corepack enable pnpm📎 Corepack docs · pnpm + Corepack
4. No foreign lockfiles — CRITICAL ✅
A package-lock.json or yarn.lock alongside pnpm-lock.yaml creates two conflicting sources of truth. CI systems may pick the wrong one, installing different (potentially malicious) resolved versions.
rm package-lock.json yarn.lock bun.lockb
pnpm install
git add pnpm-lock.yaml5. Global ignore-scripts=true — CRITICAL ✅
Packages can declare postinstall, preinstall, and install lifecycle scripts that run arbitrary shell commands. This is the primary vector for supply chain attacks (event-stream 2018, node-ipc 2022, xz-utils 2024).
pnpm config set ignore-scripts trueNote: With
ignore-scripts=true, packages that legitimately need build scripts (e.g.esbuild,sharp) will break. Use check #10 (onlyBuiltDependencies) to whitelist exactly those packages.
📎 pnpm ignore-scripts · event-stream post-mortem
6. Local .npmrc: ignore-scripts=true — HIGH ✅
The global config can differ across machines and CI environments. A local .npmrc commits the rule into the repository, protecting every developer and every CI runner regardless of their global config.
echo "ignore-scripts=true" >> .npmrc
git add .npmrc7. save-exact=true — MEDIUM ✅
By default pnpm saves deps with a ^ prefix (e.g. ^1.2.3), allowing any compatible update. An attacker who compromises a package can publish 1.2.4 with malicious code and every project using ^1.2.3 adopts it on the next install.
echo "save-exact=true" >> .npmrc8. shamefully-hoist=false — LOW ✅
pnpm uses a strict, isolated node_modules layout by default. shamefully-hoist=true flattens it like npm, allowing packages to import dependencies they never declared (phantom dependencies).
echo "shamefully-hoist=false" >> .npmrc9. engine-strict=true — MEDIUM ✅
Some security patches are Node.js-version-specific. Running code on an EOL Node version may miss critical fixes.
echo "engine-strict=true" >> .npmrc10. pnpm.onlyBuiltDependencies — HIGH ✅
Even with ignore-scripts=true, you may need certain packages to run build scripts (e.g. esbuild, sharp). This whitelist gives surgical, auditable control over which packages may run scripts.
// package.json
{
"pnpm": {
"onlyBuiltDependencies": ["esbuild", "sharp"]
}
}An empty array [] blocks all postinstall scripts without exception.
11. packageManager field — HIGH ✅
Tells Corepack the exact package manager and version the project requires. Corepack will then block npm and yarn and auto-download the correct pnpm version for any contributor.
// package.json
{
"packageManager": "[email protected]"
}12. engines.node range — MEDIUM ✅
Declares the minimum Node.js version. Combined with engine-strict=true, pnpm refuses to install on incompatible environments, preventing use of EOL runtimes with known CVEs.
// package.json
{
"engines": { "node": ">=20" }
}13. pnpm-lock.yaml present — HIGH ✅
The lockfile pins exact resolved versions AND SHA-512 integrity hashes for every package in the full dependency tree. Without it, pnpm install resolves versions fresh each time and can silently adopt a compromised release.
pnpm install # generates pnpm-lock.yaml
git add pnpm-lock.yaml
# Never add pnpm-lock.yaml to .gitignore!📎 pnpm lockfile format · Should lockfiles be committed?
CI/CD integration
GitHub Actions
# .github/workflows/security.yml
name: Security Audit
on: [push, pull_request]
jobs:
pnpm-shield:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: pnpm dlx pnpm-shield --ciNative git pre-commit hook (no extra dependencies)
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
pnpm-shield --ci
EOF
chmod +x .git/hooks/pre-commitRecommended baseline configuration
.npmrc
ignore-scripts=true
save-exact=true
shamefully-hoist=false
engine-strict=truepackage.json additions
{
"packageManager": "[email protected]",
"engines": { "node": ">=20" },
"pnpm": {
"onlyBuiltDependencies": []
}
}Project structure
pnpm-shield.js ← Entry point (20 lines)
lib/
colors.js ← ANSI color constants
docs.js ← Per-check documentation + official references
checks.js ← Runs all 13 security checks
ui.js ← Terminal output (header, results, doc panel, summary)
selector.js ← Raw TTY arrow-key interactive selector (zero deps)
fixes.js ← Auto-fix implementations for all 13 checks
runner.js ← Orchestrates the full audit + interactive menuContributing
Pull requests are welcome. To add a new check:
- Add the result in
lib/checks.jswith adocKeyandfixkey - Add documentation in
lib/docs.jswithwhy,attack,fix, andrefs - Add a fix handler in
lib/fixes.js
License
MIT
