TL;DR — A PreToolUse hook intercepts every
npm installClaude runs and blocks packages with known CVEs. Three layers: pre-install audit hook, lockfile diff check, and CLAUDE.md version pinning rules. Setup takes 5 minutes. Jump to the hook config →
⚡ Quickest fix (30 seconds): Don’t want to set up hooks? Add 6 rules to your CLAUDE.md (see CLAUDE.md Rules below). Catches 80% of risk. No shell scripts needed.
Here’s a Claude Code security hook that audits every npm install before it runs:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/npm-audit-check.sh", "timeout": 30 } ] } ] }}🔒 This hook is running on my projects right now. I set it up the day the axios news broke. The hook fires before npm even starts resolving dependencies, so the RAT payload in
plain-crypto-jsnever would have executed.
Last week, axios@1.14.1 shipped a RAT. Nearly 100 million weekly downloads. The attacker compromised a maintainer’s npm account using a stolen long-lived classic access token, then injected a malicious postinstall dependency.
The malicious postinstall hook (a script npm runs automatically after installing a package) called home to an attacker’s server within 2 seconds of npm install, before npm even finished resolving dependencies.
If Claude ran that install during the 3-hour compromise window, your machine was owned before you read the terminal output.
This hook would have caught it. Here’s the full setup.
Why Does Vibe Coding Make Supply Chain Attacks Worse?
When AI agents run npm install autonomously, the human review step disappears. 41% of all code written in 2025 is AI-generated or AI-assisted, according to the Stack Overflow 2025 Developer Survey.
The attack surface isn’t just compromised packages. It’s the speed at which they get installed without anyone checking transitive dependencies.
Here’s the pattern. Claude suggests a package. You approve the install. npm install runs. A new transitive dependency slides in. Nobody checks what it does.
The axios compromise exploited exactly this. The malicious plain-crypto-js package wasn’t something a developer chose to install. It arrived as a hidden dependency of a trusted package. This is the same mistake as accepting Claude’s output without review, except instead of buggy code, you get a remote access trojan.
Without a hook:
Claude: "I'll install axios for HTTP requests"→ npm install axios→ axios@1.14.1 resolves→ plain-crypto-js@4.2.1 postinstall runs→ RAT phones home in 2 seconds→ You see "added 3 packages"With a PreToolUse hook:
Claude: "I'll install axios for HTTP requests"→ Hook intercepts the Bash command→ BLOCKED: install without --ignore-scripts→ Claude retries with --ignore-scripts→ Postinstall attack never executesHere’s how the axios attack actually unfolded, step by step:
- Account takeover. The attacker compromised npm maintainer
jasonsaayman’s account using a stolen long-lived classic npm token. Classic tokens bypass 2FA entirely. - Staging. 18 hours before the malicious axios release, they published a clean
plain-crypto-js@4.2.0to make the dependency look established rather than brand-new. - Disabling safeguards. They removed husky git hooks from the axios repo, disabling pre-commit checks that might have flagged the change.
- Dependency injection. Added
plain-crypto-jsas a dependency in axios’spackage.json. The name mimicscrypto-js(15M weekly downloads) to look normal in dependency lists. - Postinstall trigger.
plain-crypto-jscontained asetup.jsregistered as a postinstall script. npm runs it automatically. No user interaction needed.
Understanding this chain matters. If your own packages use long-lived npm tokens, single-maintainer accounts, or lack OIDC enforcement, they have the same exposure.
Key insight: Over 99% of all open source malware in 2025 targeted npm specifically, with Sonatype identifying 800+ Lazarus Group-associated packages concentrated on the npm ecosystem (Sonatype Q4 2025). When AI agents install npm packages autonomously, they’re operating in the most targeted package ecosystem on the internet.
Why Does npm Keep Getting Hit the Same Way?
The axios attack wasn’t a wake-up call. It was the seventh alarm in an eight-year series that npm keeps hitting snooze on. Every major compromise since 2018 exploited the same structural gap: the registry trusts whoever holds the token, and postinstall runs arbitrary code with your user’s permissions by default.
| Year | Package | What happened | Vector |
|---|---|---|---|
| 2018 | event-stream (2M/wk) | Stranger gained maintainer trust over months, injected Bitcoin wallet stealer via flatmap-stream. Took 2 months to catch (npm post-mortem) | Social engineering → postinstall |
| 2021 | ua-parser-js (8M/wk) | Account hijacked. Cryptominer + password stealer in 3 versions. Lived ~4 hours (Rapid7) | Account takeover → postinstall |
| 2022 | colors, faker (22M+/wk) | Maintainer self-sabotaged. Infinite loop in colors, code wiped from faker. No attacker needed, burnout was the threat (Sonatype) | Insider threat |
| 2025 Aug | Nx (4.6M/wk) | GitHub Actions workflow exploit stole npm publish token. S1ngularity campaign deployed QUIETVAULT credential stealer, weaponized LLM tools already on victim machines (Wiz, Socket) | CI/CD pipeline → postinstall |
| 2025 Sep | chalk, debug +16 (2.6B/wk) | Phished maintainer via fake 2FA reset email. Crypto wallet malware. Largest npm attack by download count (CISA) | Phishing → postinstall |
| 2025 Sep-Nov | Shai-Hulud (700+ pkgs) | Self-replicating worm. Reads your npm token, backdoors all your packages, spreads exponentially. Version 2.0 added a dead man’s switch: if it can’t replicate or exfiltrate, it wipes your home directory (Datadog, Kaspersky) | Phishing → worm → postinstall |
| 2026 Mar | axios (100M/wk) | Stolen classic token bypassed 2FA + OIDC. RAT with self-deletion and version spoofing. 3-hour window | Token theft → postinstall |
Look at the “Vector” column. Six out of seven attacks end with the same two words: → postinstall. Different attackers, different years, different entry points. Same execution mechanism. Eight years running.
npm’s response each time: unpublish the package, publish a security stub, write a blog post. No structural change to how postinstall works. No sandboxing. No permission model. No opt-in. Just trust.
After Shai-Hulud, npm added Trusted Publishers and OIDC-based publishing. Three months later, axios got compromised with a classic token that bypassed all of it. The fix for the last attack doesn’t prevent the next one.
Here’s the uncomfortable reality: npm provenance adoption remains opt-in. OIDC publishing is opt-in. Even if npm implements every security proposal on their roadmap tomorrow, ecosystem-wide adoption takes years. Meanwhile, AI agents install packages at machine speed.
You can wait for npm to fix this. People have been waiting since 2018.
Or you can block it on your side. Right now. Today.
This blog isn’t about analyzing what went wrong. Plenty of smart people have written excellent post-mortems for every incident in that table. This is about the one thing that actually works at the individual dev level: blocking postinstall execution before it runs. On your machine. In your CI. In your AI agent’s workflow.
The math: 99.8% of all open source malware in Q4 2025 originated from npm (Sonatype Q4 2025). The overwhelming majority of that malware uses lifecycle scripts (postinstall, preinstall) as the execution mechanism. Block that one vector, and you eliminate most of the risk. Not 100%. But enough that you can vibe code without holding your breath every time npm install runs.
Here’s what blocking postinstall looks like in Claude Code specifically. Three layers, 5 minutes, no ecosystem changes needed.
Key insight: Six of the seven major npm supply chain attacks since 2018 used postinstall scripts as their execution mechanism, from event-stream’s Bitcoin stealer to the Shai-Hulud worm that hit 700+ packages (CISA, 2025). Blocking postinstall with
--ignore-scriptsis the single highest-impact defense a developer can deploy today, and it takes one flag.
How Do You Set Up a Pre-Install Audit Hook?
Add a PreToolUse hook to .claude/settings.json that matches Bash commands containing npm install or yarn add. The hook runs a shell script that checks whether --ignore-scripts is present and blocks the install if it’s missing. If you’re new to Claude Code hooks, they’re scripts that run at specific lifecycle points and can block dangerous operations before they happen.
Here’s the complete .claude/settings.json config:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/npm-audit-check.sh", "timeout": 30 } ] } ], "PostToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": ".claude/hooks/post-install-audit.sh", "timeout": 30 } ] } ] }}The PreToolUse script at .claude/hooks/npm-audit-check.sh:
#!/bin/bash# Blocks npm install commands that don't use --ignore-scripts
input=$(cat)tool_name=$(echo "$input" | jq -r '.tool_name // ""')command=$(echo "$input" | jq -r '.tool_input.command // ""')
if [ "$tool_name" != "Bash" ]; then exit 0fi
# Only check npm/yarn/pnpm install commands (including ci, npx, exec)if ! echo "$command" | grep -qE 'npm (install\b|i |ci\b)|npx |npm exec |yarn (add|install) |pnpm (add|install) '; then exit 0fi
# Allow if already using --ignore-scriptsif echo "$command" | grep -q '\-\-ignore-scripts'; then exit 0fi
# Block installs without --ignore-scriptsecho '{"error": "BLOCKED: npm install without --ignore-scripts. Postinstall scripts are the #1 supply chain attack vector (see axios@1.14.1). Rerun with --ignore-scripts."}' >&2exit 2The PostToolUse script at .claude/hooks/post-install-audit.sh:
#!/bin/bash# Runs npm audit after any install command completes
input=$(cat)tool_name=$(echo "$input" | jq -r '.tool_name // ""')command=$(echo "$input" | jq -r '.tool_input.command // ""')
if [ "$tool_name" != "Bash" ]; then exit 0fi
if ! echo "$command" | grep -qE 'npm (install\b|i |ci\b)|npx |npm exec |yarn (add|install) |pnpm (add|install) '; then exit 0fi
audit_output=$(npm audit --json 2>/dev/null)vuln_count=$(echo "$audit_output" | jq -r '.metadata.vulnerabilities.total // 0')
if [ "$vuln_count" -gt 0 ]; then high=$(echo "$audit_output" | jq -r '.metadata.vulnerabilities.high // 0') critical=$(echo "$audit_output" | jq -r '.metadata.vulnerabilities.critical // 0') echo "WARNING: npm audit found $vuln_count vulnerabilities ($critical critical, $high high). Run 'npm audit' for details."fi
exit 0Make both scripts executable:
chmod +x .claude/hooks/npm-audit-check.sh .claude/hooks/post-install-audit.shWhen Claude tries npm install axios, the PreToolUse hook blocks it and tells Claude to add --ignore-scripts. Claude retries with the flag. The install succeeds without running postinstall scripts. Then the PostToolUse hook runs npm audit and reports any known vulnerabilities.
Key insight: Sonatype identified 34,319 new open source malware packages in Q3 2025, a 140% increase from the previous quarter, bringing the cumulative total since 2019 to 877,522 (Sonatype Q3 2025). A PreToolUse hook that enforces
--ignore-scriptswould have prevented the axios@1.14.1 RAT from executing its payload entirely.
Try it now: Copy the hook config into your
.claude/settings.jsonand the two scripts into.claude/hooks/. Runchmod +xon both. The next time Claude triesnpm install, you’ll see the block in action.
📬 One Claude Code security tip every week. Hooks, CLAUDE.md tricks, and real attack breakdowns. No fluff. Subscribe to AI Developer Weekly →
What About Packages That Pass npm Audit but Are Still Malicious?
npm audit only catches known CVEs that have been reported and added to the advisory database. Zero-day compromises like axios@1.14.1 won’t appear for hours. The advisory was added after the malicious version was already pulled from npm. Layer two catches what audit misses: a lockfile diff that flags any new dependency for review.
Add this as a git pre-commit hook:
#!/bin/bash# .git/hooks/pre-commit (or .husky/pre-commit)# Flags new dependencies added since last commit
lockfile_diff=$(git diff --cached --name-only | grep -E 'package-lock\.json|yarn\.lock|pnpm-lock\.yaml')
if [ -z "$lockfile_diff" ]; then exit 0fi
new_deps=$(git diff --cached package-lock.json \ | grep -E '^\+.*"resolved":' | head -20)
if [ -n "$new_deps" ]; then echo "" echo "New dependencies detected in lockfile:" echo "$new_deps" | head -10 echo "" echo "Review these before committing. New transitive deps" echo "are how supply chain attacks hide." echo "" echo "To proceed anyway: git commit --no-verify" exit 1fi
exit 0The axios payload also used techniques specifically designed to evade static analysis. The setup.js script encrypted its configuration with a XOR cipher (key: OrDeR_7077), stored strings as reversed base64, and constructed C2 URLs at runtime from encrypted parts rather than storing them as plaintext. Tools like npm audit signatures can’t catch what they can’t read.
This catches the scenario where npm audit reports zero issues but a new, unreviewed transitive dependency slipped in. The axios attack introduced plain-crypto-js, a package that had never appeared in any lockfile before. A lockfile diff would have flagged it immediately.
Key insight: 80% of application dependencies remain un-upgraded for over a year, even when 95% of vulnerable components have fixed versions available (Sonatype 2024). Lockfile diffing catches new, potentially malicious dependencies at commit time, not weeks later during a scheduled audit.
Can You Use an Agent Hook for Deeper Analysis?
Yes. Claude Code’s agent hooks spawn a separate Claude instance with access to Read, Grep, and Glob. An agent hook can analyze a new transitive dependency by reading its package metadata, checking publication dates, and comparing the lockfile against its previous state. This is the most thorough layer, but it costs tokens per invocation.
{ "hooks": { "PostToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "agent", "prompt": "A Bash command just ran: $ARGUMENTS. If it was an npm/yarn/pnpm install command, read package-lock.json and identify any NEW dependencies not present before. For each new dependency, check: does it have a postinstall script? How many weekly downloads does it have? Was it published in the last 7 days? Flag anything suspicious.", "timeout": 120 } ] } ] }}The tradeoff is cost. Agent hooks use roughly 5-10K tokens per invocation. Use the shell hook for every install. Reserve the agent hook for CI pipelines or high-security projects where the extra analysis justifies the spend.
Key insight: 53% of teams that shipped AI-generated code later discovered security issues that passed initial review (Veracode 2025). An agent hook adds a second AI reviewer specifically for dependency risk, catching what human review and static audit both miss.
Why Does the Malware Hide Itself After Running?
The axios RAT was built to erase its own traces. After setup.js executes, it deletes itself and renames package.md back to package.json, restoring the innocent-looking package state. If you inspect node_modules/plain-crypto-js/ after the attack, everything looks clean.
It also spoofs its own version. The published version was 4.2.1, but the malware patches its own package.json to read 4.2.0 after install. Running npm list plain-crypto-js shows 4.2.0 even though 4.2.1 was installed. Simple version-checking detection fails.
This is why the PreToolUse hook is the critical layer, not the PostToolUse audit. By the time npm audit runs after installation, the malware has already executed and cleaned up. The only reliable defense is blocking the install before postinstall scripts run.
Key insight: The axios malware’s self-deletion and version spoofing mean that post-install checks see a clean package state. PreToolUse blocking (preventing script execution entirely) is architecturally superior to PostToolUse auditing (checking after the fact) for zero-day supply chain attacks.
📬 Get weekly Claude Code tips. Security hooks, workflow tricks, and real attack breakdowns. One email per week. Subscribe to AI Developer Weekly →
What Rules Should You Add to CLAUDE.md?
Pin exact versions and enforce --ignore-scripts at the instruction level. Add these rules to your project’s CLAUDE.md so Claude follows them in every session, regardless of hook configuration:
## npm Security Rules
- ALWAYS use `--ignore-scripts` with npm install- ALWAYS use `--save-exact` to pin exact versions- NEVER install a package without checking its npm page first- If a package has fewer than 1,000 weekly downloads, ASK before installing- If a package was first published within the last 30 days, ASK before installing- Prefer well-known packages over unknown alternativesThe --save-exact rule matters because the axios compromise targeted version 1.14.1 specifically. If your lockfile pinned 1.14.0, you were safe. Version ranges like ^1.14.0 would have resolved to 1.14.1 on the next clean install.
For a broader look at protecting sensitive files when using Claude Code, see How I Protect Sensitive Code on Real Projects.
Key insight: Supply chain compromises account for 15% of all breaches at an average cost of $4.91 million per incident, with an average detection time of 267 days (IBM Cost of a Data Breach 2025). Pinning exact versions in CLAUDE.md and enforcing
--ignore-scriptsby default are two rules that cost nothing and eliminate the most common attack vectors.
How Do You Check If You Were Already Compromised?
If you ran npm install with axios between March 31 00:21 and 03:29 UTC 2026, your machine may have been hit. The RAT deployed platform-specific payloads and established persistence. Here’s what to look for.
Detection commands
# macOS — check for persistent LaunchAgent disguised as Apple processls -la /Library/Caches/com.apple.act.mondlaunchctl list | grep com.apple.act
# Windows (PowerShell) — check for renamed PowerShell copyTest-Path "$env:PROGRAMDATA\wt.exe"Get-ScheduledTask | Where-Object {$_.Actions.Execute -like "*wt.exe*"}
# Linux — check for Python reverse shellls -la /tmp/ld.pynetstat -an | grep 8000
# All platforms — check for C2 communicationgrep -r "sfrclak.com" /var/log/ 2>/dev/nullC2 (command-and-control) infrastructure to block
The RAT phones home to sfrclak.com:8000. Block the domain immediately:
echo "0.0.0.0 sfrclak.com" | sudo tee -a /etc/hostsWhat each platform payload does
macOS: Uses osascript (AppleScript) to create a persistent LaunchAgent. Drops a binary at /Library/Caches/com.apple.act.mond mimicking an Apple system process. Survives reboots.
Windows: Copies PowerShell as %PROGRAMDATA%\wt.exe (mimics Windows Terminal) via VBScript. Creates a scheduled task for persistence. The renamed binary evades process monitoring.
Linux: Downloads a Python reverse shell to /tmp/ld.py via curl. Less persistent, clears on reboot or /tmp cleanup.
All platforms exfiltrate: npm tokens (~/.npmrc), SSH keys (~/.ssh/), cloud credentials (AWS, GCP, Azure configs), git credentials, and environment variables containing API keys.
Key insight: The axios RAT used process-name mimicry on every platform:
com.apple.act.mondon macOS,wt.exeon Windows. If you only checked for obviously named malware binaries, you’d miss it. The detection commands above check the specific file paths and persistence mechanisms this payload used.
Frequently Asked Questions
Does this hook work with pnpm and yarn?
Yes. Change the grep pattern in the script to match your package manager:
# For pnpmif ! echo "$command" | grep -qE 'pnpm (add|install)'; then
# For yarnif ! echo "$command" | grep -qE 'yarn (add|install)'; thenYarn 2+ (Berry) uses --skip-builds instead of --ignore-scripts. Adjust the flag check accordingly.
Will this slow down my workflow?
The shell hook adds 1-2 seconds per install command. The agent hook adds 10-15 seconds. The axios RAT exfiltrated your npm tokens, SSH keys, and cloud credentials in 2 seconds. Worth the trade.
What if I’m using Claude Code in auto-accept mode?
Hooks run regardless of your permission mode. That’s the whole point. Even in auto-accept mode, a PreToolUse hook with exit 2 blocks the tool call before it executes. This is your safety net when you’ve turned off manual review. I learned this the hard way with file deletion: test your actual threat model, not the one you imagine.
Can I combine this with Snyk or Socket?
Yes. Replace npm audit with your preferred tool in the post-install script:
# Snyksnyk test --json 2>/dev/null
# Socketsocket npm audit 2>/dev/nullSame exit code contract. Zero means clean, non-zero means problems found.
What should you do if you installed axios@1.14.1?
Seven steps, in order:
- Downgrade and pin:
npm install axios@1.14.0 --save-exact - Block the malicious dependency in
package.json:{ "overrides": { "plain-crypto-js": "npm:empty-npm-package@1.0.0" } } - Remove the phantom package:
rm -rf node_modules/plain-crypto-js - Rotate ALL credentials in two steps: first revoke old npm tokens with
npm token revoke <token-id>, then generate new ones withnpm token create. Do the same for SSH keys, cloud provider credentials (AWS IAM, GCP service accounts), GitHub PATs, and any API keys in.envfiles - Block C2: Add firewall rules for
sfrclak.com(resolve current IP withdig sfrclak.comsince C2 IPs rotate) - Remove platform persistence: On macOS delete
/Library/Caches/com.apple.act.mondand its LaunchAgent. On Windows remove%PROGRAMDATA%\wt.exeand its scheduled task (Get-ScheduledTask | Where-Object {$_.Actions.Execute -like "*wt.exe*"} | Unregister-ScheduledTask). On Linux delete/tmp/ld.py - If persistence is confirmed, rebuild from a known-good state. The RAT had full shell access. Assume everything on the machine was accessible.
Does Cursor have something similar?
No. This is an architectural difference, not a feature gap.
| Cursor | Claude Code | |
|---|---|---|
| Mechanism | .cursorrules (prompt only) | Hooks (process-level) |
| Can block commands? | Suggest only | exit 2 = blocked |
| Works in auto-accept? | N/A | Always |
Cursor can say “be careful with dependencies.” Claude Code can kill the command before it runs. This is not a feature gap. It’s a different architecture. No workaround exists for Cursor.
Quick Start: Full Setup in 5 Minutes
# 1. Create hooks directorymkdir -p .claude/hooks
# 2. Save the two scripts (copy from sections above)# .claude/hooks/npm-audit-check.sh (PreToolUse)# .claude/hooks/post-install-audit.sh (PostToolUse)
# 3. Make them executablechmod +x .claude/hooks/*.sh
# 4. Add the hook config to .claude/settings.json# (copy the JSON block from "How Do You Set Up a Pre-Install Audit Hook")
# 5. Add npm security rules to your CLAUDE.md# (copy from "What Rules Should You Add to CLAUDE.md")Next time Claude runs npm install, the hook blocks it and enforces --ignore-scripts automatically.
📬 Get weekly Claude Code tips. Security hooks, workflow tricks, and real attack breakdowns. One email per week. Subscribe to AI Developer Weekly →
What to Read Next
- Claude Code Hooks: The Power Feature Nobody Talks About — the complete guide to all 17 hook events and 4 handler types
- Claude Code Has 17 Hook Events Now — agent hooks, prompt hooks, and 7 copy-paste recipes
- I Set Up 3 Layers of Defense. It Deleted My File Anyway. — why testing your actual threat model matters more than adding layers