---
title: "Quick npm and pip Security Hardening Tweaks for Supply Chain Attacks"
description: "After recent supply-chain attacks, a practical npm and pip hardening pass: disable install scripts, delay fresh packages, require hashed Python installs, and keep exceptions explicit."
date: 2026-05-26
slug: "quick-npm-pip-security-hardening-tweaks-supply-chain-attacks"
tags: ["security", "npm", "python", "devops", "supply-chain"]
social_post: |
  Recent supply-chain attacks changed one boring default for me: package installs should not run arbitrary setup code.

  Quick npm + pip tweaks: disable scripts, add a 7-day age gate, require hashed wheel-only Python installs, and sandbox exceptions.
---

After the recent wave of supply chain attacks, I changed one boring default on
my servers and dev machines:

**Package installs should not run arbitrary setup code unless I explicitly ask
for it.**

This is not theoretical anymore. The
[XZ backdoor](https://openssf.org/blog/2024/03/30/xz-backdoor-cve-2024-3094/)
showed how much damage a trusted upstream package can do.
[`tj-actions/changed-files`](https://www.aquasec.com/blog/github-action-tj-actions-changed-files-compromised/)
showed how quickly CI secrets can leak. The
[Axios npm compromise](https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan)
used a dependency with a `postinstall` payload. Microsoft's
[Mini Shai-Hulud write-up](https://www.microsoft.com/en-us/security/blog/2026/05/20/mini-shai-hulud-compromised-antv-npm-packages-enable-ci-cd-credential-theft/)
is another reminder that attackers are targeting CI/CD credentials directly.

So here is the quick hardening pass I now want on machines that run `npm install`
or `pip install`.

## npm: disable install scripts and delay new packages

Put this in your user npm config:

```bash
npm config set --location=user ignore-scripts true
npm config set --location=user allow-git none
npm config set --location=user min-release-age 7
npm config set --location=user audit true
npm config set --location=user strict-ssl true
npm config set --location=user save-exact true
```

The two important ones:

- `ignore-scripts=true` stops npm lifecycle scripts such as `preinstall`,
  `install`, and `postinstall` from running automatically.
- `min-release-age=7` tells npm to avoid package versions published less than
  seven days ago.

The age gate is not magic. A malicious package can wait. But a lot of registry
attacks get noticed in the first hours or days, and I do not want my servers or
CI runners to be in the first wave.

You need a recent npm for `min-release-age`. I moved my server to Node `24.16.0`
through nvm, which bundled npm `11.13.0`. I did not upgrade npm separately.

## pip: wheel-only, hashes, and the same delay

pip is different from npm. It does not have the same general `postinstall`
lifecycle model, but source builds can still execute package build code. So the
safer default is: wheels only, hashes required, and no fresh uploads.

```bash
python3 -m pip config --user set install.only-binary ':all:'
python3 -m pip config --user set install.require-hashes true
python3 -m pip config --user set install.uploaded-prior-to P7D
python3 -m pip config --user set global.no-input true
```

This makes casual installs fail. That is intentional.

If I type:

```bash
python3 -m pip install boltons==25.0.0
```

I want pip to complain that hashes are missing. Server installs should come from
a pinned, hashed requirements file, not from whatever PyPI returns right now.

pip documents both
[hash-checking mode](https://pip.pypa.io/en/stable/topics/secure-installs/#hash-checking-mode)
and
[`uploaded-prior-to`](https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-uploaded-prior-to).

## Change install habits too

Config helps, but habits matter.

For Node projects, prefer:

```bash
npm ci
```

over:

```bash
npm install
```

Keep `package-lock.json` committed.

For Python projects, install from a locked file with hashes:

```bash
python3 -m pip install -r requirements.lock.txt
```

If a project cannot do this yet, that is useful information. It means the
install path still depends on trust and timing.

## Keep an explicit exception path

Some packages really do need install scripts. Native modules compile things.
Some tools download platform binaries. Some repos use `prepare` to install Git
hooks.

Fine. Make that explicit.

Run trusted setup commands yourself:

```bash
npm run prepare
```

Or build native dependencies in a disposable environment with no secrets:

- no SSH agent
- no npm or PyPI token
- no cloud credentials
- no production `.env`
- limited outbound network

Then copy the artifact into the real environment.

The point is not "never run setup code." The point is "do not let every
transitive dependency run setup code by default."

## Where I would start

Start with servers and CI:

1. Disable npm install scripts.
2. Add the seven-day npm release-age gate.
3. Make pip wheel-only, hash-required, and seven-day delayed.
4. Use `npm ci` and locked Python requirements.
5. Move exceptions into a no-secrets sandbox.

This does not solve every supply chain problem. It will not catch malicious code
that runs when you import a package. It will not save you from every compromised
maintainer account.

But it removes one dangerous default:

**Installing dependencies should not automatically execute code on machines full
of secrets.**

That is a small change with a lot of upside.

## References

- [npm config docs](https://docs.npmjs.com/cli/v11/using-npm/config/)
- [pip secure installs](https://pip.pypa.io/en/stable/topics/secure-installs/)
- [pip install options](https://pip.pypa.io/en/stable/cli/pip_install/)
- [Microsoft: Mini Shai-Hulud](https://www.microsoft.com/en-us/security/blog/2026/05/20/mini-shai-hulud-compromised-antv-npm-packages-enable-ci-cd-credential-theft/)
- [StepSecurity: Axios compromised on npm](https://www.stepsecurity.io/blog/axios-compromised-on-npm-malicious-versions-drop-remote-access-trojan)
