Package managers are one of those tools so embedded in daily development workflow that their design decisions rarely get scrutinized the way they deserve. You run npm install or pip install or cargo add, something downloads, and you move on. The friction is gone, and that frictionlessness is both the point and the problem.
Simon Willison’s recent piece arguing that package managers need to slow down is a prompt to examine something the ecosystem has been deferring for years: the gap between what package managers have been permitted to do and what developers actually understand they are consenting to. Feature velocity in this space has been impressive, sometimes genuinely useful, and increasingly concerning when you trace the full security surface area.
The Install Script Problem
The npm ecosystem normalized something unusual early on: packages can run arbitrary shell scripts when you install them. The preinstall, install, and postinstall hooks in package.json are not obscure features. They are used extensively and legitimately, from compiling native addons to downloading platform-specific binaries. node-gyp depends on them. Puppeteer uses postinstall to download Chromium. The convenience is real.
But so is the attack surface. When you run npm install, you are implicitly granting execution rights to every package in your dependency tree and every future version of every package in that tree. You are trusting not just the package author, but every contributor they have ever merged, every credential they have kept secure, and every future decision they make about account access.
The 2018 event-stream incident illustrated this clearly. The package had around 2 million weekly downloads when its original author transferred it to a new contributor, who embedded a Bitcoin wallet harvester targeting a specific downstream consumer. The malicious code was subtle enough to go unnoticed for weeks. The compromise was not a flaw in npm’s cryptography; it was the consequence of an ecosystem built around the assumption that packages are inert until executed, when installation itself is execution.
The 2022 node-ipc incident pushed this further. A maintainer with genuine ownership and a long contribution history deliberately added code that would overwrite files with a peace message for users with Russian or Belarusian IP addresses. Thousands of projects pulled the update automatically. The attack surface here was not a compromised account or a social engineering campaign; it was a maintainer exercising the permissions the ecosystem had always granted them.
How Different Ecosystems Made Different Bets
The install script model is not universal, and the divergence across ecosystems reflects different priorities being made explicit.
Go modules have no install hooks. You import a package; it compiles into your binary. There is no lifecycle callback, no pre-download script, no post-build hook. This is a deliberate constraint with real security benefits: importing a Go package cannot by itself compromise your development machine. The tradeoff is that packages needing native code or external binaries must document their requirements and leave setup to the developer.
Cargo, Rust’s package manager, occupies a middle ground. It supports build scripts (build.rs) that run at compile time and can link native libraries, generate code, or query the environment. Build scripts run with the same privileges as the user’s shell, so they are not sandboxed. The difference from npm is mostly structural: build scripts are explicit per-crate, they get code-reviewed as normal Rust, and tooling like cargo-vet exists to create auditing supply chains where organizations can delegate trust to each other. A build script doing something unusual tends to stand out in review in a way that a buried postinstall shell script often does not.
Python’s situation has historically been messier. For years, pip install could run arbitrary Python code via setup.py during installation. The migration to wheel distributions and the PEP 517 build backend interface helped; wheels are pre-built and do not execute setup.py on the end user’s machine. Source distributions still exist, still get installed when no wheel matches your platform, and still run arbitrary code during build. Astral’s uv has been genuinely fast at resolving and installing Python packages, fast enough that it has changed how many developers think about environment isolation. Speed does not change the trust model, though, and uv’s speed mostly means you reach the dangerous part faster.
Feature Velocity and the Expanding Blast Radius
The concern about cooling down is not only about install scripts. Package managers have been expanding aggressively in scope, and that scope expansion is where the subtler risk lives.
npm added workspaces, corepack, built-in audit, and a registry for provenance attestations. pip gave way to Poetry, then Hatch, then PDM, then uv, each iteration adding lock file support, virtual environment management, and build tooling. The JavaScript ecosystem produced yarn, then yarn v2 with Plug’n’Play semantics, then pnpm with its content-addressable store, then Bun, which ships its own JavaScript runtime alongside its package manager.
Each of these expansions carries risk, not because the features are bad, but because the attack surface grows with the feature set. A package manager that also manages your runtime, runs your scripts, handles your CI cache, and ships its own bundler has a much larger blast radius when something goes wrong. More features also mean more configuration surface, more edge cases in resolution logic, and more paths through which a malicious or compromised package can interact with the host system.
The xz-utils backdoor in early 2024 is the sharpest recent example of how these dynamics compound. The attacker, operating as a contributor named Jia Tan, spent two years building a credible commit history before inserting a backdoor into the build process that would have provided remote code execution on affected Linux distributions. The attack exploited the trust model the ecosystem runs on, the assumption that a version bump from an established contributor is safe to pull in, rather than any technical flaw in a package manager. When package managers automate the pulling of those version bumps, they automate the delivery of that compromise.
What Slowing Down Would Look Like in Practice
The security conversation in this space has mostly centered on auditing after the fact: npm audit, cargo-vet, PyPI’s malware detection pipelines, SLSA-based provenance attestations that verify a package’s bytes were built from a known source commit. These tools are useful. They are also reactive, and they address the authenticity of a package without saying anything about whether the authenticated package is safe.
Meaningful deceleration would look different depending on the ecosystem.
For npm, it would mean making install scripts opt-in at the consumer level. Rather than packages declaring hooks that execute automatically, a developer’s configuration would need to explicitly whitelist which packages can run scripts. This exists in limited form via --ignore-scripts, but it is not the default, and many packages break under it because the ecosystem was never designed with this being normal. Inverting the default, requiring packages to be explicitly trusted for script execution rather than trusted by default, would be a significant shift.
For Python, it would mean completing the practical migration away from source distributions as the installation default, and investing in wheel coverage for uncommon platforms rather than falling back to setup.py. The infrastructure exists; what is missing is making source distribution fallback uncommon enough that it is visible and explicit when it happens.
For the broader ecosystem, it would mean treating lock files as a publishing requirement rather than an optional best practice, and making registry-signed provenance a baseline expectation rather than an opt-in feature that sophisticated teams use and everyone else ignores.
The Developer Experience Trap
There is a tension here that deserves acknowledgment rather than dismissal. The developer experience improvements in modern package managers are substantive. uv is fast enough to make environment isolation feel cheap rather than expensive. pnpm’s content-addressable store saves gigabytes across machines with many projects. Bun’s bundled approach reduces the coordination overhead between tools.
The industry has consistently chosen convenience when it conflicts with security defaults. When npm install was slow, the response was to speed it up. When lock files created merge conflicts, teams disabled them. When --ignore-scripts broke workflows, scripts came back on. The defaults win in nearly every case, and they win because friction is what developers surface in feedback and security incidents are what they surface after.
This is not unique to package management. It is how most security decisions play out across developer tooling. The secure path introduces friction; the convenient path does not, and friction is what gets filed as a bug. Over time the convenient option becomes the norm, and then the secure option starts to feel like unnecessary overhead rather than an appropriate default.
Package managers are in a position to renegotiate what the default trust posture looks like, not by making development harder, but by surfacing the implicit decisions that developers are currently making without realizing it. Require explicit consent for install scripts. Make provenance attestation a standard publishing step rather than a publishing option. Surface what is executing at install time in the terminal during the install itself, not buried in an audit log after the fact.
Willison is right that the pace needs to slow down long enough to answer these questions deliberately. The tooling to do this exists across every major ecosystem; what has been missing is the willingness to make the secure path the default path rather than the one you reach for after reading a post-mortem.