
Install Scripts Are Off by Default Now, and Your Threat Model Has Not Noticed
Package managers closed the postinstall hole. Most supply chain defences still guard it, while the code that actually runs in your build gets imported by your own toolchain with no gate at all.
The lifecycle script is no longer the interesting way into your build. Depending on which package manager you use and which major version you are on, dependency install scripts either need an explicit allowlist entry or do not run at all. That is a real improvement, and it has quietly made most published supply chain advice describe a door that is already shut.
The threat model has not moved with it. Teams still assess dependency risk by asking whether a package runs code at install time, because that is the question the well-known incidents trained everyone to ask. The answer is now usually no. The code still runs.
What actually changed
The mechanism differs by tool, which is part of why the shift went unnoticed.
pnpm from version 10 does not execute dependency lifecycle scripts unless the package is listed in an allowlist field. Bun requires packages to appear under a trusted-dependencies key before it will run their postinstall. Yarn Berry exposes both a global switch and per-package build flags, so the behaviour depends on configuration rather than being fixed. npm still runs scripts by default and offers a flag to suppress them, which many teams now set in CI. The practical result across a typical estate is a mixture: the local development machine, the CI job and the container build may each have a different answer, and nobody has written down which is which.
The first thing worth doing is establishing that answer for your own pipelines, because it determines whether the control you think you have exists. A container build using one manager version and a developer using another is common, and only one of them is protected.
The allowlist became the control, and it has no owner
Once scripts are gated by an allowlist, the allowlist is the trust boundary. And it fills up immediately, because a set of entirely legitimate packages genuinely need install-time execution: native binary downloaders, image processing bindings, database clients that generate a typed client, browser automation tools that fetch a browser.
So the list grows, one entry per broken build, added by whoever was unblocking themselves that afternoon. Each entry is a permanent grant of arbitrary code execution in every environment that installs dependencies. It sits in a configuration file next to formatting preferences. It has no expiry, no review, no recorded reason, and no named owner.
That is the same failure pattern as an unreviewed scanner suppression list, and it deserves the same treatment: a reason per entry, a person accountable for the list, and a review when the package version changes. A control that everybody can widen unilaterally to fix a build is not a control for long.
Where the code runs now
The blind spot is that install-time execution was never the majority of the attack surface. It was just the part with a name. Your build imports and executes an enormous quantity of third-party code as a matter of routine, in the same process, with the same filesystem and network access an install script would have had.
| Where dependency code executes | Gated by install-script controls? | What actually governs it |
|---|---|---|
| Package postinstall or prepare script | Yes, on the newer defaults | Allowlist review with an owner |
| Bundler, transpiler and PostCSS plugins | No | Lockfile integrity, egress control on the build |
| Linter and formatter plugins loaded from config | No | The same, plus config file review |
| Test framework setup files and reporters | No | The same, and CI credential scope |
| Application code at runtime | No | Dependency review, runtime network policy |
| A transitive package you never chose | No | Lockfile diffing and resolution pinning |
| CI actions and runner plugins | No | Pinning to immutable revisions, scoped tokens |
Read that table as a single point: a malicious ESLint plugin and a malicious postinstall script have identical privileges in a CI job, and only one of them now passes through a gate. The plugin is arguably better placed, because it runs after checkout, inside a job that usually holds a registry token, a cloud credential and a signing key.
This also explains why a compromised patch release is more dangerous than a typosquat for most organisations. A typosquat requires somebody to type the wrong name. A compromised patch release of a package you already import requires only that your version range accepts it and that something resolves the tree again. No script, no prompt, nothing to approve.
Why your scanners are quiet about it
Composition analysis matches installed versions against published advisories. A package published this morning with an exfiltration payload has no advisory, no CVE and no fingerprint to match. The tool is working correctly and will report nothing. This is a category limit rather than a tuning problem, and it is one of the clearer distinctions in how SAST, DAST, IAST and SCA differ in what they can prove: none of them are detectors for novel malicious code in a dependency.
What does give you signal is the lockfile. A dependency change is a diff, and a malicious update almost always shows up as one: a version moving, an integrity hash changing, a new transitive package appearing, or a resolved URL pointing somewhere unexpected. That diff is reviewable by a human in seconds and by a rule in less. We set out the mechanics of treating it as a control rather than as build noise in using lockfiles as a supply chain control.
Five changes that match the current shape
- Determine, per pipeline, whether install scripts run. Write the answer down. Make the CI behaviour and the local behaviour the same, so a developer's working install is evidence about production.
- Give the build-script allowlist an owner and a reason per entry. Additions go through review like any other privilege grant.
- Review lockfile diffs on every dependency change, and fail on unexpected integrity hash or resolution changes rather than only on advisory matches. That gate belongs in the small set of things that genuinely should stop a merge, which we cover in deciding what should actually fail a build.
- Restrict outbound network access from the build. Exfiltration needs a route out, and a build job that can only reach your registry and your artefact store removes the payoff for every category in the table above, whether the code arrived through a script or an import.
- Scope and shorten CI credentials. Assume any dependency in the graph can read the job environment, because it can. Tokens should be job-scoped and short-lived so that a compromise is a rotation event rather than an incident, and the response path should be decided in advance, along the lines set out in what to do after a secret leaks in git.
Points four and five carry most of the weight, and they are pipeline engineering rather than security review, which is why they usually sit unowned between the two functions. Closing that gap is the bulk of our DevSecOps implementation work.
What to do next
Open the configuration for your main build pipeline and answer two questions. Can this job reach the public internet, and what credentials are in its environment. If the answers are yes and several, the install script setting is not the thing protecting you, and it was not the thing protecting you before either. Fix the egress path and the credential scope first. The allowlist review is worth doing afterwards, once it is guarding something that matters.
Fastnexa Security Practice
Cybersecurity Team at Fastnexa. We write from real client work, and we are happy to talk through yours.
Ready to ship this?
Bring this problem to a free 30-minute call with the team that wrote the post.
Book a demoMore from the blog
View all
Nobody Owns a Security Finding, Which Is Why the Backlog Never Moves
Findings are keyed to artefacts. Ownership is a property of deployments. Nothing maintains the map between them, so the queue fills with work that has no addressee and no due date anyone feels.

A Pen Test Scoped to Pass an Audit Only Tells You About the Audit
Compliance frameworks write the scope of most penetration tests. That scope is drawn around the assets named in the control text, not around the route an attacker would take.

Your Alert Volume Is a Budget Decision Dressed as a Security Decision
The number of alerts a team can survive is fixed by the analyst hours you bought. Every rule you enable spends that budget, and the decision to enable it is almost never costed.
Related services
Want help putting this into practice? Here is how we deliver it.