Key takeaways

  • In the tested flow, npm install --ignore-scripts installed the synthetic dependency without running preinstall, install or postinstall.
  • The resulting lockfile still recorded hasInstallScript: true; a later npm rebuild aag-rebuild-probe ran all three hooks in documented order.
  • Passing --ignore-scripts to the rebuild suppressed the hooks again, showing that the policy is evaluated for the current command rather than remembered from installation.
  • A script-suppressed dependency tree is therefore dormant, not approved or permanently inert. Any later rebuild, wrapper or toolchain step needs the same explicit policy.
  • For untrusted or agent-modified dependencies, keep script policy in project and runner configuration, inventory pending hooks, and execute required builds only in a disposable least-authority environment.
01

The sharp question: did the safe install settle future execution?

A common containment pattern is to install a dependency tree with npm install --ignore-scripts so package lifecycle hooks cannot run during resolution. The immediate command can be clean while the operational conclusion is still wrong. Does that flag mark the installed packages as unapproved, or does it merely suppress scripts for that one invocation?

In npm 11.6.0, it was the latter. My synthetic dependency stayed silent during the ignored install. The package lock retained an indication that the dependency had an install script, and a later npm rebuild without the flag ran preinstall, install and postinstall. Nothing had to be downloaded again and package.json did not change. The authority arrived because the second command evaluated its own script policy.

02

Chronology: install suppression and rebuild are separate controls

npm has long exposed lifecycle hooks around installation. Current CLI v11 documentation lists preinstall, install and postinstall in the npm rebuild operation order. The rebuild page explains that the command reruns lifecycle scripts for matched installed packages and relinks bins; it is deliberately an execution command for repairing or recompiling dependencies.

The same command page lists ignore-scripts among rebuild configuration options. That placement is the important chronology for automation: script suppression must be present when rebuild runs. The documentation does not describe an ignored install as creating durable trust metadata, and the tested implementation has no handoff from an earlier install command to a later process.

03

The fixture was local, synthetic and intentionally boring

The checked-in audit creates a package named aag-rebuild-probe at version 1.0.0. Its only behavior is three package.json commands that append preinstall, install and postinstall to trace.log inside the installed package directory. There are no dependencies, binaries, native modules, credentials, sockets or external URLs.

The harness packs that directory into a local tarball with npm pack --ignore-scripts, computes its SHA-256, and installs the same bytes into three fresh temporary projects. This removes registry mutability and third-party behavior from the question. The marker answers only whether the selected npm command launched the declared lifecycle event.

04

Three flows isolate where execution resumes

The positive control runs npm install <local-tarball> --no-audit --no-fund. It produced the expected trace: preinstall, install, postinstall. The deferred flow runs the same install with --ignore-scripts, checks for an empty trace, and then runs npm rebuild aag-rebuild-probe without the flag. The trace appears at rebuild time in the same three-event order.

The negative rebuild control starts with another ignored install and runs npm rebuild aag-rebuild-probe --ignore-scripts. Its trace remains empty. Across the three isolated projects, this distinguishes a broken fixture from policy behavior: scripts can run, are suppressed by the install flag, resume under an ordinary rebuild, and are suppressed when the rebuild itself receives the flag.

05

The lockfile remembers capability, not approval

Both projects installed with --ignore-scripts had package-lock.json metadata at packages["node_modules/aag-rebuild-probe"].hasInstallScript equal to true. The field did not say whether a person had reviewed the command, whether the prior install intentionally deferred it, or whether a future invocation should remain blocked.

That distinction is useful. npm needs enough metadata to know a package may require building. But a capability marker is not an authorization decision. A policy engine, CI workflow or coding agent that reads only “install succeeded” can miss the dormant command surface sitting in node_modules.

06

The top-level command passes current options, not history

npm 11.6.0’s lib/commands/rebuild.js declares ignore-scripts as a rebuild parameter. At execution time it constructs Arborist with this.npm.flatOptions, loads the actual tree when package selectors are present, chooses matching nodes and invokes arb.rebuild. The file does not inspect the shell command used to create node_modules or infer an earlier trust decision.

The source evidence is bound to the locally executed file by SHA-256. That matters because online documentation can move while installed behavior remains pinned. The result applies to this npm binary and exact source; another npm release or wrapper should be treated as a new evidence object and rerun through the harness.

07

Arborist reconstructs an execution queue from installed packages

The bundled Arborist 9.1.4 rebuild implementation loads default nodes from the actual dependency tree when no explicit set is supplied. For each candidate, it inspects bin, preinstall, install, postinstall and prepare fields. If metadata says an install script exists but scripts are not already loaded, it reads package.json from disk and refreshes the node before adding it to the build set.

This explains why an ignored install can remain executable later. Suppressing the first command does not erase package scripts. Rebuild discovers them from installed state, builds queues and has enough information to run them without re-resolving the package.

08

ignoreScripts guards the current queues

In the tested Arborist source, #build constructs queues first. It runs the preinstall queue only when this.options.ignoreScripts is false, links bins according to a separate binLinks option, and then runs install and postinstall under another current-option guard. For linked dependencies it similarly guards prepare.

The early return is also narrower than a universal no-change promise: rebuild returns immediately only when ignoreScripts is true and binLinks is false. With default bin linking, the command may still inspect the tree and relink executables while suppressing lifecycle commands. “No scripts” and “no filesystem changes” are different claims.

09

This is deferred authority, not a bypass

The behavior matches npm’s documented purpose. npm rebuild is supposed to rerun package build hooks, and --ignore-scripts is available on that command. I am not reporting that npm ignored a flag, escaped a sandbox or executed an undeclared event. The mistake would be assigning durable semantics to an invocation-scoped option.

That wording matters for remediation. The fix is not to assume the package manager can remember an organization’s approval intent from one command. The fix is to make script policy explicit at every execution boundary and to separate packages that are merely installed from packages whose build procedures have been reviewed and run.

10

Coding agents make the second command easy to overlook

An agent may choose --ignore-scripts during dependency installation, run static tests, and later invoke npm rebuild to repair a native module or binary link after an environment error. A CI image may perform the ignored install in one layer and an ordinary rebuild in another. A developer bootstrap script may call rebuild because that has always fixed local machines.

In each case, review tends to focus on the install line. The later command can be far from the dependency change, owned by another script, or inherited from a base image. If the runner has source credentials, cloud tokens, writable caches or broad egress at that point, dormant package hooks receive those capabilities.

11

Persist policy at the project and runner layers

If the intended baseline is no package lifecycle execution, encode it where every npm process sees it: a reviewed project configuration, controlled environment or wrapper that fails closed. Command-line flags remain useful as visible defense in depth, but a single install line is fragile when later jobs can invoke npm independently.

Also inventory scripts before deciding to enable them. Record package name and exact version, resolved source and integrity, lifecycle command text, interpreter, expected files, network destinations and required credentials. An approval should be specific enough that an unexpected version, command or output invalidates it rather than silently inheriting trust.

12

Required builds belong in a disposable authority envelope

Some dependencies legitimately need native compilation, platform detection or generated assets. Refusing all scripts may produce an unusable tree, so the production choice is not always permanent suppression. Run the approved build in a disposable environment with no repository write token, deployment identity, signing key or unrelated workspace mount. Restrict egress to declared build inputs and capture process and filesystem evidence.

Promote the built artifact or cache only after checking expected outputs and binding them to the package, platform and toolchain. Do not let a rebuild job also hold release authority merely because it produces binaries. Build, verify, sign and deploy are separate trust transitions.

13

Independent evidence supplies risk context, not this result

The independent Backstabber’s Knife Collection paper assembled 174 historical malicious packages from npm, PyPI and RubyGems dating from November 2015 through November 2019 and modeled ways injected code can execute downstream. It supports treating package execution stages as a real supply-chain concern.

It does not test npm 11.6.0, --ignore-scripts, npm rebuild, this local fixture or current prevalence. I found no independent reproduction of this exact deferred-rebuild sequence during the run. The mechanism claim therefore rests on npm documentation, pinned implementation and the reproducible local audit; the paper is labeled only as broader historical context.

14

What failed and what remains untested

An initial design considered a local directory dependency, but directory links can introduce prepare and symlink behavior that would blur the regular installed-package question. The final harness uses one immutable local tarball and targets its exact package name. It also adds a normal-install positive control and an ignored-rebuild negative control so an empty marker cannot be mistaken for a broken script.

The audit did not test npm ci, npm install-scripts, npm approve-scripts, npm trust, workspaces, linked packages, native node-gyp defaults, bundled dependencies, optional failures, global installs, bin relinking, Windows, Linux, containers, caches, registry packages or malicious behavior. It does not show credential access, network access, exploitation or prevalence, and it is not a vulnerability disclosure.

15

Decision: treat ignored installs as dormant trees

Do not describe a dependency tree created with --ignore-scripts as permanently inert or approved. Describe it as installed with lifecycle execution deferred. Before any rebuild-like command, either enforce ignore-scripts again or move the operation into an approved, disposable builder after reviewing the exact pending hooks.

Stop or roll back when a later command runs a script absent from the execution record, the package or command differs from the approved version, outputs escape the declared set, the process reaches an undeclared destination, the builder has sensitive credentials, or a toolchain update no longer passes both positive and negative controls. Resume only after rebuilding the evidence for the exact command path.

16

Copy-ready deferred lifecycle execution record

Use this record whenever a dependency tree is installed with scripts suppressed. “Not run yet” is not the same state as “reviewed and approved.”

Entries stay in this browser tab and are not submitted to AccessAllGPT. Blank responses are copied as [Unresolved].

Exact Node and npm versions, npm integrity, Arborist version, OS/architecture, wrapper commit and configuration sources.

Lockfile digest, package name@version, resolved source, integrity, node_modules creation command and clean status.

Exact install command, ignore-scripts source and precedence, lifecycle trace absence, exit status and runner identity.

preinstall, install, postinstall, prepare, node-gyp fallback and wrappers; interpreters, expected purpose and diff from prior approval.

Every npm rebuild/install-scripts/trust/approve or wrapper path, package selector, workspace scope, bins and native-build behavior.

Disposable runner, UID, mounts, secret exclusions, egress allowlist, process/time limits, writable paths and teardown proof.

Exact command, event order, stdout/stderr, process/network log, filesystem diff, exit status and retries.

Expected paths, artifact hashes, platform binding, cache key, reproducibility comparison and promotion destination.

Unexpected package, script, version, write, process, destination, secret, nondeterminism or toolchain/source mismatch.

Keep dormant, approve bounded build or reject; owner, expiry, known-good artifact, cache purge and credential revocation route.

Primary sources

  1. npm rebuild documentation, CLI v11npm Docs · Reviewed: Description, synopsis, lifecycle behavior, package selection and ignore-scripts configuration · Retrieved · Supports: npm documents rebuild as running matching lifecycle scripts for installed packages and exposes ignore-scripts as a command configuration, not as persisted package approval state.
  2. npm scripts documentation, CLI v11npm Docs · Reviewed: Life cycle scripts, dependencies, working directory and npm rebuild operation order · Retrieved · Supports: npm documents preinstall, install and postinstall as the lifecycle order for npm rebuild and explains that scripts run from the package root.
  3. npm 11.6.0 rebuild command implementationnpm CLI source · Reviewed: Accepted configuration, Arborist option construction, package-spec matching and rebuild dispatch · Retrieved · Supports: The pinned command accepts ignore-scripts, passes current flat options into Arborist and invokes rebuild over the selected installed nodes; it does not read a previous install command or persisted approval record.
  4. Arborist 9.1.4 rebuild implementation bundled with npm 11.6.0npm CLI source via unpkg · Reviewed: Default-node loading, build queues, ignoreScripts guards, package metadata refresh and lifecycle execution · Retrieved · Supports: The implementation discovers scripts from installed package metadata, queues preinstall/install/postinstall, and suppresses those queues only when the current rebuild options set ignoreScripts.
  5. Backstabber's Knife Collection: A Review of Open Source Software Supply Chain AttacksDIMVA preprint on arXiv · Reviewed: Abstract, historical malicious-package dataset, attack-tree scope and execution-stage framing · Retrieved · Supports: Independent researchers documented historical malicious packages and code-execution opportunities across package ecosystems; this is risk context, not validation of npm 11.6.0 or the synthetic experiment.

Limitations

This is a bounded local reproduction on Node v24.10.0, npm 11.6.0, @npmcli/arborist 9.1.4 and Darwin arm64. It used one synthetic local tarball with preinstall, install and postinstall markers. It did not test npm ci, install-scripts, approve-scripts, trust, workspaces, links, prepare, node-gyp defaults, native compilation, bundled or optional dependencies, global mode, bins, Linux, Windows, containers, caches, registries, credentials, network access or malicious packages. The source review covered the direct rebuild command and bundled Arborist path, not every wrapper or delegated implementation. The independent paper is historical context and does not validate this behavior. --ignore-scripts is a lifecycle control, not a sandbox, malware assessment or proof of zero filesystem changes.

Disclosures

AccessAllGPT executed only a synthetic package created for this article and did not install or accuse a third-party dependency. No registry package script, exploit, external endpoint or publication was used. npm maintainers did not review the harness or article. No vendor supplied access, private data, payment or endorsement. AccessAllGPT Research is operated by NeuralArc, is independent, and is not affiliated with OpenAI or npm. Publication-wide relationships are listed on the disclosures page.

Further AccessAllGPT guidance

  1. npm pack --dry-run Still Executes Package Scripts
  2. npm 12 Blocks Dependency Scripts—But a Git Dependency Still Ran prepare
  3. npm Install Is an Execution Boundary
  4. A pnpm Build Approval Can Outlive the Version You Reviewed
  5. Node’s Permission Model Is a Seat Belt, Not an AI Code Sandbox
  6. AccessAllGPT Research methodology
  7. Publication disclosures

Continue the research

Get evidence-led updates for teams making production AI decisions.