Code quality
Dependency Cleanup: Remove Unused and Vulnerable Packages
Dependencies are useful until they become invisible infrastructure. A package added for one experiment can stay for years. A transitive dependency can pull in risk nobody reviews. A build tool can remain long after the framework that needed it is gone.
Dependency cleanup is not about having the smallest possible package.json, go.mod, Cargo.toml, or lockfile. It is about knowing what your software depends on, why it depends on it, and how quickly you can react when one layer becomes risky.
Sort packages by job
Start by giving each direct dependency a job. The fastest audit is a table with four columns.
| Package job | Keep when | Remove when |
|---|---|---|
| Runtime | The application imports it in production code | It is only used by old scripts or dead paths |
| Build | The build, bundler, or compiler needs it | The toolchain changed and the package no longer runs |
| Test | It supports active tests | The test framework or fixtures were removed |
| Development | It improves local workflows | It is unused, undocumented, or replaced by a standard command |
This step usually exposes easy wins: duplicate utilities, old plugins, obsolete type packages, stale polyfills, and libraries used by one deleted feature.
Separate unused from vulnerable
Unused and vulnerable packages need different handling.
Unused packages can often be removed immediately after a clean build and test run. Vulnerable packages require more context. A vulnerability in a dev-only package may have a very different risk profile from one in a request parsing library exposed to users.
Use this triage:
- Is the package direct or transitive?
- Is it used at runtime, build time, test time, or only in local development?
- Is the vulnerable code path reachable in your application?
- Is there a patched version?
- Does the patch introduce a breaking change?
Avoid blind bulk upgrades for serious production systems. They can hide behavior changes in a huge diff. Prefer small upgrade batches that can be tested and reviewed.
Reduce your reaction surface
The fewer packages you depend on, the easier security maintenance becomes. That does not mean rewriting mature libraries yourself. It means removing packages that no longer carry their weight.
Good cleanup candidates:
- One-function utility packages replaced by the language or runtime.
- Framework plugins for features you no longer use.
- Duplicate date, validation, logging, or HTTP libraries.
- Deprecated packages with maintained alternatives.
- Packages only referenced by old scripts that nobody runs.
When you remove a dependency, delete the related config, lockfile entries, examples, and docs references too. Half-removed dependencies are how cleanup turns into confusion.
Make dependency review recurring
Dependency cleanup works best as a monthly habit. Review direct dependencies, run your audit tools, check stale packages, and verify that the lockfile still matches the project you actually ship.
For teams, make ownership explicit. Security should not be the only group responsible for dependency hygiene. The service owner knows whether a package is reachable, whether an upgrade is risky, and when it is safe to delete.
The goal is not dependency minimalism for its own sake. The goal is a codebase where every package has a reason to exist and every risky package has a clear path to removal or upgrade.