
When the Sandworm Came for My Secrets: Lessons from Shai-Hulud 2.0
Earlier this week, I discovered that this very repository had fallen victim to Shai-Hulud 2.0 - a sophisticated supply chain attack that compromised npm packages and exfiltrated secrets from thousands of developers and CI/CD pipelines. My API keys for services like FAL.ai, OpenAI, Perplexity, and others were published to attacker-controlled repositories before I even knew what had happened.
This post covers what the attack is, how it works, exactly how I got caught, and the changes I’ve made to prevent this from happening again.
How I Found Out
On November 27th, I received an email from Omar Hammou, a security researcher who monitors GitHub for exposed secrets:
I have identified several leaked credentials associated with your accounts, including OpenAI and Perplexity tokens. I operate a GitHub monitoring system that continuously flags exposed secrets, and although some repositories were deleted, the leaked data is still present in GitHub’s historical snapshots.
The credentials had been exfiltrated to a repository at github.com/dp5iveme/zf0jxl7wqn14g9enav (since deleted by GitHub). As Omar explained, even when repositories are deleted, forks and clones preserve the repository state as archived snapshots - meaning the leaked data can persist indefinitely. Omar was kind enough to share the full payload that was captured - a JSON dump of my entire shell environment, including every environment variable that was set at the time of the attack.
What is Shai-Hulud 2.0?
Named after the giant sandworms from Frank Herbert’s Dune, Shai-Hulud 2.0 is an ongoing malware campaign that targets npm package maintainers. Attackers compromise maintainer accounts and publish trojanized versions of legitimate packages.
According to Wiz’s analysis, the scale is staggering:
- 25,000+ repositories affected
- 350+ unique users compromised
- 1,000 new repositories being created every 30 minutes during the initial outbreak
- Packages from major companies including Zapier, ENS Domains, PostHog, and Postman were trojanized
How the Attack Works
The malware is deceptively simple but devastatingly effective:
Preinstall Hook Execution: The malicious code runs during npm’s
preinstallphase via scripts likesetup_bun.jsandbun_environment.js. This ensures execution before any code review or static analysis can occur.Credential Harvesting: The malware systematically steals:
- Local configuration files (
.npmrc,.env, cloud CLI configs) - Environment variables
- Cloud metadata service tokens
- Secrets from AWS Secrets Manager, Google Secret Manager, and Azure Key Vault
- Local configuration files (
Triple Base64 Encoding: Stolen data is encoded three times before exfiltration, evading simple detection rules.
Exfiltration to GitHub: Credentials are published to attacker-controlled repositories with the description “Sha1-Hulud: The Second Coming.”
Persistent Backdoor: The malware injects a
discussion.yamlGitHub Actions workflow that registers infected machines as self-hosted runners named “SHA1HULUD”, enabling future remote command execution.Destructive Fallback: If credential exfiltration fails, the malware attempts to delete the victim’s entire home directory - converting espionage into sabotage.
The Smoking Gun: kill-port@2.0.3
Thanks to the exfiltrated payload Omar shared, I was able to identify exactly which package compromised my system. The evidence was right there in the environment variables:
{ "npm_lifecycle_script": "node setup_bun.js", "npm_lifecycle_event": "preinstall", "npm_package_name": "kill-port", "npm_package_version": "2.0.3", "PWD": "/Users/russ.mckendrick/Code/blog/node_modules/kill-port"}The culprit was kill-port - a utility package for killing processes on specific ports. According to the GitLab Advisory (GMS-2025-498), versions 2.0.2 and 2.0.3 were compromised on November 24th, 2025. The vulnerability is rated HIGH severity (CVSS 8.6) and classified as CWE-506 (Embedded Malicious Code).
The npm_lifecycle_script value of node setup_bun.js is the exact malicious script identified in the Wiz security report - it’s designed to look innocuous (who wouldn’t trust something called “setup_bun.js” in 2025?), but its sole purpose is credential theft.
The package maintainer confirmed on GitHub that npm support has since removed the infected versions (2.0.2 and 2.0.3) from the registry. Version 2.0.1 is safe and does not contain the malicious files.
What Was Stolen
The payload captured my entire environment, including:
| Service | Credential Type |
|---|---|
| OpenAI | API Key |
| Perplexity | API Key |
| FAL.ai | API Key |
| Tavily | API Key |
| Exa | API Key |
| Last.fm | API Key |
Plus my username, home directory path, shell configuration paths, and various other environment details that could be useful for further attacks.
Fortunately, I don’t store AWS, GCP, or Azure credentials in environment variables (they use CLI-based authentication), but many developers aren’t so lucky.
The Global Damage
According to Wiz’s preliminary analysis, attackers obtained:
| Credential Type | Count |
|---|---|
| GitHub Access Tokens | 775 |
| AWS Credentials | 373 |
| GCP Credentials | 300 |
| Azure Credentials | 115 |
These numbers only represent what was discovered in the attacker’s public repositories before GitHub took action.
What I’ve Changed
After rotating all my compromised credentials (a fun few hours, I must say), I’ve implemented several changes to reduce the attack surface:
Migrated from npm to pnpm
Beyond being faster and more disk-efficient, pnpm has better security defaults. I’ve updated all my GitHub Actions workflows to use pnpm with the --frozen-lockfile flag:
- name: 🌸 Setup PNPM uses: pnpm/action-setup@v4
- name: 📦 Install dependencies run: pnpm install --frozen-lockfileThe --frozen-lockfile flag ensures CI/CD installs exactly what’s in the lockfile - no surprises from updated packages.
I’ve also added aliases to my .zshrc to prevent accidental npm usage locally:
# 📦 pnpm aliases in .zshrcalias npm='echo "Error: Use pnpm"; false'alias n="pnpm"alias ni="pnpm install"This ensures I can’t accidentally run npm install out of muscle memory.
Explicitly Approve Build Scripts
pnpm 10+ has a security feature that blocks lifecycle scripts by default, showing warnings like:
Ignored build scripts: esbuild, sharp, workerd.Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.Rather than blanket-allowing all scripts, you can explicitly whitelist only the packages you trust. I’ve added this to my package.json:
{ "pnpm": { "onlyBuiltDependencies": [ "esbuild", "sharp", "workerd" ] }}This means:
| Package | Script | Purpose |
|---|---|---|
sharp | install | Downloads native image processing binaries |
esbuild | postinstall | Downloads platform-specific bundler binary |
workerd | postinstall | Downloads Cloudflare Workers runtime |
Any other package trying to run lifecycle scripts will be blocked. This is a much better approach than --ignore-scripts (which breaks legitimate packages) or allowing everything (which is what npm does by default).
Regular Security Audits
I’ve added pnpm audit to all my GitHub Actions workflows:
- name: 🔒 Security audit run: pnpm auditThis checks dependencies against the npm security advisory database and will fail the build if any known vulnerabilities are found. You can also run it locally:
pnpm auditPinned Dependencies
Rather than allowing automatic updates, I’m now pinning dependencies to specific versions and reviewing changes manually:
{ "dependencies": { "astro": "5.1.1" }}Auditing for Compromise
If you’re concerned your environment may have been affected, check for:
- Suspicious workflow files: Look in
.github/workflows/forshai-hulud-workflow.yml,discussion.yaml, or unexpected branches - Self-hosted runner registrations: Check for runners named “SHA1HULUD” in your GitHub repository settings
- Unexpected npm packages: Review your
package-lock.jsonorpnpm-lock.yamlfor packages added between November 21-25, 2025
Credential Rotation
If you installed any npm packages during the attack window (November 21-25, 2025), you should:
- Clear your npm/pnpm cache:
pnpm store prune - Delete
node_modulesand reinstall - Rotate all credentials that were accessible to your development environment
- Review GitHub for any unauthorized workflow changes
Lessons Learned
This attack is a stark reminder of several realities in modern development:
Supply chain attacks are the new normal: We implicitly trust thousands of packages and their maintainers every time we run
npm install.CI/CD environments are high-value targets: They often have access to production credentials, cloud resources, and deployment capabilities.
Lifecycle scripts are a security risk: The ability for packages to execute arbitrary code during installation is powerful but dangerous.
Defense in depth matters: No single measure would have prevented this, but layers of security (restricted CI/CD permissions, pinned dependencies, disabled scripts) reduce the blast radius.
The security community is watching: I’m grateful to researchers like Omar who actively monitor for exposed secrets and alert affected developers. Without his email, I might not have known for weeks.
Acknowledgments
Thanks to Omar Hammou for the heads-up and for sharing the exfiltrated payload so I could identify the attack vector. Security researchers who responsibly disclose findings like this are invaluable to the community.
Further Reading
- Wiz: Shai-Hulud 2.0 - Ongoing Supply Chain Attack
- The Hacker News: Second Sha1-Hulud Wave Affects 25,000+ Repositories
- GitLab Advisory GMS-2025-498: kill-port
- GitHub Issue: kill-port infected with malware
Stay vigilant out there !!!
Share
Related Posts

How to Install Ansible on a Mac: A Modern Approach
A guide to installing and managing Ansible on macOS using Conda, with tips for handling collections and dependencies.

Azure DevOps Ansible Pipeline; Boosting Efficiency with Caching
Discover how to optimize your Azure DevOps Ansible pipeline with caching techniques. Learn to reduce execution time, improve efficiency, and maintain security checks in your infrastructure as code deployments.

My Starship Prompt Setup
A Deep Dive into a Gruvbox-Themed Terminal