Block
npm,npx, andyarnin Claude Code. Forcepnpminstead. Defense against Shai-Hulud-style npm supply-chain attacks.
A two-layer guard for Claude Code that
prevents any accidental npm / npx / yarn invocation and steers you
toward pnpm. Soft layer is a skill (instruction Claude reads); hard
layer is a PreToolUse hook that blocks the Bash call at the harness
level with exit 2 and an ANSI-bold warning.
The npm ecosystem went through a chain of major supply-chain attacks:
- Shai-Hulud (Sep 2025) — first self-propagating worm, 500+ packages compromised
- Shai-Hulud 2.0 (Nov 2025) — 700+ packages, 27 000+ malicious GitHub repos, 14 000 secrets exfiltrated
- Mini Shai-Hulud (May 2026) — 170+ npm packages + 2 PyPI packages
- PackageGate (Jan 2026) — six zero-days in npm / pnpm / vlt / Bun
Common vector: malicious code runs during the preinstall / install /
postinstall phase, automatically, before any tests or security
checks. This is exactly what npm i --ignore-scripts disables — and it is
why npm (which runs those scripts without asking) is the problem.
pnpm closes both vectors by default — and pnpm 11 (April 2026) turned the cooldown on out of the box:
| pnpm behavior | Status | What it does |
|---|---|---|
| Dep build scripts blocked outside allowlist | default (v10+) | Lifecycle scripts of dependencies are not run unless the package is in onlyBuiltDependencies / allowBuilds (pnpm-workspace.yaml). Review pending ones with pnpm approve-builds. |
minimumReleaseAge: 1440 |
default (v11+) | Won't install versions younger than 1440 min (24h) — malware is usually detected and pulled within hours. Was 0 (opt-in) before v11. Opt out with minimumReleaseAge: 0; bypass one package with minimumReleaseAgeExclude. |
blockExoticSubdeps |
default (v11+) | Blocks transitive deps pulled from git repos / tarball URLs. |
trustPolicy: no-downgrade |
default (v11+) | Won't install a package whose trust level dropped vs earlier releases. |
strictDepBuilds: true |
opt-in | Turns the skipped-build warning into a hard error. |
pnpm install --ignore-scripts |
manual | Nuclear option — blocks all scripts, including the allowlist. Use for a package you don't trust. |
ℹ️
pnpm config get minimumReleaseAgereturnsundefinedwhen you rely on the built-in v11 default —config getonly echoes explicit config, not defaults. The cooldown is still active. Pin it explicitly (version-independent) withpnpm config set minimumReleaseAge 1440. Source: pnpm settings — "Default: 1440 (since v11)".
This repo enforces "pnpm only" inside Claude Code, where the agent might
otherwise run npm install autonomously.
- Skill —
no-npmis registered in Claude Code's skill list; the agent reads the instruction and refuses to runnpm/npx/yarn, offering thepnpmequivalent instead. - PreToolUse hook — Node.js script that inspects every
Bashtool invocation; if the command word-matchesnpm/npx/yarn, it prints a bold ANSI-red banner and exits with code2, which the harness treats as "deny". - Idempotent installer — re-running
node install.jsis safe; it does not duplicate the hook entry and leaves any other hooks (e.g.block-destructive.js) intact. - No false positives on
pnpm— word-boundary regex distinguishespnpmfromnpmandpnpxfromnpx.
- Claude Code (CLI or VS Code extension)
- Node.js ≥ 18 LTS in
PATH— the hook is a JS script; the installer usesnodefor JSON merging
node --versionOne command, every platform (Windows / macOS / Linux):
git clone https://github.com/UAantovakul/no-npm-claude.git
cd no-npm-claude
node install.js- Copies
SKILL.mdandblock-npm.jsinto~/.claude/skills/no-npm/and~/.claude/hooks/. - Merges the hook entry into
~/.claude/settings.jsonunderPreToolUse → Bash. Idempotent — existing hooks are preserved, no duplicates on re-run. - Pins the pnpm cooldown
minimumReleaseAge=1440(24h) explicitly — pnpm 11+ already defaults this, but an explicit value is version-independent (survives a downgrade to pnpm 10). Only if pnpm is present and not already set; skipped silently if pnpm is missing. - Runs a smoke test (
npm installmust produceexit 2).
Restart Claude Code, then in a new chat:
"Run
npm installin the current project."
Expected response — a warning in bold that looks like this:
⛔ NPM / NPX / YARN BLOCKED IN THIS ENVIRONMENT. USE pnpm INSTEAD.
Why: the npm ecosystem suffered a series of supply-chain attacks (Shai-Hulud 2025/2026). npm runs lifecycle scripts without asking; pnpm blocks dependency build scripts outside its allowlist by default.
Blocked command:
npm installSuggested replacement:npm install → pnpm install
If Claude tries to run npm as a Bash call anyway, the harness intercepts
it via the hook and prints the same banner to stderr with exit 2.
Manually test the hook:
echo '{"tool_input":{"command":"npm install"}}' | node ~/.claude/hooks/block-npm.js
echo $? # should print 2node uninstall.jsRemoves the hook entry from settings.json (other hooks untouched), then
deletes ~/.claude/skills/no-npm/ and ~/.claude/hooks/block-npm.js. The
pnpm cooldown is left in place — remove it yourself if you want it gone:
pnpm config delete minimumReleaseAge.
/(^|[\s;&|()`]|\$\()(npm|npx|yarn)(\s|$|[;&|`)])/Word-boundary with Bash-aware separators: leading position, whitespace,
;, &&, ||, |, (, `, $(.
Blocked: npm install, npm i react, npx vite, yarn add lodash,
sudo npm i -g foo, cd app && npm test.
Allowed: pnpm install, pnpm add react, pnpx whatever,
cat npmlock.txt, echo "npm is bad", git log.
Claude Code's PreToolUse hook receives a JSON payload on stdin:
{ "tool_input": { "command": "npm install" } }The hook prints to stderr and uses exit codes:
exit 0— allow the command (default forpnpm,git, etc.)exit 2— block the command; stderr is shown to the agent and the user
no-npm-claude/
├── README.md — this file
├── LICENSE — MIT
├── install.js — cross-platform installer (Node only)
├── uninstall.js — cross-platform uninstaller (Node only)
└── payload/
├── skills/no-npm/SKILL.md — the skill
└── hooks/block-npm.js — the hook
The skill + hook only works inside Claude Code (it uses the skills API and the PreToolUse hook contract). If you also use Claude (or another LLM) in a different client, paste this rule into the system prompt / custom instructions:
HARD RULE: do not use npm, npx, or yarn in this environment. Always use pnpm.
If I (or the user) want to run an npm/npx/yarn command:
1. Do not execute the command.
2. Issue a warning in BOLD letters:
**⛔ NPM IS BLOCKED IN THIS ENVIRONMENT. Use pnpm instead.**
3. Offer the pnpm equivalent:
npm i -> pnpm install
npm i <pkg> -> pnpm add <pkg>
npm run <s> -> pnpm <s>
npx <pkg> -> pnpm dlx <pkg>
yarn add <pkg> -> pnpm add <pkg>
npm i --ignore-scripts-> pnpm install --ignore-scripts
npm audit -> pnpm audit
4. Ask the user to confirm before running the pnpm variant.
Reason: a chain of npm supply-chain attacks (Shai-Hulud 2025-2026). The
vector is auto-run lifecycle scripts. npm runs them without asking; pnpm
blocks dependency build scripts outside its allowlist by default, and pnpm 11+
also defaults to a 24h version cooldown (minimumReleaseAge=1440) + blockExoticSubdeps.
Where to paste it:
- Cursor — Settings → Rules → User Rules, or
.cursorrulesin the repo root - Claude Desktop — Settings → Custom Instructions
- ChatGPT — Settings → Personalization → Custom Instructions
- VS Code Continue / Cline — system prompt in the config
- CLI agents (Codex CLI, Aider, etc.) —
--systemflag or system file
For one-off cases where npm is genuinely required (e.g. debugging an
incompatible tool):
- Recommended: run the command in your own terminal outside Claude Code. The hook only intercepts the harness Bash channel.
- Temporary disable: comment out the hook entry in
~/.claude/settings.json(PreToolUse → Bash → block-npm.js), run the command, restore.
- Claude Code — tested with releases that support the skills API and PreToolUse hooks (late 2025+).
- Node.js — ≥ 18 LTS (the hook uses only stdlib features).
- OS — Windows 10/11, macOS 12+, modern Linux distros.
The hook does not depend on Claude Code internals — it is a plain JSON-stdin / stderr / exit-code contract, so it will keep working as long as the PreToolUse hook contract is stable.
Issues and PRs welcome. Quick test for any regex change:
# should print 2 (blocked)
echo '{"tool_input":{"command":"npm install"}}' | node payload/hooks/block-npm.js; echo $?
# should print 0 (allowed)
echo '{"tool_input":{"command":"pnpm install"}}' | node payload/hooks/block-npm.js; echo $?Keep the regex word-boundary intact — false positives on pnpm, pnpx,
npmlock, or string literals containing the word npm should never
occur.
- pnpm — Mitigating supply chain attacks
- pnpm 11.0 release — supply-chain protection on by default (minimumReleaseAge=1440 + blockExoticSubdeps, April 2026)
- CISA — Widespread Supply Chain Compromise Impacting npm Ecosystem
- Microsoft Security — Shai-Hulud 2.0
- Palo Alto Unit 42 — "Shai-Hulud" Worm Compromises npm Ecosystem
- Anthropic — Claude Code documentation
Maintained by @UAantovakul.