Socrates standing between two doorways. At one, a hooded figure walks straight through unchecked. At the other, an identical figure is stopped by a raised hand and made to wait. Warm editorial illustration style, burnished gold and brown tones.

Who Does Your Package Manager Trust?

dependenciessupply-chainpackage-managersdevopssecurity

Two teams pulled the same poisoned package this spring.

Same package, a popular one, deep in both their trees as a dependency of a dependency neither of them had chosen directly. Same week, the same compromised version, published by an attacker who had taken over the maintainer’s account. Both teams ran the same command they run every day. Install.

The first team got robbed. Somewhere in that package was a script set to run the moment it landed, before a single line of their own code executed, before any test, before anyone looked at anything. The script read the environment it was installed into, found the CI tokens and the cloud credentials sitting in environment variables, and sent them somewhere. Nobody typed a wrong command. Nobody clicked a bad link. Somebody ran install, the way they had a thousand times, and the install did exactly what install is allowed to do.

The second team got a prompt. Their tool had resolved the same poisoned version into their lockfile, the same bytes, the same everything. But when it came time to run that install script, the tool stopped. It had been configured, by default, not to run a dependency’s install scripts unless someone had explicitly said this one is allowed. So instead of executing, the package sat there, flagged, with a note that amounted to: this thing wants to run code on your machine, and you have never told me it could. Someone looked at the name, did not recognize it, and said no.

Same package. Same command. One team spent the next week rotating every credential they owned and explaining to customers what “may have been accessed” means. The other team closed the prompt and went to lunch.

The difference was not luck, and it was not that the second team was smarter. The difference was that the two tools answer a question differently, a question most engineers never realize they are delegating: when a stranger’s code arrives on your machine, do you trust it by default?

Every package manager has an answer to that question. You are running one of those answers right now. Have you ever read it? Because once you see how much of your security posture rides on a default you never chose, you stop being able to unsee it.

What does “install” actually do?

Start with the word, because the word is hiding something.

What do you picture when you read the word install? Copying, probably. The tool reaches out to a registry, pulls down some files, places them in a folder, the way you might drag a document onto a USB stick. And if that were all it did, how much damage could a poisoned package really do? It would sit there inert until you used it. You would have to import it, call it, actually run the path with the bad part in it. There would be a gap between receiving the code and running it, and in that gap, you could look.

For a lot of ecosystems, that gap does not exist.

In the npm world, packages can declare lifecycle scripts: commands that run automatically at install time. The most famous is postinstall, a script that fires the moment the package lands on disk. It exists for good reasons. Some packages need to compile a native component, fetch a platform-specific binary, set something up. But what is “run an arbitrary command when this lands” if not a capability, and does a capability care whether the hand holding it is friendly? The same hook that compiles an honest binary can read your .npmrc, your environment, your SSH keys, and quietly phone home. It runs with your user’s permissions, on your laptop or your build server, with whatever credentials that context happens to hold. And it runs before you have imported a single line. So where did the gap go, the one where you were going to look? Receiving the code and running the code turn out to be the same event.

So why did the first team get robbed? Not for being careless. Because their tool answers one quiet question, should a stranger’s install script run the moment it arrives, with a standing yes. Did anyone on that team ever actually choose that answer?

Now look at what the second team’s tool did. Recent versions of pnpm flipped that default. A dependency’s build scripts do not run unless the package sits on an allowlist you maintain. The first time a package wants to execute something at install, you get a prompt: this one wants to run code, approve it or do not. A small change, but who holds the decision now? Not the package, which would have run because it could. You, who runs it only if you say so. Does that make the stranger trustworthy? Of course not. It only stops the tool from vouching for a stranger, on your behalf, without ever telling you it did.

Because that is the move, under all of this. The good tools do not remove the trust you have no choice but to extend. They stop spending it without asking you first.

So here is the first question to ask any package manager, and it holds whatever you are running: does it execute code from dependencies at install time, by default? If it does, can you turn that off, and what breaks when you do? You do not need anyone to tell you which tool to adopt. You need to know what the one already on your machine does while you are not looking. Do you?

The extreme case, and why it should scare you more than npm

If you want to see this trust model with the safety rails removed entirely, look at how software gets installed on Arch Linux from its community archive.

The community archive does not ship built packages. It ships build instructions: a file, the PKGBUILD, that describes how to fetch and assemble the software. That file is a shell script. When you install something from the archive, a helper runs that script on your machine. There is no sandbox, no review board, no gate. The PKGBUILD can contain any command a shell can run, because that is what it is, a shell script you are about to execute, written by whoever currently controls the package.

Read that last clause again: whoever currently controls the package. Because the second half of this story is about how control changes hands.

In that archive, a package whose maintainer has wandered off can be taken over by another user. There is a process for it, and the word the process uses is adopt. A generous word, and most of the time a generous act: someone steps in to keep an abandoned but useful thing alive. But look at what the word quietly grants. Who earned the trust attached to that package? The old maintainer did, slowly, over years. And who inherits it, whole, the instant they click adopt? Someone who earned none of it. The name did not change, so the trust never reset. Should it have?

This June, someone industrialized it. Across a single weekend, more than nineteen hundred orphaned packages in that archive were adopted and their build scripts quietly rewritten to pull in hostile code. The payload reached across ecosystems, the Arch build script fetching malicious packages from the npm registry, and went hunting for credentials: SSH keys, browser sessions, the tokens that open the next system. Why spend three years grooming one maintainer when abandonment is a queryable list? Nobody had to. The mechanism for transferring trust without re-earning it was already a feature, with a tender name, waiting.

Is the lesson to avoid that archive? No. For a lot of people it is genuinely the best way to get certain software, and the official Arch repositories, the ones with review, were never touched. The lesson is the shape, because where does that shape not appear, once you know to look for it? Build instructions are code. Code runs. Trust attached to a name outlives a change of the hands behind the name. Every ecosystem has this. The archive just shows it to you with the padding stripped off.

So the second tool-agnostic question: when you install something, whose code runs, and how did that person come to control that name? In most ecosystems you cannot fully answer that. Sit with how strange it is that you cannot.

What a lockfile actually promises (and what it does not)

There is a defense people reach for here, and it is a real one, but it is misunderstood often enough to be worth taking apart.

The lockfile. Most modern tools generate one: a file that records the exact version of every package in your tree, transitive dependencies included, usually with a cryptographic hash of each one. The hash matters. It means that when the next person, or your build server, installs from that lockfile, they get the exact bytes you got. If anyone tampers with a package after the fact, or a registry serves something different, the hash will not match and the install will fail. Go’s module system leans hard on this idea, with a checksum database that records the known hash of every module version so that a swapped or backdoored release gets caught by mismatch.

This is good, and you should want it. But what exactly does it buy you? Be precise, because the imprecision right here is where people fool themselves.

A lockfile with hashes guarantees integrity. Does it guarantee safety? Those are not the same word, and the gap between them is the whole game. It promises the code you run is the same code everyone else on this lockfile got, the same code that was there yesterday. Does it promise the code is good? If a malicious version gets published and pulled into your lockfile before anyone notices, what does the hash do then? It pins the malware, faithfully. Every machine reproduces the compromise perfectly, identically, with cryptographic certainty. The lockfile did its job. Its job was never to judge the code, only to make sure nobody could change it without you knowing, where “you” means a hash mismatch in a log nobody reads.

So a lockfile turns “did I get tampered-with code” into a question you can actually answer. It does not touch “did I get hostile code.” Those are different questions, and most of the supply-chain incidents that make the news are the second kind, where the hostile code was published legitimately, through a real account, and pinned perfectly by everyone’s lockfile.

Third tool-agnostic question, then: does your tool verify integrity by default, lockfile plus hashes, ideally a checksum authority outside the registry itself? Adopt it if you can. And then do not let the green checkmark tell you a story it was never qualified to tell. Reproducible is not the same as safe. It just means that whatever you got, you will keep getting, exactly.

The other thing a tool can leak: reach

There is a quieter trust decision your package manager makes, and it has nothing to do with scripts or hashes. It is about layout. About which package can see which other package.

In the flat install model the npm ecosystem used for years, dependencies get hoisted into one shared space. The practical consequence? Your code can import a package you never declared, because some dependency of a dependency happened to drop it nearby. It works. It keeps working, right up until that invisible package changes version, or vanishes, or turns, and you find you were leaning on something that appears nowhere in your own manifest. How do you audit what you cannot see? How do you remove what you do not know you have?

Stricter models change this. pnpm, for one, builds a layout where a package can reach only the dependencies it actually declared, the rest kept out of its line of sight by symlinks rather than thrown in a shared pile. Forget the mechanism. What is the principle? A dependency should be able to touch only what it admitted to needing. Get that, and the blast radius of any one package shrinks to what it declared, instead of the whole tree.

Fourth question: can a dependency in your tree reach code it never declared? If the answer is yes, you have phantom dependencies, things you rely on that are written down nowhere, and the first time you will hear their names is the morning one of them breaks or turns.

Who pays to keep it safe?

Everything up to here has been about the door, the technical trust model, what runs and what it can reach. You can harden all of it. You can pick the strictest defaults in every category, scripts off, hashes on, reach contained. Do it. It is the cheap, high-leverage part.

And it will not save you from the deepest problem, because under every one of these tools and every one of those packages is a person, and the question you have been avoiding is who pays that person.

Consider how the load-bearing software of our industry is actually maintained, because there are really only a few models and none of them is comfortable.

There is the unpaid volunteer. The de-facto standard way to get software onto a Mac was written, and is still largely shepherded, for free, including by an author a famous company declined to hire even as its own engineers leaned on his work daily. And how much of your tree is this same arrangement? Most of it. Someone, somewhere, holding up a thing the world runs on, for nothing, on weekends, fielding issues from strangers who file them like invoices. When did you last thank one of them by name?

There is the groomed volunteer, which is just the unpaid one after an attacker notices. The compression library inside nearly every Linux system was kept alive for years by one exhausted, unpaid man, until a patient stranger spent three years earning his trust and his commit access and came within a half-second timing fluke of slipping a master key into the world’s servers. What was the vulnerability there? Not a bug. The burnout. And what produced the burnout, if not the funding model?

And there is the new model, the one that fixes the burnout with money: the venture-backed company. The most exciting recent tool in the Python world replaced a decade of bolted-on, underfunded community plumbing with one fast, coherent, genuinely excellent piece of software. It is built by a funded startup. That is not a knock. Paid, full-time maintenance is the thing the volunteer model was dying for the absence of. But a dependency is a thing you may hold for ten years, and a venture-backed company is a thing that, by construction, owes its investors a return on a shorter clock than that. So the question you are taking on is not “is this tool good.” It is good. The question is what happens at the exit. Open-core pricing, a license change, an acquisition, a pivot. You are not just adopting a tool. You are adopting a company’s cap table. Whose endgame are you betting your next decade on, and are you sure it matches yours?

An unpaid man. A groomed man. A term sheet. When you choose an ecosystem, what are you actually choosing? Not only the defaults. You are choosing which of these your foundation rests on, for the tool itself and for every package beneath it. And who chooses that consciously? Almost no one. It came attached to the language, which came attached to the hire, which came attached to a decision someone made on a Tuesday for reasons that had nothing to do with any of this.

Fifth question, and it is the one no tool can answer for you: for the things your business actually depends on, who pays the people who keep them alive? Is the answer nobody, a volunteer, or an investor who needs an exit? You do not have to act on the answer today. You have to stop not knowing it.

The door and the foundation

So here is where the two halves meet. Not in a checklist, because a checklist would tell you this is more solvable than it is.

The door is cheap. You can change your package manager in an afternoon. You can flip the defaults, turn off install scripts, turn on integrity checks, tighten the reach, generate the lockfile, wire the checksum database. Most teams can do the whole technical hardening in a sprint, and most teams should, because it is the highest-leverage security work available for the effort. Do that much, even if you do nothing else.

But notice what the hardened door still cannot do. It can stop a stranger’s code from running until you say yes. Can it tell you who the stranger is? It can guarantee you got the exact same code as everyone else. Can it tell you whether the person who wrote that code is being paid, or being groomed? The strictest tool in the world, every default set to suspicion, still stands on the same foundation as the careless one: a tree of people you have never met, holding up the floor under your company, in conditions you have never once looked at.

You can buy the door. The foundation is not for sale, because what the foundation needs is not a setting. It is attention, and maybe money, pointed at the humans your tree actually rests on. That part does not have a default you can flip. It has only the question of whether anyone will look.

So: go change the lock. You should. It is real, and it helps, and you can do it on Tuesday.

Then tell me whether you ever learned a single one of their names.