Safeguard
Open Source Security

The Twenty-Minute Review to Run Before You Add a Dependency

Adding a dependency takes ten seconds and commits you to trusting a stranger's code on your build machines for as long as the project lives. Six checks, the signals that should stop you, and what this deliberately does not defend against.

Sofia Marchetti
Open Source Program Lead
6 min read

Adding a dependency takes ten seconds and commits you to trusting a stranger's code, on your build machines and in production, for as long as the project lives. It is the least reviewed decision in most codebases and one of the most consequential.

This is a twenty-minute review to run before the install command. Not a policy, not a committee: a short list of things to look at that catch most of what goes wrong. For any engineer about to add a package, and for whoever wants a reviewable standard in pull requests.

First: do you need it

The cheapest dependency is the one you do not add.

If the package exists to do something you could write in under about fifty lines, and you understand those fifty lines, write them. The famous small-package incidents are not really about the packages, they are about a dependency graph where hundreds of trivial packages each carry a maintainer, a release process, and an account that can be compromised.

This is not an argument against libraries. Cryptography, parsers, protocol implementations and anything where correctness is subtle should absolutely be a dependency. It is an argument against a package for padding a string.

The six checks

1. Is it maintained? Last release date, last commit, open issue count and whether recent issues get responses. A package with no commits in two years is not stable, it is unowned, and when a vulnerability is found there will be nobody to fix it.

2. How many people can publish it? The bus factor cuts both ways. One maintainer means no succession and a single account to compromise. It also means a smaller attack surface than a large org with many publish-capable accounts and uneven credential hygiene. What you want to see is a small number of long-tenured maintainers and required two-factor authentication on the registry.

3. What runs at install time? The question that decides whether a compromise of this package is a build-machine compromise.

npm view <package> scripts          # look for preinstall/postinstall
pip download <package> --no-deps    # then inspect setup.py for executed code

A package with no install scripts can only affect you when you call it. One with a postinstall script runs as soon as anyone installs it, including on a laptop, before any of your scanning sees it.

4. What does it drag in? Count the transitive tree, not the package.

npm info <package> dependencies
# or, more honestly, install in a scratch directory and count
npm i --prefix /tmp/scratch <package> && ls /tmp/scratch/node_modules | wc -l

Four hundred transitive packages for one utility is a real cost: four hundred maintainers to trust and four hundred sources of future advisories. Two libraries that do the same job can differ by an order of magnitude here, and it is rarely mentioned in either README.

5. What can it reach? Does it make network calls, read the filesystem, spawn processes, read environment variables. A date formatting library that opens a socket is a finding. Some ecosystems expose this directly; otherwise grep the source for the obvious primitives. This takes five minutes and it is the check that finds deliberate badness.

6. Is it the package you think it is? Typosquats rely on you not looking. Confirm the name character by character against the documentation you came from, check the repository link actually goes to the project, and be suspicious of a package with a familiar name, a recent first release and no history.

The signals that should stop you

Treat any of these as a reason to pick something else:

  • A recent first publication for a package with a familiar-sounding name.
  • An install script that is obfuscated or fetches from a remote host.
  • A maintainer handover in the last few months, to someone with no other history. This is the event-stream pattern, and it remains the most effective way to take over a widely used package.
  • A repository link that does not resolve, or points somewhere unrelated to the package.
  • Download counts wildly out of proportion to the project's visible activity.

None of these proves malice. All of them are cheap to check and expensive to miss.

Write down the decision

The review is worth more if the next person can see it. A short note in the pull request that adds the dependency:

Adding: fast-parse@3.2.1
Why: replaces 200 lines of hand-rolled parsing in the import path
Checked: 4 maintainers, active (last release 3 weeks), no install scripts,
         11 transitive deps, no network/fs access, repo matches docs
Alternative considered: slow-parse (unmaintained since 2023)

Four lines. It converts an invisible decision into a reviewable one, and in two years when somebody asks why this package is here, the answer exists.

Automate the parts that are mechanical

Checks 1, 3, 4 and 6 are machine work: maintenance signals, install scripts, transitive count, and name similarity to popular packages. Run them in CI on any pull request that changes a manifest, and post the result as a comment.

That leaves the human with the two judgement calls, whether you need it at all and what it can reach, which is the right division. A reviewer who is handed the mechanical answers will actually engage with the other two.

The concession

This does not stop the case that matters most. A legitimate, well-maintained, widely used package whose maintainer account is compromised passes every check here, because at review time it was genuinely fine. That is the shape of most serious supply chain incidents, and no adoption review prevents it.

What adoption review does is reduce the number of packages you are exposed to, and refuse the obvious bad ones. The compromised-maintainer case is defended elsewhere: quarantining new releases, pinning with lockfiles, and watching what your build actually pulls. Different control, different position, and both are needed.

The implication

Every dependency is a standing trust relationship, and most are entered into without anyone deciding to. Twenty minutes at the point of adoption is the cheapest moment in the entire lifecycle to say no, because afterwards the package is load-bearing and removing it is a project.

Spend the twenty minutes. The one you decline to add is the one you never have to triage.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.