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.
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.
# 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.
# 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 --versionAfter the audit runs, an interactive prompt lets you explore and fix findings without leaving the terminal.
| 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 |
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
pnpm-shield runs 13 security checks across three categories. All non-passing checks support auto-fix.
| # | 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 |
| # | 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 |
| # | Check | Severity | Auto-fix |
|---|---|---|---|
| 10 | pnpm.onlyBuiltDependencies whitelist |
HIGH | β
Adds [] to package.json |
| 11 | packageManager field pinned to pnpm@X.Y.Z |
HIGH | β Sets current pnpm version |
| 12 | engines.node range specified |
MEDIUM | β
Sets >= current Node major |
| 13 | pnpm-lock.yaml present |
HIGH | β
Runs pnpm install |
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.
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 | shπ pnpm Installation
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 ~/.zshrcCorepack (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
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.yamlπ Dependency confusion attack
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
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 .npmrcBy 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" >> .npmrcπ Semver hijacking
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" >> .npmrcπ Phantom dependencies
Some security patches are Node.js-version-specific. Running code on an EOL Node version may miss critical fixes.
echo "engine-strict=true" >> .npmrcEven 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.
An empty array [] blocks all postinstall scripts without exception.
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": "pnpm@11.1.1"
}π packageManager field
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" }
}π engines field
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?
# .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 --cicat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
pnpm-shield --ci
EOF
chmod +x .git/hooks/pre-commit.npmrc
ignore-scripts=true
save-exact=true
shamefully-hoist=false
engine-strict=truepackage.json additions
{
"packageManager": "pnpm@11.1.1",
"engines": { "node": ">=20" },
"pnpm": {
"onlyBuiltDependencies": []
}
}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 menu
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
MIT