The Dependency You Did Not Choose
By Trent SecOps Supply Chain Development 3 min read
Open a package.json for a modest site and count the dependencies. A dozen, maybe twenty. Now count what actually lands in node_modules. The ratio is the whole problem.
You chose the dozen. You did not choose the rest, you have not read the rest, and the rest runs with the same privileges as the code you wrote.
Install-time is the part people forget
The instinct is to worry about what ships to production. That is the wrong end of the pipeline to worry about first.
A package can run code during installation. That code executes on a developer’s machine and on your CI runner, both of which have things a compromised package would like: credentials, tokens, an authenticated npm session, a cloud role, source for repositories the package will never appear in.
Runtime exposure is bounded by what your application does. Install-time exposure is bounded by what your build environment can reach, which is almost always more.
Two settings change this materially and cost nothing:
ignore-scripts=true
and a lockfile-respecting install in CI — npm ci, pnpm install --frozen-lockfile — so a resolution that differs from the lockfile is an error rather than a silent upgrade.
Some packages genuinely need install scripts. Allow those explicitly, one at a time, having looked. The point is that “allow all, always, by default” is not a decision anyone made — it is what happens when nobody turns it off.
The lockfile is the artifact, not the manifest
A version range in a manifest describes what you would accept. The lockfile describes what you got. Only the second is reproducible, and only the second is auditable.
That has a few consequences worth being literal about:
- Commit it. Always. A repository without a lockfile builds a different program on Tuesday than it did on Monday.
- Review changes to it. A pull request that touches one line of source and four hundred lines of lockfile is a pull request about the lockfile.
- Never regenerate it to “fix” a conflict. Resolve the conflict. Regenerating silently upgrades everything.
The last one is the common failure and it looks like housekeeping. It is not housekeeping; it is an unreviewed bulk upgrade of every transitive package in the tree.
Pin the things that decide what your build means
Version ranges are a reasonable default for libraries and a bad one for anything that determines the output of your build.
Exact-pin your framework, your bundler, your test runner and anything whose minor release changes rendering. A caret on a browser-automation library means the browser it bundles updates overnight, which means your visual baselines fail on a Tuesday for reasons that have nothing to do with your code — and the fix people reach for is to loosen the tolerance, which turns a broken referee into an ignored one.
The rule is narrower than “pin everything”: pin what decides what “passing” means. Everything else can float within a range and be updated on a schedule.
Reduce, don’t just scan
Scanning tells you about known vulnerabilities in what you have. It does nothing about the fact that you have it.
Before adding a dependency, three questions in order:
- Does the platform already do this?
fetch,crypto,structuredClone, date formatting viaIntl, argument parsing — a decade of “you need a library for that” is no longer true. - Is the useful part small enough to vendor with attribution? A forty-line function you can read is a smaller liability than a package with nine transitive dependencies.
- What is its own tree? A package with zero dependencies and a package with sixty are not comparable, whatever their download counts say.
None of this is austerity. It is recognising that every dependency has a maintenance cost you pay later, in a currency (attention during an incident) that is much more expensive than the currency you save now (an afternoon).
Make the boundary checkable
The pattern that generalises: a supply-chain rule you have written down is advice, and a supply-chain rule your build enforces is a control.
If production must not ship third-party script, assert the output contains none. If the lockfile must match the manifest, use the flag that makes a mismatch fail. If install scripts are off, keep them off in a committed config file rather than in a wiki page.
The distinction is the same one that governs every other constraint on a codebase, and it is worth stating plainly one more time: writing the sentence is not the work. The work is making the sentence false to violate.
Read next
The whole archive-
Development
Timezones Are Not a Formatting Problem
A calendar day and an instant in time are different types, and most date bugs are the moment one is silently converted into the other. The fix is not better formatting — it is refusing the conversion.
-
AI
An Agent With Write Access Is an Outage Waiting for a Schedule
Autonomous agents are good at proposing changes and bad at being trusted with them. The whole design problem is putting a reviewable boundary between the two, and most setups skip it because the boundary is inconvenient.
-
Development
Zero JavaScript Is a Product Decision, Not a Performance Trick
Shipping no client-side script is usually sold as a speed optimisation. The speed is the smallest thing you get. What actually changes is the set of failures your site is capable of having.
Keep reading
More on SecOps
Every vertical on this site carries its own writing, its upcoming events and its answers on one page — and the whole archive is available as a feed with a real publication date on every item.