Open any package.json that's been alive for a year and I'll bet there's a
package in there you stopped importing three refactors ago. It's still installed,
still in the lockfile, still showing up in npm audit, still adding seconds to
every npm install. Nobody removes it because nobody's sure it's safe to
remove.
The classic answer was depcheck. I reached for it for years — until I noticed it
got archived in 2025 with a long tail of
open false-positive bugs. And those false positives are the whole problem: a tool
that tells you react is unused (it isn't, you're just on the new JSX transform)
teaches you to ignore it. The successor, knip, is genuinely
great — but it's a ~150-plugin tool built for big codebases, and sometimes you
just want a five-second answer to "what can I delete right now?"
So I built depsnap, and it makes the opposite bet from depcheck.
The bet: prove unused, not used
depcheck tries to prove a dependency is used — trace the imports, resolve the
specifiers — and every gap in that analysis becomes a false "unused". TypeScript
type-only imports, subpath imports, dynamic import(), CLIs invoked only in npm
scripts, @types/* stubs, peer deps: each one is an edge case where the analysis
misses the usage and wrongly flags the package.
depsnap flips it. It only flags a dependency when its name appears nowhere —
not as an import, not as a require, not as a dynamic import(), not as a string
in a config file, not as a CLI in an npm script. The bias is always toward not
flagging. The payoff: every result is high-confidence removable, and all those
edge cases that broke depcheck just work, because a name match doesn't care how
the package is referenced:
| Case | depcheck | depsnap |
|---|---|---|
import type { X } from 'pkg' |
often flagged unused | kept |
import 'pkg/subpath' |
sometimes missed | kept |
await import('pkg') |
sometimes missed | kept |
@types/* stubs |
flagged unused | skipped |
CLI used only in a script (tsc) |
flagged unused | kept (reads node_modules bins) |
| peer dependencies | flagged unused | not checked |
What it looks like
npx depsnap
depsnap — 1 unused dependency in my-app (of 14 checked)
● left-pad dependencies no import or script reference
Verify before removing — depsnap only flags names it found nowhere · 2 @types/* skipped.
depsnap --dev # also check devDependencies
depsnap --format json # for CI
It exits non-zero when it finds something, so it drops into a pipeline:
- run: npx depsnap --dev
How it actually decides
- Read
package.jsonfor declared dependencies. - Walk the project — skipping
node_modules,dist,.git— and build a corpus of all your source and config files. Crucially, it never readspackage.jsonor the lockfile, because those list every dependency by name and would make everything look "used". (That bug is easy to ship; I'm telling you so you don't.) - For each dependency, check whether its name appears in that corpus, in the npm
scripts, or as one of its installed bin names — using import-aware word boundaries solodashdoesn't matchlodash-es. - Report the ones that appear nowhere.
The honest limitation
depsnap will not catch a dependency you import but never actually call. That
needs real reachability analysis, and that's exactly what knip is for. depsnap is
the fast, zero-config, zero-dependency first pass — the "low-hanging fruit, with no
false alarms" tool. The two compose well: run depsnap to delete the obvious dead
weight today, reach for knip when you want the deep clean.
Install
npx depsnap # Node — zero deps
pip install depsnap # Python — pure stdlib, works on any JS/TS project
Both implementations are byte-for-byte identical in their output.
Try it / break it
It's MIT and tiny. I'd genuinely like to know what it flags wrongly in your repo —
the failure mode I care about is a false positive, so if depsnap ever tells you
to remove something you actually need, that's a bug I want to hear about.
What's your current move on a dependency you think is unused — delete it and run
the tests, or leave it because you're not sure?