| name | dependency-hygiene |
| description | Manage a project's dependencies so they stay small, current, and safe. Use when deciding whether to add a dependency, auditing an existing dependency tree, configuring Dependabot or Renovate, dealing with upgrade churn or a flood of bot PRs, resolving version conflicts, choosing between dependency ranges and pinning, or removing an unmaintained dependency. Also use for "our install is huge", "too many Dependabot PRs", or before publishing a library that others will depend on. |
Dependency Hygiene
Every dependency is code you did not write, cannot review continuously, and are
nonetheless responsible for. For a library, each one is also imposed on every user
downstream — a cost you spend on their behalf without asking.
Should you add it?
Run this before every npm install:
- How much of it will you use? One function from a 40-file package is usually a
copy (with attribution and license compliance — see
license-and-legal), not a
dependency.
- Could the standard library do it? Modern stdlibs cover far more than they did
when most "small utility" packages were written.
- Is it maintained? Last release, open issue age, bus factor, response time on
recent issues. A package with one maintainer and no release in three years is a
future migration you have scheduled without noticing.
- What does it pull in? Check the transitive tree, not the direct entry.
- What is its license? Especially transitively.
- Does it run code at install time?
postinstall, setup.py, build.rs.
- How hard is removal? If it is woven through your public API types, you have
adopted its release cadence permanently.
npm view <pkg> dependencies maintainers time.modified
npx howfat <pkg>
npm ls --all <pkg>
cargo tree -i <crate>
pipdeptree -r -p <pkg>
Library authors: the bar is much higher than for applications. A dependency in a
library is a dependency in every consumer's tree, a potential version conflict, and a
row in their security scanner's report. Many successful libraries advertise zero
runtime dependencies, and that is a feature users select on.
Ranges vs pinning
The rule that resolves most confusion:
| Applications | Libraries |
|---|
| Manifest | Ranges are fine | Wide ranges, e.g. ^1.2.0 |
| Lockfile | Commit it | Commit it (for CI reproducibility) |
| Install in CI | npm ci / --locked | npm ci, plus a job testing latest deps |
Libraries must not pin exact versions in the manifest — it creates unresolvable
diamond conflicts for consumers. Libraries should still commit a lockfile: it makes
your own CI reproducible. Add a separate scheduled job that installs the newest
matching versions so you learn about upstream breakage before your users do.
peerDependencies (npm) exist for the case where the consumer must control the version
— plugins, framework integrations. Use them there and nowhere else.
Bot configuration that people don't mute
The default Dependabot setup generates a PR per dependency per update, and the
predictable outcome is a maintainer who stops reading them — which means security
alerts get ignored too. Group aggressively.
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule: { interval: weekly, day: monday }
open-pull-requests-limit: 5
groups:
dev-dependencies:
dependency-type: development
update-types: [minor, patch]
production-patch:
dependency-type: production
update-types: [patch]
ignore:
- dependency-name: "typescript"
update-types: ["version-update:semver-major"]
- package-ecosystem: github-actions
directory: /
schedule: { interval: weekly }
Renovate offers more control and is worth it past a certain size: automerge patch
and dev-dependency updates when CI is green, batch minors into one weekly PR,
require manual review for majors, and use a dependency dashboard issue instead of open
PRs. Also enable minimumReleaseAge (a few days) so you do not automerge a package
version published ten minutes ago by a compromised account.
Always update github-actions as an ecosystem — that is what keeps pinned action SHAs
from going stale (see supply-chain-security).
Auditing an existing tree
npm audit --omit=dev
npm ls --all | wc -l
npx depcheck
cargo audit && cargo udeps
pip-audit && deptry .
go mod tidy && govulncheck ./...
govulncheck is the model the rest of the ecosystem is moving toward: it reports only
vulnerabilities in code paths you actually call, rather than every CVE anywhere in
the tree. Prefer reachability-aware tools where they exist — they turn an unreadable
100-item report into a 3-item one you will act on.
Triaging an advisory:
- Is it reachable from your code? Many are not.
- Is it in a dev dependency? Lower severity for users, but real for maintainers —
dev machines hold credentials.
- Is there a fix? If not, evaluate: pin to a safe version, apply an override,
vendor a patch, or replace the dependency.
- Do not suppress without a note. Every ignore entry gets a reason and a date.
Overrides for a transitive vulnerability with no upstream fix:
{ "overrides": { "vulnerable-pkg": "1.2.4" } }
[patch.crates-io]
vulnerable = { git = "https://github.com/you/fork", branch = "fix" }
Track every override in an issue with a removal condition. Overrides are technical
debt that silently becomes permanent.
Removing dependencies
The most durable win available. Look for:
- Single-function packages — inline them with attribution.
- Polyfills for platforms you no longer support. Check your engine range and
delete accordingly; this often removes dozens of transitive packages.
- Overlaps — two date libraries, three HTTP clients, two argument parsers. Usually
from different contributors at different times. Pick one and migrate.
- Dev dependencies replaced by built-ins — Node has a test runner; Python has
tomllib; most languages have shipped a formatter.
- Heavy dependencies used for one call — the classic is a full HTTP client used to
make a single request that
fetch handles.
npx depcheck
npm ls --all --parseable | awk -F/ '{print $NF}' | sort | uniq -c | sort -rn | head
Measure the result — install size, cold-start time, number of packages — and put it in
the changelog. Users notice.
Unmaintained dependencies
When a dependency goes quiet: check for a community fork with real adoption; check
whether a stdlib or a modern alternative now covers it; consider vendoring it if it is
small and stable (a 200-line stable utility is a fine thing to own); or offer to
maintain it upstream. Only migrate to an alternative when you have to — migration is
the most expensive option and often the first one proposed.
Never quietly depend on an abandoned package with a known unfixed vulnerability
because migration is inconvenient. Say something in your README if you are stuck.
Anti-patterns
- Adding a dependency for one function.
- Lockfile not committed. Nondeterministic builds, unreproducible bugs.
npm install in CI instead of npm ci. Silently ignores the lockfile.
- Exact pins in a library manifest. Creates conflicts for every consumer.
- 50 open Dependabot PRs. The maintainer has stopped reading them; so has security.
- Automerging major version bumps.
- Automerging a version published minutes ago with no cooldown.
npm audit fix --force without reading what it does — it will happily install a
breaking major to silence a warning.
- Suppressing advisories with no reason recorded.
- A dependency whose types leak into your public API, binding your semver to theirs.