GitHub to Disable npm Install Scripts by Default: What It Means for Your CI/CD Pipelines

ยท

GitHub has announced a set of “breaking changes” coming with npm version 12, due for release next month. The headline change is that npm install will no longer automatically execute lifecycle scripts (like preinstall, install, and postinstall) from your dependencies unless you explicitly approve them. GitHub describes these install-time scripts as the single largest code-execution surface in the npm ecosystem, since a single compromised package buried deep in your dependency tree can currently run arbitrary code on your machine or your CI runner the moment you run npm install.

To understand why this matters so much, it helps to step back and look at how pipelines work, how GitHub Actions fits into that picture, and how the long-running confusion between npm install and npm ci ties directly into this security story.

What is a pipeline?

A pipeline (often called a CI/CD pipeline, for Continuous Integration / Continuous Deployment) is an automated sequence of steps that takes your code from “just written” to “tested, built, and deployed” without a human manually running each command. A typical pipeline might:

  • Check out your repository’s code
  • Install dependencies
  • Run linters and automated tests
  • Build the application (compile, bundle, transpile)
  • Deploy the result to a server, container registry, or hosting platform

The point of a pipeline is consistency and speed. Every commit or pull request triggers the exact same sequence of steps, on a clean machine, so you catch problems early and avoid “it works on my machine” surprises.

How pipelines work with GitHub Actions

GitHub Actions is GitHub’s built-in automation platform for running pipelines. You define a workflow as a YAML file inside a .github/workflows folder in your repository. A workflow is triggered by an event, such as a push, a pull request, or a scheduled time.

Each workflow is made up of one or more jobs, and each job runs on a “runner,” which is a fresh virtual machine spun up specifically for that job. Inside a job, you define a series of steps, which are individual commands or reusable “actions” published by GitHub or the community. A very common Node.js workflow looks something like this:

  1. Check out the repository code
  2. Set up the correct version of Node.js
  3. Install dependencies, typically with npm
  4. Run the test suite
  5. Build and optionally deploy the project

That third step, installing dependencies, is exactly where this new security change lands, because it’s the step that runs npm install or npm ci, pulling in not just your direct dependencies but everything they depend on too.

npm install vs npm ci: what’s the difference?

Both commands install your project’s dependencies, but they behave quite differently:

npm install reads your package.json, resolves version ranges (like ^1.2.3), and will update your package-lock.json if the resolved versions have changed. It’s flexible and is the right tool for day-to-day development when you’re adding or updating packages.

npm ci requires an existing package-lock.json and installs exactly the versions specified there, with no resolution or updates. It first deletes node_modules entirely for a guaranteed clean install, and if package.json and the lock file are out of sync, it errors out rather than trying to reconcile them. This makes it faster and far more reproducible, which is why it’s the standard recommendation for CI/CD pipelines.

Crucially, both commands have historically run lifecycle scripts from every package in the dependency tree by default. That’s the behaviour npm 12 is changing.

How this change helps stop supply chain attacks

A “software supply chain attack” happens when an attacker compromises a package somewhere in your dependency tree, then has malicious code run automatically when developers or CI systems install it. Lifecycle scripts are the perfect vehicle for this: a package can define a postinstall script that, say, exfiltrates environment variables, downloads a second-stage payload, or modifies other files on disk, all without the developer ever running that code directly.

The changes coming in npm 12 close off several of these paths:

  • npm install will no longer run preinstall, install, or postinstall scripts from dependencies unless they’re explicitly allowed for that project, including implicit node-gyp rebuilds for native packages.
  • Git dependencies (direct or transitive) will no longer be resolved unless explicitly allowed with --allow-git.
  • Dependencies fetched from remote URLs, such as tarballs over HTTPS, will no longer be resolved unless explicitly allowed with --allow-remote.
  • prepare scripts from git, file, and link dependencies are blocked in the same way.

This also closes a sneaky bypass where a Git dependency’s own .npmrc file could override which Git executable npm uses, even when the --ignore-scripts flag was set.

What developers should do now

GitHub recommends upgrading to npm 11.16.0 or newer ahead of the npm 12 release, then running a normal install and reviewing the warnings it produces about packages with scripts. From there, you can run npm approve-scripts --allow-scripts-pending to review which packages have scripts, approve the ones you genuinely trust, and commit the updated package.json. Once you’re on npm 12, only the scripts you’ve explicitly approved will continue to run, and anything unapproved simply won’t execute.

For teams relying on GitHub Actions, this is worth testing in a branch before it lands, since a CI pipeline that depends on an unapproved postinstall script (for example, one that builds a native binary) could suddenly fail once the new defaults take effect. The safest approach is to audit your dependency tree now, understand which packages genuinely need install-time scripts, and approve only those.


Leave a Reply