Usually yes, but not the way most people run it. Installing exact versions from a committed lockfile, with dependency install scripts turned off and a cooldown that skips versions published in the last few days, removes most of the risk from the worm and hijacked release waves of 2025 and 2026. A plain npm install or pip install that pulls the newest version of everything, with your credentials in the environment, is the risky part.
The question keeps coming back because the incidents keep coming back. On 21 October 2026 a compromised LiteLLM release reached PyPI and Docker Hub with a credential stealer inside. In August, Singapore's Cyber Security Agency warned about a Shai-Hulud family worm that had touched more than 1,300 npm package versions. In each case, the dangerous window was hours, not weeks, and the people who got hurt were the ones who installed the newest version during those hours.
This post explains what actually runs when you install a package, how long bad versions tend to stay live, which defenses work and where each one stops, how to check in minutes whether a bad version reached a laptop, a CI cache or a container image, and what to rotate if it did. It covers npm, pnpm and Yarn on the JavaScript side and pip and uv on the Python side.
Key takeaways
- A cooldown of a few days removes most of that exposure. npm, pnpm, Yarn, pip and uv all now have a minimum release age setting. Security fixes need a documented way around it.
- A lockfile protects you only when you install from it.
npm ciand frozen installs do;npm install some-package,npx, global installs and unpinned Dockerfile lines do not. - Install scripts are the main way npm malware runs. pnpm 10 and later, and current Yarn, already block dependency scripts by default. On npm you set it yourself.
- Python has no single off switch. Source builds run code, and a
.pthfile runs code every time any Python process starts, even if you never import the package. Prefer wheels, hashes and a cooldown. - If a bad version ran, assume the host is compromised. Rotate from a clean device, starting with the tokens that let the attack spread.
What actually runs on my machine during npm install or pip install?
An install is closer to running a stranger's program with your account, environment and network than to a download.
npm, pnpm and Yarn: lifecycle scripts
An npm package can declare scripts in its package.json that run during installation. According to the npm scripts documentation, installing a package runs its preinstall, install and postinstall scripts, plus the prepare family in some cases. If a package ships a binding.gyp file and declares no install script of its own, npm defaults to running node-gyp rebuild, which compiles native code on your machine.
That is the mechanism almost every npm worm uses. The Shai-Hulud worm of September 2025, as GitHub described it, spread "by injecting malicious post-install scripts into popular packages" (GitHub blog). The script runs the moment the package lands, before any of your code imports it, and it inherits whatever is in your environment: npm tokens, GitHub tokens, cloud keys, SSH agent access.
Three other paths matter:
- The package's own code at import time. Even with scripts off, a malicious package runs when your application or test suite imports it. Turning off scripts moves the risk from install time to run time; it does not remove it.
- Git and tarball dependencies. A dependency pointing at a git URL can be built from source, which may run its
preparescript. npm'sallow-gitsetting, which defaults toall, limits fetching from git references (npm config). npxand one-off CLIs.npx some-tooldownloads and runs the newest matching version unless you name one. There is no lockfile in the loop.
pip and uv: builds, wheels and .pth files
Python installs have more ways to run code, and none of them has a universal off switch.
- Source distributions (sdists) run a build. If no compatible wheel exists, pip or uv builds one by running the package's build backend, which is arbitrary Python code (
setup.pyor whatever thepyproject.tomlbuild system specifies). The pip secure installs guide says plainly that by default pip "involves running arbitrary code from distributions". - Wheels do not run code at install time, but they can drop files that run later. A wheel is unpacked, not executed. The catch is what it unpacks.
.pthfiles run at every Python start. Files ending in.pthinsite-packagesare processed whenever the interpreter starts. The Pythonsitedocumentation says lines that begin withimportare executed and "are always run at Python startup", regardless of whether the module is used. The current docs mark theseimportlines as deprecated from Python 3.15, but they still execute during the deprecation period.sitecustomize.pyalso loads at startup. A package that drops one at the root ofsite-packagesgets the same effect.
The October 2026 LiteLLM release used both. According to an independent technical analysis, installing version 1.107.0 added cook.pth, cook.py and sitecustomize.py to site-packages, so the stealer ran in "any Python process" on the host: your app, pip itself, your editor's language server and CI jobs. The OSV entry MAL-2026-2144 notes that the same .pth mechanism appeared in the compromised 1.82.7 and 1.82.8 releases in March.
What the attacker gets
Whatever the install process can read. Per the independent analysis, the October LiteLLM payload went after SSH keys, cloud and Kubernetes credentials, GitHub CLI tokens, CI secrets, AI coding tool tokens, browser passwords and environment variables, then installed an hourly backdoor. Singapore's advisory on the August npm worm lists a similar set (CSA AD-2026-009). The target is the developer's keys, because those keys publish the next round of packages.
How long do malicious versions usually stay live?
Short enough that "right now" is the right way to think about it, and long enough that thousands of machines can be hit.
| Incident | Ecosystem | Time live before removal | Source |
|---|---|---|---|
| LiteLLM, March 2026 | PyPI | 2 h 32 min from upload to quarantine; over 119,000 downloads in the window | PyPI incident report |
| Telnyx, March 2026 | PyPI | 3 h 42 min from upload to quarantine | PyPI incident report |
| LiteLLM 1.107.0, 21 October 2026 | PyPI | About 1 h 44 min (11:39 to 13:23 UTC) | Independent analysis |
| LiteLLM 1.107.0, 21 October 2026 | Docker Hub | About 2 h 34 min until tags were removed or repointed | Independent analysis |
| Shai-Hulud, September 2025 | npm | GitHub was notified 14 September and removed over 500 packages | GitHub, CISA |
Two numbers in PyPI's report explain why the window matters so much. First, users sent 13 reports about the March LiteLLM release, and those reports drove the quarantine. Second, PyPI estimates that 40 to 50 percent of installs fetched the latest version unpinned. That second group is the population a cooldown protects.
PyPI's quarantine feature hides a reported project from the simple index so it is no longer installable while admins review it. That shortens the window but does not close it, because someone has to notice and report first. For a closer look at npm's side of the same problem, see our post on npm takedown times during Shai-Hulud.
What this means for "right now": on any given day the chance that the version you pull is malicious is low, but it rises sharply for versions published in the last few hours, and almost all documented damage happened inside that window. A rule of "nothing published in the last three days" would have kept every incident in the table above off your machine.
Do a lockfile, npm ci and a frozen lockfile protect me?
Yes, against new malicious versions of packages you already depend on, as long as the lockfile was written before the bad version existed and you install from it without letting it change. They do not protect you in the many cases where the lockfile is bypassed or regenerated.
What they do
A lockfile records the exact version and integrity hash of every package in the tree. The OpenSSF npm best practices guide says "Hash pinning informs the package manager of the expected hash for each dependency without trusting the registries." The npm ci documentation adds two useful properties: it requires an existing lockfile, and if package.json and the lockfile disagree it "will exit with an error, instead of updating the package lock". It also deletes any existing node_modules first, so a stale tree cannot hide anything.
The equivalents elsewhere:
# npm: install exactly what the lockfile says, fail on drift
npm ci
# pnpm
pnpm install --frozen-lockfile
# Yarn (modern)
yarn install --immutable
# uv: fail if uv.lock is out of date, never re-resolve
uv sync --locked
# pip: only exact, hashed requirements
pip install --require-hashes -r requirements.txt
When they do not
| Situation | Lockfile protects you? | Why |
|---|---|---|
npm ci in CI with a lockfile committed last week | Yes | Exact versions and hashes from before the bad release |
npm install with no arguments on a laptop | Usually | npm respects the lockfile but can update it if package.json ranges no longer match |
npm install new-package or npm update | No | Resolves fresh versions and rewrites the lockfile |
| An automated dependency update pull request merged the same day | No | The new lockfile can contain the bad version |
npx tool, npm install -g tool, pipx install tool | No | No project lockfile involved |
RUN pip install litellm in a Dockerfile | No | Resolves the newest version at build time |
pip install -r requirements.txt with ranges and no hashes | No | Ranges resolve to the newest match |
pip freeze > requirements.txt used as a lock | Partly | Pins versions but, as PyPI points out, omits checksums |
| A lockfile generated during the bad window | No | It faithfully records the malicious version |
The last row catches teams out. A lockfile records a past resolution; it does not judge it. If someone ran npm install on a bad day and committed the result, every later npm ci installs the bad version perfectly. So review lockfile changes in pull requests, and pair the lockfile with a cooldown: the cooldown stops a fresh resolution picking something brand new, and the lockfile stops anything changing afterwards.
Should I set a minimum release age or cooldown, and will it delay security fixes?
Yes, set one. Three days is a reasonable default for most teams; a week is defensible for production services. It will delay fixes unless you give security updates an explicit bypass, so build that bypass in from day one.
The settings, per tool
| Tool | Setting | Unit | Default | Per-package exception |
|---|---|---|---|---|
| npm | min-release-age in .npmrc | Days | Off (null) | Override on the command line for one install |
| pnpm | minimumReleaseAge in pnpm-workspace.yaml | Minutes | 1440 (one day) in current docs | minimumReleaseAgeExclude |
| Yarn | npmMinimalAgeGate in .yarnrc.yml | Duration such as 1w | 1w in current docs | npmPreapprovedPackages |
| uv | exclude-newer in uv.toml or pyproject.toml | Timestamp or duration such as 3 days | Off | exclude-newer-package, set a package to false |
| pip | uploaded-prior-to option | ISO 8601 date or duration such as P3D | Off | Run a one-off install with P0D |
Sources: npm config, pnpm supply chain security, Yarn configuration, uv settings, pip install options.
Configuration that implements a three day cooldown:
# .npmrc (needs a recent npm; check npm config docs for min-release-age)
min-release-age=3
# pnpm-workspace.yaml
minimumReleaseAge: 4320 # minutes, 3 days
minimumReleaseAgeExclude:
- "@your-org/*" # your own packages, published by your own pipeline
# .yarnrc.yml
npmMinimalAgeGate: "3d"
npmPreapprovedPackages:
- "@your-org/*"
# pyproject.toml
[tool.uv]
exclude-newer = "3 days"
# pip: per command
pip install --uploaded-prior-to P3D -r requirements.txt
Two caveats for pip: its documentation says the option is "Only effective when using indexes that provide upload-time metadata", so check your internal mirror, and PyPI's report says the flag arrived in pip 26.0 with the config setting in 26.1.
Will it delay security fixes?
Yes, if you let it apply to everything. When a critical CVE lands and the fixed release is six hours old, a three day cooldown blocks the fix. The LiteLLM discussion on Hacker News raised exactly this: one commenter reported that cooldowns delay security patches and need manual exceptions, and others pointed out that a fixed, public cooldown lets a patient attacker time a release to mature before anyone installs it (HN thread).
The practical answer has three parts:
- Write down the bypass. For npm, install the fixed version with an override for that one command. For pnpm, Yarn and uv, add the package to the exception list in the same pull request as the fix, with a comment and an expiry date. For pip, run the one install with
P0D, which PyPI's own report suggests. - Gate the bypass on evidence. Only bypass for a version that an advisory names as the fix, and confirm the version against the advisory or a lookup such as Gold Open Source before you merge.
- Keep the cooldown short enough to be honest. A 30 day cooldown will be bypassed so often that it stops meaning anything. Three to seven days catches the documented incidents without becoming routine paperwork.
PyPI's report also notes that common automated update tools already let security updates skip their own cooldowns by default. If you use one, check that setting rather than assuming it. We go deeper on the trade-offs in minimum release age and cooldown policies.
How do I turn off install scripts by default without breaking native modules?
Turn scripts off globally, then allow the handful of packages that genuinely need them. Most projects need fewer than five.
npm
npm runs scripts by default. The ignore-scripts setting defaults to false; when true, "npm does not run scripts specified in package.json files".
# 1. Turn scripts off for this project (or use ~/.npmrc for your user)
echo "ignore-scripts=true" >> .npmrc
# 2. Install as usual
npm ci
# 3. Find which installed packages declare install-time scripts
npm query ':attr(scripts, [postinstall])' | grep '"name"'
npm query ':attr(scripts, [install])' | grep '"name"'
# 4. Rebuild only the ones you trust and need
npm rebuild esbuild sharp --ignore-scripts=false
Common legitimate needs are image libraries, bundlers with native cores and database drivers, and many of these now ship prebuilt binaries as optional platform packages that need no script. Test with scripts off before adding anything back, and keep the allowed list in the repository so a reviewer sees when it grows.
pnpm
pnpm made this the default. Its supply chain security page says "pnpm v10 disables the automatic execution of postinstall scripts in dependencies." You approve exceptions with pnpm approve-builds, which writes an allowBuilds map:
pnpm approve-builds esbuild sharp !core-js
# pnpm-workspace.yaml
allowBuilds:
esbuild: true
sharp: true
core-js: false
pnpm warns against the global dangerouslyAllowAllBuilds switch. Two other pnpm settings are worth turning on at the same time: trustPolicy: no-downgrade, which refuses a package whose trust level has dropped since earlier versions, and blockExoticSubdeps: true, which stops transitive dependencies from pulling from git URLs or tarballs.
Yarn
Current Yarn documentation lists enableScripts with a default of false, meaning third-party postinstall scripts do not run, and dependenciesMeta in package.json lets you re-enable one package. Older Yarn versions behaved differently, so check yarn --version and set the value explicitly rather than relying on the default.
What you give up
Scripts off breaks packages that compile on install with no prebuilt binary for your platform. The fix for each is one line in the allowed list. In return, a hijacked release of any of your other dependencies cannot run code at install time, though it can still run when imported, which is why scripts off pairs with a cooldown.
How do I check, in minutes, whether a compromised version reached my laptop, CI cache or container images?
Work from the advisory. Every credible advisory names a package and one or more versions. Your job is to answer "does that exact version exist anywhere we run code", in this order: laptops and dev containers, CI runners and caches, built images, and anything already deployed.
Step 1: Confirm the bad versions from a primary source
Take the version list from the maintainer's notice, the registry, a national CERT or an OSV entry, not from a social media post. You can query OSV directly:
curl -s -d '{"package":{"name":"litellm","ecosystem":"PyPI"},"version":"1.107.0"}' \
https://api.osv.dev/v1/query | python3 -m json.tool | head -40
A MAL- identifier in the response means the version is recorded as malicious.
Step 2: Search repositories and lockfiles
# npm: is the package anywhere in the tree, at which versions?
npm ls keyv --all
# Search every lockfile in a directory of checkouts for one exact version
grep -rn --include=package-lock.json -A2 '"node_modules/keyv"' ~/code | grep '"version": "6.0.0"'
grep -rn --include=pnpm-lock.yaml 'keyv@6.0.0' ~/code
grep -rn --include=yarn.lock -A1 '^"\?keyv@' ~/code | grep '6.0.0'
# Python lockfiles and requirements
grep -rn --include=uv.lock -A1 'name = "litellm"' ~/code | grep '1.107.0'
grep -rn --include='requirements*.txt' -i 'litellm==1.107.0' ~/code
npm ls shows transitive copies too, which matters because most people who received a compromised package never asked for it by name. The CSA advisory on the August npm wave lists keyv, cacheable, flat-cache and file-entry-cache, which are deep dependencies of common build tools.
Step 3: Check what is actually installed on the machine
# Global npm packages
npm ls -g --depth=0
# Python: every interpreter and virtual environment you use
python3 -m pip show litellm 2>/dev/null | head -3
uv pip show litellm 2>/dev/null | head -3
# Look for .pth files that execute code, in the active environment
SITE=$(python3 -c 'import site; print(site.getsitepackages()[0])')
grep -l '^import' "$SITE"/*.pth
ls -la "$SITE"/sitecustomize.py 2>/dev/null
Most .pth files contain only paths. A handful of well-known tools legitimately use import lines, so review the list rather than deleting everything that matches. For a specific incident, use the indicator files the advisory names; for LiteLLM 1.107.0, the independent analysis lists cook.pth, cook.py and sitecustomize.py, along with marker files in /tmp and a detached Python process.
Step 4: Clear local caches
Package caches are an easy way for a removed version to come back on the next offline or cached install.
npm cache clean --force
pnpm store prune
yarn cache clean
pip cache purge
uv cache clean litellm
If you want evidence before clearing the npm cache, its index records the tarball URLs it fetched:
grep -rl 'keyv/-/keyv-6.0.0.tgz' ~/.npm/_cacache/index-v5 | head
Step 5: Check CI caches and runners
CISA's Shai-Hulud alert specifically says to "Search for cached versions of affected dependencies in artifact repositories and dependency management tools" (CISA). That includes CI dependency caches, internal registry mirrors and self-hosted runners that keep state between jobs.
# GitHub Actions caches for a repository
gh cache list --limit 100
gh cache delete --all # after you have recorded what was there
Then check build logs for the window. A job that ran npm install or pip install without a lockfile between the publish time and the removal time is a candidate, and on a self-hosted runner the runner itself may be compromised.
Step 6: Check container images
Images built during the window can carry the bad version long after the registry has removed it.
# Python image
docker run --rm --entrypoint sh your-image:tag -c 'pip show litellm 2>/dev/null | head -2'
# Node image
docker run --rm --entrypoint sh your-image:tag -c 'cd /app && npm ls keyv --all'
Running a suspect image to inspect it is itself a risk for Python images, because the .pth payload runs when the interpreter starts. Prefer inspecting the image without starting Python, for example by scanning the image or its SBOM. With the Safeguard CLI:
safeguard scan --image your-image:tag
safeguard scan --sbom ./sbom.cdx.json
safeguard scan --dir .
Results land as JSON under ./output, and the Safeguard platform shows which projects contain a given component and version, so one search answers the question across every repository, image and SBOM you have scanned. For more on running this kind of search at scale, see finding affected products fast with SBOMs.
If I installed a bad version, what do I rotate, and in what order?
Treat the machine as compromised, rotate from a different machine, and start with the credentials that let the attack spread. Both the CSA advisory and the independent analysis say to treat affected systems as compromised and rebuild where you can. The order matters because modern payloads install a backdoor that can capture a newly rotated key on the same host.
- Isolate first. Disconnect the machine or runner from the network, stop the job, and do not log in to anything new from it. For LiteLLM 1.107.0, the independent analysis describes an hourly backdoor process; rotating keys while it runs only hands over the new ones.
- From a clean device, revoke publishing and source control tokens. npm tokens, PyPI tokens, GitHub personal access tokens and GitHub CLI OAuth tokens, deploy keys, and CI tokens. These are what worms use to publish the next wave. Review recent npm and PyPI publishes under your accounts and recent pushes, new repositories and workflow changes in your GitHub organization. The CSA advisory says to watch for repositories whose description contains "Shai-Hulud: Here We Go Again".
- Rotate CI and CD secrets. Anything stored in your CI system that the job could read, including cloud deploy credentials and signing keys.
- Rotate cloud credentials. Access keys, service account keys, and any instance metadata credentials the host could reach. Review cloud audit logs for activity from unfamiliar addresses since the install time.
- Replace SSH keys and GPG keys. Generate new keys on a clean device, update authorized keys on servers and on your source control host, and remove the old ones.
- Rotate Kubernetes, Terraform and Vault credentials. Kubeconfig tokens, state backend credentials and anything in local configuration files.
- Rotate AI provider and SaaS API keys. LLM provider keys, AI coding tool tokens and keys in
.envfiles. The independent analysis notes the October payload also targeted LLM keys stored in a LiteLLM proxy database. - End browser sessions and change saved passwords. If the payload collected browser data, sign out of sessions and change passwords that were saved in the browser.
- Rebuild the machine or runner. Reimage the laptop or destroy the runner. Rebuild images from a known clean lockfile and redeploy.
- Write down the timeline. Install time, removal time, what ran and what you rotated when.
Our compromised dependency incident response playbook covers the communication and evidence side in more detail.
What is different about pip and uv compared with npm?
npm has one switch that stops install-time code. Python does not, because the risky code paths are different.
| Risk | npm, pnpm, Yarn | pip | uv |
|---|---|---|---|
| Code at install time | Lifecycle scripts | Source builds (setup.py, build backends) | Source builds |
| Off switch for install-time code | ignore-scripts; off by default in pnpm 10+ and current Yarn | --only-binary :all: refuses sdists | no-build refuses sdists |
| Code that runs later without import | No direct equivalent | .pth import lines and sitecustomize.py | Same as pip |
| Lockfile with hashes | Built in | Not built in; use hashed requirements | uv.lock, built in |
| Cooldown | min-release-age (npm), minimumReleaseAge (pnpm), npmMinimalAgeGate (Yarn) | uploaded-prior-to | exclude-newer |
The practical Python policy has four parts:
# 1. Lock with hashes (pick one approach)
uv lock
pip-compile --generate-hashes -o requirements.txt requirements.in
# 2. Install from the lock only, wheels only, with a cooldown
uv sync --locked --no-build
pip install --require-hashes --only-binary :all: --uploaded-prior-to P3D -r requirements.txt
# 3. Install your own project without pulling unchecked dependencies
pip install --no-deps -e .
The --no-deps step comes straight from the pip secure installs guide, which says to install your project with it "to be safe".
The fourth part is the uncomfortable one: wheels-only and hashes do not stop a malicious wheel. The LiteLLM releases were wheels. Their payload ran through .pth files at interpreter start, not through a build. Hashes would have helped only if your lock was written before the bad release, and wheels-only did nothing. The defense that covers that case is the cooldown, plus a habit of reviewing lockfile diffs for new top-level .pth or sitecustomize.py files when you can.
If a package has no wheel for your platform and you must build it, use no-build-package (uv) or a targeted pip install for that one package, in a build container with no credentials mounted.
What should a team-wide install policy look like?
Individual settings help one laptop. A team needs the same protections enforced where installs actually happen: developer machines, CI, image builds and AI coding agents that run installs on your behalf.
The policy, in one page
- Every project has a committed lockfile with integrity hashes. CI fails if it is missing or out of date (
npm ci,pnpm install --frozen-lockfile,yarn install --immutable,uv sync --locked,pip install --require-hashes). - Dependency install scripts are off by default. An allowed list lives in the repository and changes go through review.
- A cooldown of three to seven days applies everywhere, with a written bypass for security fixes that names the advisory.
- Installs go through an internal registry proxy. The proxy applies the cooldown centrally, keeps a copy of every artifact you have used, and lets you block a version for the whole company in one place. Per the OpenSSF guidance, private registries should not fall back to the public registry for your internal names, and you should register your scope on the public registry so nobody else can.
- No unpinned installs in Dockerfiles or CI scripts.
- Installs run without secrets in the environment. Split dependency installation into its own step that has no cloud, registry publish or deploy credentials. Inject secrets only into the later steps that need them.
- Restrict network egress from build jobs so they can reach only your proxy.
- Check signatures and provenance where they exist.
npm audit signaturesverifies registry signatures and provenance attestations. The npm provenance documentation is clear that provenance does not guarantee a package "has no malicious code"; it tells you where and how it was built. A drop in provenance between versions is a signal worth blocking on, which is what pnpm'strustPolicy: no-downgradedoes. - Publish your own packages with trusted publishing and short-lived tokens. GitHub's npm roadmap moves publishing to 2FA, seven day granular tokens and trusted publishing (GitHub blog). PyPI recommends trusted publishers and phishing resistant 2FA.
- AI coding agents follow the same rules. An agent that runs
npm install some-packageorpip installon your behalf bypasses the lockfile just like a person would. Make it ask before installs, and make the same.npmrcanduv.tomlsettings apply in its environment. See whether AI coding agents add vulnerable dependencies for the evidence and controls.
A CI job that applies most of it
# .github/workflows/build.yml
name: build
on: [pull_request]
permissions:
contents: read
jobs:
install-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install from lockfile, no scripts, with cooldown
run: |
npm config set ignore-scripts true --location=project
npm config set min-release-age 3 --location=project
npm ci
npm rebuild esbuild --ignore-scripts=false
- name: Verify registry signatures and provenance
run: npm audit signatures
- name: Test
run: npm test
Note what is missing: secrets. The job has read-only repository access and nothing worth stealing; deployment runs in a separate job with its own narrowly scoped credentials.
Choosing your controls
| Control | Stops | Does not stop | Cost |
|---|---|---|---|
Lockfile plus npm ci or frozen install | New bad versions of existing dependencies | Bad versions already in the lockfile; ad hoc installs | Low |
| Cooldown of 3 to 7 days | Fresh hijacked releases removed within days | Slow attacks that wait out the cooldown; malicious code that sits unnoticed | Low, plus a bypass process |
| Install scripts off | Install-time payloads in npm packages | Code that runs on import; Python .pth payloads | Low to medium |
Wheels only (--only-binary, no-build) | Python build-time code | Malicious wheels | Medium for packages without wheels |
| Hash-checked installs | Tampered artifacts and silent substitution | A bad version hashed into the lock | Low |
| Registry proxy with central blocking | Repeat installs after a version is flagged; public fallback for internal names | The first hours before anyone flags it, unless the proxy enforces a cooldown | Medium |
| No secrets during install | Credential theft from CI | Theft from laptops with keys on disk | Low |
| Egress restrictions | Exfiltration from build jobs | Laptops; allowed destinations | Medium |
| Signature and provenance checks | Packages not built where they claim | A pipeline that was itself compromised | Low |
On the last row: the independent analysis assesses that the October LiteLLM wheels were built by the release pipeline itself from attacker-modified source, so provenance would not have flagged them. It is still worth checking, because attacks that publish from a stolen token outside the real pipeline lose their attestation.
So, should I run npm install right now?
Use this as a quick decision guide.
| What you are about to run | Is it safe right now? | What to do instead |
|---|---|---|
npm ci or a frozen install from a lockfile you reviewed | Yes, as safe as it gets | Keep scripts off and run it without secrets in the environment |
npm install to add a new package | Usually, with a cooldown | Check the package and version first, confirm it is more than a few days old, read the lockfile diff |
npm update or merging a fresh dependency update | Wait a few days | Let the cooldown filter it, or review each bumped version |
npx some-cli or a global install | Riskier | Pin the version (npx some-cli@1.2.3) and run it in a container or throwaway environment |
pip install some-package with no version | Riskier | Pin, hash, prefer wheels, apply uploaded-prior-to |
| Installing during an active, named incident | Not until you have checked | Search your lockfiles for the named versions first, then install with a cooldown |
To vet one package before adding it, see is this npm package safe to install and the checklist in npm package safety checks before you install. For the history of the worm family behind the 2025 and 2026 npm waves, see the Shai-Hulud npm supply chain attack explained.
Common mistakes
- Treating the lockfile as a safety certificate. A lockfile generated during the bad window reproduces the bad version forever. Review lockfile diffs like code.
- Setting
ignore-scriptson laptops but not in CI or Dockerfiles. CI runners hold the most valuable secrets. Put the setting in the project.npmrcso it travels with the repository. - Assuming wheels are safe. Python malware in 2026 shipped as wheels and ran through
.pthfiles at interpreter start. - Inspecting a suspect Python image by running Python inside it. That starts the interpreter and runs the payload. Scan the image or its SBOM instead.
- Rotating keys on the infected machine. If a backdoor is running, it receives the new keys too. Rotate from a clean device.
- Rotating cloud keys before publishing tokens. Worms spread through npm, PyPI and GitHub tokens. Cut those first.
- A cooldown with no bypass. Teams then disable it entirely during the first urgent CVE and never turn it back on.
- Letting AI coding agents install freely. An agent running
pip installmid-task skips every control that lives in your CI pipeline.
Fix it without waiting for upstream
A cooldown and a lockfile protect you from brand new bad versions, but they also slow down the good ones, and the same habit of waiting leaves known vulnerable versions in place for months. Closing that second gap is how you make "install from the lockfile" mean "install something safe".
In incidents like this one, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
FAQ
Is it safe to npm install right now?
For most projects, yes, if you install from a committed lockfile with npm ci, keep dependency install scripts off and use a cooldown of a few days for anything new. The documented incidents of 2025 and 2026 were removed within hours, so the risk sits almost entirely in installing brand new versions.
Is pip install safe?
Not by default: source builds run code, and a wheel can drop .pth files that run at every Python start. Pin with hashes, prefer wheels, add a cooldown and install without credentials in the environment.
Does npm ci protect against supply chain attacks?
It protects against new malicious versions of dependencies already in your lockfile, because it installs exactly what the lockfile records and fails if package.json disagrees. It does not help if the lockfile already contains a bad version or if someone runs npm install new-package.
Should I use ignore-scripts in npm?
Yes, for most projects. Set it in the project .npmrc, then rebuild the few packages that genuinely need install scripts. pnpm and current Yarn already default to blocking dependency scripts.
What is npm min-release-age?
An npm configuration option that installs only versions that have been available for more than a given number of days. Secondary sources date it to npm 11.10. Set min-release-age=3 in .npmrc for a three day cooldown.
Will a dependency cooldown stop me getting security patches?
It will delay them unless you write down a bypass, used only for a version an advisory names as the fix.
How do I know if I installed a compromised npm package?
Take the exact versions from the advisory, run npm ls <package> --all in each project, search lockfiles across your checkouts, check global installs, clear and search caches, and scan built container images. If the version is present and was installed, treat the machine as compromised.
What should I rotate after installing a malicious package?
From a clean device: publishing and source control tokens first (npm, PyPI, GitHub), then CI secrets, cloud keys, SSH and GPG keys, Kubernetes and Terraform credentials, AI and SaaS API keys, and browser sessions. Then rebuild the machine or runner.
Are lockfiles enough to stop malicious packages?
No. A lockfile written during a bad window records the bad version, and npx, global installs and unpinned Dockerfile lines never use one.
Does npm provenance mean a package is safe?
No. npm's documentation says provenance does not guarantee a package has no malicious code. It shows where and how the package was built, which helps catch packages published from stolen tokens outside the official pipeline.
See how Safeguard would help
The checks in this post come down to two jobs: finding where a named package version lives across your machines, repositories and images, and replacing it with a safe one under human control. The demos below show those flows. When you are ready to try it on your own code, download the apps at safeguard.sh/download and read the CLI documentation.
This first demo sets up the Safeguard runner on your own machine, approves it with its device code, and then scans from the terminal. It is the same flow you would use in Step 6 above to scan a repository or a suspect container image locally instead of starting the image and risking the payload.
Click through it at your own pace, or open it full screen.
This second demo is a real Safeguard CLI session. You ask it to fix the lodash CVE in a repository; it reads the finding and the advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing. Because every install command waits for your approval, you decide whether it runs, and you can check that your .npmrc cooldown and script settings are in place first.
Click through it at your own pace, or open it full screen.
The third demo is a real Android session: you send work to a computer running the CLI and approve the command on the phone with Allow once, Allow for turn or Deny. During an incident that means you can start the lockfile search from your phone and still see, and approve, each command before it runs on the workstation.
Click through it at your own pace, or open it full screen.
Quick check
Why did a wheels-only, hash-checked pip install not protect against the October 2026 LiteLLM release?
The malicious release was itself a wheel, and its payload ran from a .pth file at every Python interpreter start, not from a build step. Hashes help only if your lock predates the bad version. A cooldown of a few days would have skipped it, because PyPI removed it in under two hours.
After installing a compromised package, which credentials should you rotate first, and from where?
Publishing and source control tokens first (npm, PyPI and GitHub), because worms use them to spread, and always from a clean device, because a backdoor on the affected machine would capture the new keys.
Sources
- TechCrunch: The internet is having a very bad cybersecurity day (21 October 2026)
- LiteLLM documentation: security posts, including the 21 October 2026 incident notice
- OSV: MAL-2026-2144, malicious code in litellm (PyPI)
- PyPI blog: Incident report, LiteLLM and Telnyx supply chain attack
- PyPI blog: Project quarantine
- Cyber Security Agency of Singapore: Advisory AD-2026-009
- CISA: Widespread supply chain compromise impacting npm ecosystem (23 September 2025)
- GitHub blog: Our plan for a more secure npm supply chain
- npm documentation: config
- npm documentation: npm ci
- npm documentation: scripts
- npm documentation: generating provenance statements
- OpenSSF: npm package manager best practices
- pnpm: supply chain security
- pnpm: approve-builds
- Yarn: configuration (yarnrc)
- uv: settings reference
- pip: secure installs
- pip: install options
- Python documentation: the site module and .pth files
- OSV: query API example
- Hacker News discussion of the LiteLLM 1.107.0 compromise
Last reviewed: 22 October 2026. Package manager settings change between releases; check your tool's current documentation before rolling out a policy.