What is an install script attack?

The package runs its own code before you import it.

An install script attack is malicious code executed while a package is installed or built, through a hook such as an npm postinstall script, a Python setup.py build or a Cargo build.rs, before any application code imports the package. The payload runs on a developer laptop or CI runner with access to tokens, SSH keys and cloud credentials. The sections below cover where each package manager runs install-time code, seven 2026 attacks built on the hooks, and what the settings for turning the hooks off still miss.

Questions and answers

What is an install script attack?

An install script attack is malware that runs while a package is installed or built, before any of your code imports the package. Package managers let a package run its own code at install time for legitimate jobs such as compiling a native extension: npm runs preinstall, install and postinstall scripts, pip builds a source distribution through its setup.py, Cargo compiles and runs a crate’s build.rs, and Composer loads plugins. An attacker who publishes a malicious version, through a hijacked maintainer account or a lookalike name, puts the payload in one of those hooks. The payload then runs on a developer laptop or a CI runner with that user’s access to tokens, SSH keys and cloud credentials. In practice, in the March 2026 axios compromise, a postinstall hook in the injected dependency plain-crypto-js installed a remote-access trojan on macOS, Windows and Linux during an ordinary npm install.

Where does install-time code run in each ecosystem?

Install-time code runs in a different place in each ecosystem, and attackers in 2026 used most of them. The npm docs, the Python site docs, the Cargo Book and the Composer docs describe each hook:

  • npm: preinstall, install and postinstall scripts run on install. A git dependency’s prepare script runs before npm packs the package, and a package with a root binding.gyp and no install script gets node-gyp rebuild by default.
  • PyPI: installing a source distribution runs its build backend, usually setup.py. A wheel runs nothing at install, but a .pth file in site-packages runs its import lines every time Python starts.
  • Cargo: a crate’s build.rs is compiled and run just before Cargo builds the crate.
  • Composer: only the root package’s own scripts run, but a dependency that is a Composer plugin runs code during the Composer run once you allow the plugin.

A check that looks only for postinstall misses most of these hooks.

What did install script attacks look like in 2026?

Install script attacks in 2026 were spread across six different npm, PyPI and Cargo hooks, not only the obvious postinstall. Seven cases from Chain305’s teardowns, built from the incident reports, show the range:

  • axios (npm, March): a postinstall hook in the new dependency plain-crypto-js dropped a remote-access trojan.
  • pgserve (npm, April): a postinstall stealer republished itself with the victim’s npm tokens.
  • @bitwarden/cli 2026.4.0 (npm, April): the first preinstall script in any release downloaded Bun and ran a 9.7 MB payload.
  • TanStack (npm, May): the prepare script of a git-hosted optional dependency stole credentials.
  • Miasma, Phantom Gyp wave (npm, June): a 157-byte binding.gyp ran a 4.5 MB index.js through node-gyp, with no install script declared.
  • litellm 1.82.8 (PyPI, March): a .pth file ran the stealer at every Python start.
  • onering 1.4.1 (crates.io, June): a build.rs sent each build’s latest commit and diff to an attacker.

Which package-manager settings help?

Four package-manager settings turn off or narrow install-time code, and each is one line of configuration:

  • npm: ignore-scripts=true in .npmrc stops npm running the scripts in package.json files, as CISA advised after the axios compromise.
  • pnpm: a dependency not listed in allowBuilds may not run build scripts, and strictDepBuilds, on by default, fails the install when one tries.
  • pip: --only-binary :all: installs wheels only, so no setup.py or build backend runs. uv’s no-build and Poetry’s installer.only-binary do the same.
  • Composer: allow-plugins defaults to an empty list since Composer 2.2.0, so no plugin loads until you allow the plugin by name.

Cargo runs build.rs before it builds the crate, and the Cargo Book says a build script “may perform any number of tasks”, so read each new build.rs in a dependency diff. A package which needs its script, for instance a native module, is allowed by name. The guides for npm, PyPI and Maven, Go and Cargo give the full settings.

What do these settings miss?

Install-script settings stop install hooks, not code that runs later. A wheel’s .pth file passes --only-binary :all: and runs at the next Python start, which is how litellm 1.82.8 ran in March 2026. Microsoft’s durabletask 1.4.1 to 1.4.3 ran nothing during pip install: the dropper fired when the package was imported. The AsyncAPI wave of the Miasma worm ran at module load, and JFrog notes that --ignore-scripts does not help there. npm’s ignore-scripts also leaves npm start, npm test and npm run running the script they name. The settings work per machine and per project, so one laptop or one CI job without the line is exposed. And the switches are all or nothing per package: a native module that needs its script gets an exception, and from then on every new version of that module runs its script unchecked. None of the settings reads what a script does.

Where does a package firewall fit?

A package firewall reads the install-time code when the package is first fetched and refuses the package before the hook can run, for every client that pulls through the firewall. Chainsaw ships two seeded rules for install-time code. The first refuses install scripts that fetch remote URLs: Chainsaw follows a postinstall command into the script it runs. When we measured Chainsaw against 58 attacks from 2026, the rule refused pgserve, the SAP CAP preinstall loader and onering’s build.rs on first download. The second refuses import-time execution of fetched or decoded code, which refused litellm 1.82.8’s .pth file and all three durabletask versions. A 48-hour release hold, longer when a version first adds an install script, covers what the rules miss. The rules have limits: axios’s setup.js decoded its strings without a direct download call, so the age rule stopped axios, not the script rule. The 2026 attack results.

Sources

Each fact on this page links its source, and the list below is the full set: the npm, pnpm, pip, Python, Cargo and Composer documentation, the CISA alert on axios, a StepSecurity report, and Chain305’s teardowns of nine 2026 attacks.

Install hooks run on first download

Read the install script before it runs

Start a free org in monitor mode and see which install scripts Chainsaw would refuse in your dependency tree.