Back to publishing & release Automate semantic versioning Publish to the npm registry Harden the supply chain

Blocking Malicious Install Scripts with --ignore-scripts

A dependency's postinstall script is arbitrary code that runs on every install, including in CI — a prime supply-chain attack vector. This page shows how to neutralize lifecycle scripts with --ignore-scripts without breaking packages that genuinely need them.

Exact symptoms and error messages

Exact symptoms and error messages Exact symptoms and error messages in production JavaScript package workflows. Exact symptoms and error messages Exact symptoms and error messages in production JavaScript package workflows.
Exact symptoms and error messages — the core idea of this section at a glance.
> compromised-pkg@1.2.3 postinstall
> node ./steal-env.js
# arbitrary code executes during install, with your env and tokens

Root cause analysis

npm, pnpm, and Yarn execute preinstall/install/postinstall scripts from every dependency by default, so a single compromised transitive package can run code with your environment and credentials. Disabling scripts closes that vector; the few packages that truly need a build step (native modules) are then allow-listed explicitly. This is a core defense in Supply-Chain Security Hardening, paired with the frozen lockfile installs that keep the graph fixed.

Root cause analysis npm, pnpm, and Yarn execute preinstall/install/postinstall scripts from every dependency by default, so a single comprom Root cause analysis npm, pnpm, and Yarn execute preinstall/install/postinstall scripts from every dependency by default, so a single compromised transitive package can run code wit
Root cause analysis — the core idea of this section at a glance.

Lifecycle scripts run automatically on install with the full privileges of the installing user, which in CI often means access to environment secrets and publish tokens. A single compromised transitive dependency — anywhere in the graph — can therefore execute arbitrary code during a routine npm install, exfiltrate credentials, or tamper with the build. Disabling scripts closes that vector wholesale; the few packages that genuinely need a build step are then re-enabled explicitly.

The reason a blanket block is safe is that most packages do not need install scripts at all — they are pure JavaScript that requires no compilation. The exceptions are native modules like esbuild or sharp that build a binary on install. Allow-listing those specific, vetted packages while blocking everything else gives you the security of default-deny with the functionality of the handful of builds you actually trust.

Lifecycle scripts are arbitrary code that a package manager runs automatically on install, with the full privileges of the installing user, and every dependency in the tree can declare them. A preinstall, install, or postinstall from any transitive package executes during a routine npm install — in CI, often with access to environment secrets, cache-write tokens, and deploy credentials. This is a first-class supply-chain vector: a single compromised package deep in the graph can exfiltrate credentials or tamper with the build the moment it is installed, before any of your code runs.

The vector is exploitable precisely because most developers never see the scripts execute. Installation is routine and its output is noise, so a malicious postinstall that runs silently and then behaves normally is easy to miss. Blocking scripts wholesale removes the vector; the small number of packages that genuinely need a build step — native modules that compile a binary — are then re-enabled explicitly through an allow-list, which turns a default-open surface into a default-closed one with a short, reviewed set of exceptions.

Resolution and configuration patch

Disable scripts globally and allow-list only the packages that must build:

Script gate Disable all scripts, allow-list only vetted builds. ignore-scripts block all allow-list esbuild, sharp safe install no arbitrary code
Block every lifecycle script, then allow-list the few native builds you trust.
# .npmrc
ignore-scripts=true
# pnpm: allow only trusted native builds
onlyBuiltDependencies[]=esbuild
onlyBuiltDependencies[]=sharp

Run CI installs with --ignore-scripts so no lifecycle code executes on the runner.

Disable scripts globally and allow-list only the vetted native builds that need them:

# .npmrc
ignore-scripts=true
# pnpm: only these packages may run build scripts
only-built-dependencies[]=esbuild
only-built-dependencies[]=sharp
# CI installs run no lifecycle code
npm ci --ignore-scripts

With ignore-scripts=true, no dependency's lifecycle scripts run; the pnpm allow-list re-enables exactly the native builds you have vetted, so a newly-introduced malicious dependency cannot execute simply because it declared a postinstall.

CLI validation and debug commands

CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows. CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows.
CLI validation and debug commands — the core idea of this section at a glance.
# Install without running any lifecycle scripts
npm ci --ignore-scripts
# List packages that declare install scripts
npm query ":attr(scripts, [postinstall])" 2>/dev/null || npm ls --all
# pnpm: see which builds were skipped
pnpm install --frozen-lockfile 2>&1 | grep -i 'ignored build'

Prevention and CI guardrails

  • Set ignore-scripts=true and allow-list only vetted native builds.
  • Always pass --ignore-scripts on CI installs.
  • Review any package that requires a build script before allow-listing it.
  • Combine with lockfile-lint and audit thresholds so the whole install path is constrained.
Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows. Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows.
Prevention and CI guardrails — the core idea of this section at a glance.
  • Set ignore-scripts=true and pass --ignore-scripts on every CI install.
  • Allow-list only vetted native builds with pnpm's only-built-dependencies.
  • Review each package before adding it to the allow-list — every entry is a hole in the default-deny wall.
  • Layer script blocking with an audit threshold, host allow-listing, and provenance for defense in depth.

Allow-listing the builds you actually trust

A default-deny posture only works if re-enabling the necessary builds is precise. pnpm's onlyBuiltDependencies is an explicit allow-list: scripts run for exactly the packages you name and nothing else, so a newly-introduced malicious dependency cannot execute simply because it declared a postinstall.

Vetted allow-list Default-deny scripts, re-enable only vetted native builds. ignore-scripts block all vet the package known native build onlyBuiltDependencies explicit allow
A short, reviewed allow-list keeps default-deny while permitting trusted builds.
# .npmrc
ignore-scripts=true
onlyBuiltDependencies[]=esbuild
onlyBuiltDependencies[]=sharp

Vet each package before adding it: confirm it is a known native module whose build step you understand, not an arbitrary dependency that happens to want to run code. The list should be short and reviewed, because every entry is a small hole in the default-deny wall. When a dependency update adds a new package that needs a build, adding it to the allow-list is a deliberate, reviewable decision — which is exactly the friction you want around code that runs with your credentials.

Layering script blocking with other defenses

Blocking install scripts closes one vector, but a hardened supply chain stacks several. --ignore-scripts stops arbitrary install-time code; lockfile-lint restricts which hosts packages may resolve from, blocking a typosquat served from an unexpected registry; an audit threshold fails the build on known-vulnerable versions; and provenance verification confirms a tarball came from its claimed source.

Defense in depth Script blocking plus host allow-listing, audit, and provenance. --ignore-scripts no install-time code lockfile-lint allowed hosts only audit threshold no known CVEs provenance verify trusted origin
Stacking controls forces an attacker to defeat every gate, not just one.
- run: npm ci --ignore-scripts
- run: npx lockfile-lint --path package-lock.json --allowed-hosts npm
- run: npm audit --audit-level=high

No single control is sufficient — a package can be script-free but vulnerable, or audited-clean but resolved from a malicious host. Layering them means an attacker has to defeat every gate, not just one. The install-scripts block is the first and cheapest layer; combining it with host allow-listing, audit thresholds, and provenance is what turns a single control into defense in depth.

Allow-listing the builds you actually trust

A default-deny posture only works if re-enabling the necessary builds is precise, so the allow-list must be short and deliberately reviewed. pnpm's only-built-dependencies names exactly the packages whose scripts may run — typically native modules like esbuild, sharp, or better-sqlite3 that compile a binary on install — and nothing else runs, so a dependency update that introduces a new package with a postinstall cannot execute simply by being present. Each addition to the list is a decision to trust that package's build step, which should be reviewed like any other security-relevant change.

Allow-listing the builds you actually trust A default-deny posture only works if re-enabling the necessary builds is precise, so the allow-list must be short and de Allow-listing the builds you actually trust A default-deny posture only works if re-enabling the necessary builds is precise, so the allow-list must be short and deliberately reviewed.
Allow-listing the builds you actually trust — the core idea of this section at a glance.

The discipline is to vet before adding: confirm the package is a known native module whose build step you understand, not an arbitrary dependency that happens to want to run code. When a dependency genuinely needs a build and you have verified it, adding it to the allow-list is a small, reviewable pull request that documents the exception. Keeping the list minimal — auditing it periodically and removing entries for dependencies you no longer use — keeps the default-deny wall mostly intact, with a handful of understood, trusted holes rather than a growing set nobody remembers the reason for.

Layering script blocking with the wider gate

Blocking install scripts closes one supply-chain surface, but a hardened install stacks several because no single control is sufficient. Script blocking stops install-time code execution; it says nothing about a package resolving from an untrusted host, a known-vulnerable version, or a tampered tarball. Pairing --ignore-scripts with a lockfile-lint host allow-list, a thresholded npm audit, and provenance verification covers those surfaces, so a dependency must be unable to run arbitrary code and resolved from an allowed host and free of known high-severity vulnerabilities and verifiably from its claimed source.

Layering script blocking with the wider gate Blocking install scripts closes one supply-chain surface, but a hardened install stacks several because no single contro Layering script blocking with the wider gate Blocking install scripts closes one supply-chain surface, but a hardened install stacks several because no single control is sufficient.
Layering script blocking with the wider gate — the core idea of this section at a glance.

Structured as one CI job, these checks make the install something you can reason about precisely rather than trust blindly: known hosts, verified hashes, no arbitrary execution, a frozen graph, and a provenance chain back to source. An attacker then has to defeat every layer rather than any single one, which is the essence of defense in depth for a dependency graph. Script blocking is the first and cheapest layer — a one-line .npmrc change plus a --ignore-scripts flag — and combining it with the others is what turns a routine install from the softest target in the supply chain into a controlled, inspectable step.

Auditing which dependencies declare install scripts

Before or after enabling ignore-scripts, it is worth knowing which of your dependencies actually declare lifecycle scripts, both to understand what you are blocking and to vet the ones that genuinely need to run. Most packages declare no install scripts at all — they are pure JavaScript that requires no compilation — so the set that does is usually small and dominated by native modules. Enumerating it tells you exactly which packages to consider for the allow-list and surfaces any unexpected package that runs code on install.

Audit script-runners Enumerate script-declaring deps, vet the allow-list. list ignored builds what was blocked vet each known native? prune allow-list remove unused
Knowing which dependencies run install scripts keeps the allow-list small and reviewed.
# pnpm reports packages whose build scripts were skipped
pnpm install --frozen-lockfile 2>&1 | grep -i 'ignored build'
# inspect a specific package's scripts
npm view <pkg> scripts

Reviewing that list periodically is a lightweight supply-chain practice: a dependency that newly starts declaring a postinstall after an update is worth a look before you allow it, and a package on your allow-list that no longer needs its build can be removed. Keeping the set of script-running dependencies small, known, and reviewed is what makes the default-deny posture sustainable rather than a one-time configuration that drifts as dependencies change.

Frequently Asked Questions

Won't disabling scripts break native modules?

It can — packages like esbuild or sharp build on install. Allow-list exactly those with pnpm's onlyBuiltDependencies (or run their build explicitly) while blocking everything else.

Is --ignore-scripts enough on its own?

It closes the install-script vector but not others. Combine it with audit thresholds, lockfile-lint host allow-listing, and provenance verification for defense in depth.

Won't disabling scripts break packages that need to build?

It can — native modules like esbuild and sharp build on install. Allow-list exactly those with pnpm's onlyBuiltDependencies while blocking everything else, so only vetted builds run.

Is --ignore-scripts enough on its own?

No — it closes the install-script vector but not others. A package can be script-free yet vulnerable, or audited-clean yet resolved from a malicious host. Layer it with lockfile-lint, audit thresholds, and provenance verification.

How do I decide what to add to the allow-list?

Add only known native modules whose build step you understand, and review each addition. Every entry is a small hole in the default-deny wall, so keep the list short and treat new entries as deliberate decisions.

Won't --ignore-scripts break packages that need to build on install?

It can — native modules like esbuild and sharp compile a binary on install. Allow-list exactly those with pnpm's only-built-dependencies while blocking everything else, so only vetted builds run and no other package can execute install-time code.

Is blocking install scripts enough on its own?

No — it closes only the install-time code vector. A package can be script-free yet vulnerable, or resolved from a malicious host, or tampered with. Layer it with an audit threshold, lockfile-lint host allow-listing, and provenance verification for defense in depth.

How do I decide what goes on the allow-list?

Add only known native modules whose build step you understand, and review each addition like a security change. Keep the list short, audit it periodically, and remove entries for dependencies you no longer use, so the default-deny wall stays mostly intact.

How do I see which dependencies run install scripts?

With pnpm, the install output reports packages whose build scripts were skipped under ignore-scripts (grep -i 'ignored build'), and npm view <pkg> scripts shows a specific package's lifecycle scripts. Reviewing that set tells you what to consider for the allow-list and surfaces any unexpected script-runner.

Does --ignore-scripts affect my own package's scripts?

It blocks lifecycle scripts during install, including your own postinstall, so anything your package needs at install time will not run. For your own build steps, invoke them explicitly (a build script, a prepublishOnly gate) rather than relying on an install hook that --ignore-scripts disables.

Do Yarn and npm both support blocking install scripts?

Yes. npm and pnpm honor ignore-scripts=true in .npmrc and the --ignore-scripts flag; Yarn Berry disables most lifecycle scripts by default and gates the rest through its own allow mechanism. The principle is the same across managers — block by default, allow-list the vetted native builds.

Related

Supply-Chain Security Hardening