skip to content

Install-Time Execution

A package can run code the moment it is installed, long before anything imports it. Blast radius differs sharply between a developer laptop and a runner holding a publish token.

on this pageshow

questions

4

Why does installing an npm dependency execute that package's code before your build starts?

level: juniorimportance: must knowfreq 72%

answer

  1. installing is not a file copy
  2. the manager runs the package's own script
  3. same identity, same environment as you
  4. it happens before tests and scanners
  5. transitive packages get the same hook

basics

~20 s

Many package managers let a package declare install hooks, scripts the manager runs automatically as part of installing it. Installing is therefore code execution, not a file copy, and it happens before your tests or scanners ever run.

solid answer

~50 s

In several ecosystems a package can ship install hooks - a `postinstall` script is the familiar one - and the package manager runs them automatically while unpacking the package. That code executes as whatever identity ran the install, with that machine's environment variables, filesystem and network access. So an install is not a download; it is running code from every package in the resolved tree, including the hundreds of transitive ones nobody picked or read. It also happens early: dependency installation is usually the first real step of a build, so hooks have already run by the time tests, linters or a dependency scan look at the tree. Two consequences follow. Trust is decided at install time, not when your application first imports something. And depth in the graph buys you nothing - a hook five levels down runs with exactly the same reach as one in a direct dependency.

go deeper

for a junior

Be ready to state plainly that installing a dependency runs that package's code, name a postinstall hook as the vehicle, and say that it runs with your own account's access rather than in a sandbox.

for a middle

Explain what the hook inherits - environment variables including injected secrets, the checkout, the home directory, network access - and where installation sits relative to the test and scan steps of a pipeline.

for a senior

Show that you design around the ordering: any decision about trusting a package has to happen before installation, because afterwards the code has run. Speak to transitive reach rather than the manifest.

for a principal

Own the framing that adopting a dependency is an execution decision, not a procurement one, and argue for a single enforcement point for the estate instead of per-team habits that differ between laptops and CI.

## Installing is running code Most people picture dependency installation as a download: fetch an archive, verify a hash, unpack it into a directory. In several ecosystems that picture is wrong. A package can declare **install hooks** - scripts the package manager is contractually obliged to run as part of installing that package. In the npm ecosystem the best known is `postinstall`; other ecosystems have their own equivalents, and some have none at all. Where they exist, adding a dependency to your manifest is an agreement to execute that publisher's code on your machine. This is why malicious-package campaigns reach so reliably for the install hook. The attacker does not have to convince anyone to call a function, to import a module, or to ship the package to production. They only have to be present in a resolved tree that somebody installs. ## What the hook actually gets A hook is an ordinary process started by the package manager. It inherits, by default: - **The identity that ran the install.** On a laptop, that is the developer's user account, with their SSH agent, their cloud session and their browser profile on the same disk. On a build runner, it is the job's identity. - **The environment block.** Every variable the shell or the CI job exported, including tokens injected as secrets. Log masking hides a secret's *text in the output*; it does not remove the variable from the process environment. - **The filesystem.** The source checkout, the home directory, config files, and any cache or workspace directory the job writes. - **Network access**, unless something outside the package manager stops it. - **Time, unconditionally.** The hook runs for every install of that package, on every machine, regardless of whether anything ever imports it. ## The ordering problem Dependency installation sits near the front of almost every pipeline, because everything downstream needs a resolved tree. Tests need it. Linters need it. Most dependency scanners need it too - they inspect what was actually resolved. That produces an uncomfortable ordering: **the hook has already run by the time your protective steps begin.** A scanner that flags a malicious package after installation is telling you about code that executed minutes ago. It is a detective control reporting on a completed event, not a preventive one. Changing that means deciding something *before* the install: resolving metadata without executing, quarantining or reviewing new packages at ingest, or refusing to install at all until a name and version clear a gate. ## Transitive reach A typical application resolves a direct dependency list you can read in a minute and a transitive closure of hundreds or thousands of packages that no one has read. Every one of those may carry a hook, and every hook runs with the same privileges. There is no privilege gradient by depth: the package manager does not care that a package arrived five levels down. This is why 'we review our dependencies' is usually a claim about the manifest, not about the tree. ## Ecosystem variation Install-time execution is a property of the ecosystem and even of the artifact format, not a universal law. Some package managers run nothing at install, so a fetched module sits inert until a build or an import touches it. Some run code only when a package carries a native extension that has to be compiled locally. Others run a build backend whenever the artifact is source rather than pre-built. The practical rule is to check per ecosystem rather than carrying a habit across languages, because the wrong assumption is silent in both directions. ## What this does not mean It does not mean install hooks are the only execution point. A package that runs nothing at install still runs its module body when something imports it, and can still run code during a build. Removing install hooks narrows a window; it does not turn an untrusted package into a trusted one. It also does not mean the risk is exotic. The vector is cheap, needs no interaction, works on laptops and runners alike, and executes before the machinery most teams believe protects them.

  • Does a hook in a deep transitive dependency have less reach than one in a direct dependency?
    No. The install walks the whole resolved tree, and every hook it runs gets the same identity, environment and network access as any other. Depth changes only the odds that a human ever looked at the package. Since a typical closure is hundreds of packages nobody read, depth is where the risk actually lives.
  • Where does an install hook run relative to a dependency scan in CI?
    Almost always before it. Scanning normally needs the resolved tree, and producing that tree is what runs the hooks. So the scan reports on code that has already executed - useful for knowing you were hit, useless for preventing it. Only a check that happens before or instead of installation changes that ordering.
  • Do all ecosystems execute package code at install time?
    No, and the difference matters. Some package managers have no install hook at all, so a fetched module does nothing until a build or an import touches it. Others compile native extensions during installation, or build a source artifact before installing it. Verify per ecosystem instead of assuming your usual one generalises.

Unpacking a delivery is safe. This is a delivery that comes with instructions the courier is required to carry out in your kitchen before you have opened the box.

saying these in an interview costs you the question

  • Thinks installing a package only downloads and unpacks files
  • Assumes only direct dependencies can run install hooks
  • Believes CI scans dependencies before anything is installed
  • Says install hooks run sandboxed by default
  • Confuses install-time execution with running the application

context

open as a page

A postinstall hook runs on a CI runner holding a production deploy role - what is the blast radius?

level: seniorimportance: must knowfreq 62%

basics

~20 s

Everything the job can reach: injected secrets in the environment, the short-lived credentials behind the deploy role, the source checkout, caches it writes and outbound network. The hook runs before any test, so production access is available to it from the start.

open as a page

Why does installing a Python sdist execute package code when a prebuilt wheel does not?

level: middleimportance: should knowfreq 48%

basics

~20 s

An sdist is source, so the installer has to build it first, and that build runs the package's own build code. A wheel is already built, so installing it only unpacks files and metadata. The package's code then waits for an import.

open as a page

Disabling install scripts fleet-wide breaks a third of your builds - is that switch a control?

level: principalimportance: nice to knowfreq 38%

basics

~20 s

On its own, no. The flag removes one execution point without deciding which packages you trust, and it holds only where someone set it. Import-time code still runs. A control has an owner, an enforcement point, an exception path and evidence.

open as a page