Install-Time Execution Is the Bug Class: Six 2026 Campaigns, One Primitive, and the Practitioner's Checklist

Written by the Rafter Team

Six of the most damaging supply-chain attacks of 2026 used the same primitive: code that runs at package install time, before the developer has reviewed any of what they just downloaded. Shai-Hulud and its variants, Mini Shai-Hulud, the Trivy / TeamPCP compromise, CanisterSprawl, the PyTorch Lightning hit, and the LiteLLM PyPI incident all turned a routine npm install or pip install into a credential exfiltration event before the developer's terminal returned a prompt.
This is not a clever new attack. It is the oldest primitive in the package-manager threat model, and the npm and PyPI ecosystems are still treating it as a feature.
The defenses are well-known. They are rarely applied. This post is the practitioner's checklist for fixing that on the developer side, because the ecosystems are not going to fix it on theirs.
On every developer machine and CI runner today, set npm config set ignore-scripts true for npm-using projects, and pin Python dependencies with --require-hashes inside isolated virtual environments. Maintain a per-project allowlist for the handful of legitimate packages whose install scripts you've audited. This is the structural defense that survives every variant of every Shai-Hulud-class worm.
The shape of the bug class
Install-time execution is what happens when a package manager runs code from a freshly downloaded package before the developer has had a chance to read it.
npm — postinstall hooks
In npm, this is the postinstall lifecycle script. A package can declare:
{
"scripts": {
"postinstall": "node ./bootstrap.js"
}
}
…and npm will run node ./bootstrap.js automatically when the package is installed. Legitimate uses include compile steps for native modules, prebuilt-binary downloads, platform-specific setup, and developer-experience flourishes. Malicious uses include literally anything else.
The same applies to preinstall (runs before package extraction) and install (runs during install). Each of these is a documented npm lifecycle hook, and each one is a code-execution channel for any maintainer who has ever published the package.
Python — module-level and .pth files
Python's situation is more diffuse. There is no direct equivalent of npm's postinstall hook at the install step itself, but Python's module system runs module-level code on import. A package that defines top-level statements in its __init__.py, or that uses a .pth file to inject sys.path manipulation, can execute arbitrary code the first time the package is loaded.
.pth files are particularly sharp. A .pth file placed in a Python site-packages directory is read on every Python process startup, regardless of whether the developer's code imports the relevant package. The LiteLLM PyPI compromise used exactly this primitive: a malicious litellm_init.pth that ran on every Python process startup on the affected machine.
From the attacker's perspective, these mechanisms are functionally equivalent to npm's postinstall hooks: "the user typed pip install, then ran their code; the credential stealer ran when their code first imported the package — or, with .pth, the moment any Python process started."
The structural problem
Both ecosystems share the same load-bearing assumption: the first install is the first execution. There is no signed-execution boundary, no sandbox, no per-package permission grant. The package manager is, in effect, a remote-code-execution channel from any maintainer who has ever published a package to every developer who installs it.
The 2026 receipts
Six campaigns this year, all using install-time execution as their delivery primitive:
Shai-Hulud (September 2025 onward)
The npm worm family. Every variant (1.0, 2.0, 3.0, Mini) uses postinstall hooks to harvest credentials at install time. The exact extraction code rotates between variants; the primitive does not. Four variants in eight months, all sharing the same load-bearing trick.
Mini Shai-Hulud — PyTorch Lightning hit (April 30, 2026)
Used Python import-time execution. The compromised lightning 2.6.2 and 2.6.3 releases included a hidden _runtime/ directory. A Python entry point start.py ran on import lightning, invoked a JavaScript dropper, downloaded the Bun runtime from GitHub releases, and executed an obfuscated multi-megabyte payload that harvested credentials, validated GitHub tokens, and worms into accessible repositories.
Trivy / TeamPCP compromise (March 2026)
The compromised trivy-action GitHub Actions releases bundled npm packages with malicious postinstall hooks. Those hooks ran on every CI build that used the action, exfiltrating CI secrets — AWS, GCP, Azure, GitHub tokens, npm tokens — and pivoted within days to Checkmarx, VS Code extensions, and downstream npm packages. The Trivy incident is the canonical example of how install-time execution scales when it lands inside CI infrastructure.
CanisterSprawl (April 21, 2026)
TeamPCP "CanisterWorm" tradecraft applied to automagik/genie and pgserve on npm. Postinstall hooks ran credential collectors, validated stolen npm tokens against the registry, and attempted self-propagation by republishing other packages the stolen tokens could access. Combined weekly downloads of the targeted packages were roughly 8,000 at the time of compromise.
LiteLLM PyPI compromise (March 24, 2026)
Attackers bypassed official CI/CD workflows and uploaded malicious packages (litellm==1.82.7 and litellm==1.82.8) directly to PyPI. The releases contained a malicious .pth file (litellm_init.pth) that executes on every Python process startup on the host, regardless of whether the developer's current code imports LiteLLM. The .pth mechanism is a sharper version of import-time execution — it doesn't even require the package to be imported.
Intercom-client npm compromise (late April 2026)
Sibling of the PyTorch Lightning hit, attributed to the same Mini Shai-Hulud actor. intercom-client@7.0.4 used a preinstall hook (similar to postinstall but earlier in the lifecycle) to drop the same credential-stealer payload family.
Six campaigns. One primitive.
Why the ecosystems haven't fixed this
Install-time execution is a feature that some legitimate packages depend on, and the ecosystems' threat models prioritize compatibility over defense.
In npm, the postinstall hook is the documented way to handle native-module compilation (node-gyp invocations, prebuilt binary downloads), platform-specific setup, and developer-experience flourishes (printing "thanks for installing!" messages — yes, really). Deprecating it would break a non-trivial set of packages that use the hook for legitimate reasons. The maintainer pushback against deprecation is real and consistent.
In Python, import-time side effects are a language feature. Module-level code is, by definition, executed when the module is loaded. There is no clean way for pip to disable this without changing how Python's import system works. .pth files are a separate but related feature — they exist precisely to allow packages to do install-time setup, and were never designed with an adversarial-publisher threat model in mind.
The net effect is that the ecosystems will not deprecate install-time execution in a foreseeable timeline. The defense has to be on the developer side.
The practitioner's checklist
npm — turn off install scripts globally
The single-line fix:
npm config set ignore-scripts true
This blocks preinstall, install, postinstall, and the related lifecycle hooks across every npm install on the machine. The downside is that some legitimate packages — bcrypt, puppeteer, node-sass, anything with a native compile step — will not install correctly without their hooks.
Maintain a per-project allowlist. When a legitimate package needs its install script, document why, and run it in a one-off npm install --ignore-scripts=false against just that package. The friction is the defense.
For CI runners, the same flag applies, and the friction is even less of a problem because CI environments are typically ephemeral and well-defined. Set ignore-scripts in your base CI image and allowlist exceptions per pipeline.
Python — virtual environments + hash-pinned lockfiles
Python doesn't have npm's clean kill-switch. The defensive primitive is a combination:
python -m venv .venv && source .venv/bin/activate
pip install --require-hashes -r requirements.txt
--require-hashes requires every dependency in requirements.txt to include a SHA-256 hash that pip verifies against the downloaded package. If the hash doesn't match, the install fails. This defeats both "the package on PyPI was silently replaced with a poisoned version" attacks and "the package is delivered through a registry mirror you didn't choose."
Combine with pip install --only-binary=:all: where possible to prefer pre-built wheels over source-built packages. Wheels are not immune to malicious code, but they are easier to verify and don't require running setup.py from source.
For .pth-file attacks specifically (the LiteLLM vector), the structural defense is virtual environment isolation: a .pth file in one venv does not affect another. The LiteLLM .pth file ran on every Python process startup within the affected venv. Discipline about which venv your shell is in is part of the defense — a global pip install outside any venv places .pth files in your system Python's site-packages, where they run on every Python process you launch.
Both ecosystems — pin dependencies, scan on every push
package-lock.json with --frozen-lockfile. requirements.txt with --require-hashes. Pin to exact versions. Update dependencies deliberately, with review, rather than receiving them as a side effect of npm install or pip install.
Scan dependencies on every push, not on a quarterly audit cycle. The Shai-Hulud variants ship faster than quarterly. The diff that introduces a new dependency, or that updates a dependency to a poisoned version, is the diff where the warning is most useful. Rafter's Code Analysis Engine flags known-vulnerable versions on every push; the gap between "advisory exists" and "the next merge is blocked" is the defensive window that matters.
Both ecosystems — scope tokens narrowly
Every install-time attack of 2026 had the same goal: harvest credentials. A repo-write npm publish token is worth significantly more to the attacker than a read-only token. An AWS root key is worth significantly more than a scoped IAM credential. A GitHub Personal Access Token with repo:* is worth significantly more than one with repo:read.
Use single-purpose tokens with the minimum scope each tool needs. The attackers will keep finding tokens to steal. The question is what those tokens are good for once stolen.
What this is not
It is not a clever attack. It is the oldest primitive in the package-manager threat model. Researchers have been pointing at it since at least 2017. The reason it keeps working is that the defenses are well-known and rarely applied.
It is not a problem specific to AI tooling. AI tools are the most prominent recent victims because they are the fastest-growing dependency class, but the primitive predates AI by a decade. Mini Shai-Hulud would have used the same primitive against a popular Express middleware in 2018.
It is not a problem that goes away with better detection. Detection-engineering teams are getting faster — Mini Shai-Hulud's PyTorch Lightning compromise was quarantined within 42 minutes. That number is good. It is not zero, and zero is what install-time execution requires when an attacker's payload is a credential exfiltrator that runs to completion in seconds.
How Rafter helps
Rafter's Code Analysis Engine flags known-vulnerable dependency versions and known-suspicious dependency patterns on every push — including newly published packages, packages with no download history, and dependencies that drift from known-good names or maintainers. The diff that introduces a poisoned dependency is exactly where the warning is most useful, before the next CI run downloads and executes the install hook.
What Rafter does not do is patch a package manager's install-script behavior — that's upstream of any code scanner. What scanning addresses is shortening the window between "advisory exists for this poisoned version" and "the next merge is blocked." For variants released ahead of any public advisory, the structural defenses — ignore-scripts, hash-pinning, venv isolation, narrow token scope — are what actually stop the chain.
Closing on the primitive
Six campaigns this year used install-time execution. The next six will too. The npm and PyPI ecosystems are not going to deprecate it.
Turn off install scripts on npm. Use hash-pinned virtual environments on Python. Pin dependencies. Scan on every push. Scope tokens narrowly.
If your malware can execute the moment a developer types pip install, the defense isn't "find the malware faster." It is "don't let unaudited code run on import."
Further reading
- PyTorch Lightning, Mini Shai-Hulud, and Malware That Signs Commits as Claude Code — Python import-time execution at scale.
- Three Supply Chains, One Trust Relationship — npm postinstall execution layered with marketplace and runtime trust relationships.
- Trivy / TeamPCP Supply-Chain Compromise — install-time execution at CI scale.
- Shai-Hulud at Eight Months: Four Variants, One Campaign — the campaign retrospective that reuses this primitive every quarter.
Sources
- LiteLLM Security Update — March 2026 incident report: https://docs.litellm.ai/blog/security-update-march-2026
- FutureSearch — litellm 1.82.8 Supply Chain Attack on PyPI: https://futuresearch.ai/blog/litellm-pypi-supply-chain-attack/
- Trend Micro — Inside the LiteLLM Supply Chain Compromise: https://www.trendmicro.com/en_us/research/26/c/inside-litellm-supply-chain-compromise.html
- Semgrep — Shai-Hulud Themed Malware Found in the PyTorch Lightning AI Training Library: https://semgrep.dev/blog/2026/malicious-dependency-in-pytorch-lightning-used-for-ai-training/
- GitGuardian — No Off Season: Three Supply Chain Campaigns Hit npm, PyPI, and Docker Hub in 48 Hours: https://blog.gitguardian.com/three-supply-chain-campaigns-hit-npm-pypi-and-docker-hub-in-48-hours/
- Wallet-stealer payloads in package ecosystems
- A year of AI developer-tool supply-chain attacks
- The TeamPCP campaign retrospective