Also called Unused dependency, Zombie package, Phantom dependency
How a zombie dependency is measured
The method is a set difference: list what the manifest declares, list what the code actually imports, and report the declared packages with no importer.
| Declared in manifest | Imported by code | Verdict |
|---|---|---|
lodash, axios, left-pad | lodash, axios | left-pad is a zombie |
The check has two known blind spots. A dependency used without an import statement (a CLI run from a script, a plugin loaded by name from config, a type-only package) looks unused. The reverse problem, an import of a package missing from the manifest (a phantom dependency), is found by the same tools.
Three things get confused with it:
- Unused import: one statement in one file. Linters catch it.
- Abandoned upstream package: unmaintained but possibly in heavy use.
- Zombie dependency: declared or shipped, imported by nothing.
Why zombie dependencies matter
An unused dependency costs something every time the project is built or scanned. It is downloaded in CI, included in container images, and listed in vulnerability reports, so a CVE in a package you never call still creates an alert someone has to triage. In a monorepo the same problem exists one level up: an internal package that nothing depends on keeps being built, tested and kept in sync with shared tooling. Removing it is cheaper than maintaining it, but only after you confirm it is really unused.
How to measure zombie dependencies yourself
Use the checker native to your ecosystem:
- JavaScript and TypeScript:
npx kniplists unused dependencies along with unused files and exports;npx depcheckis the narrower single-purpose tool. - Python:
deptry .comparespyproject.tomlorrequirements.txtagainst imports and reports unused, missing and transitive dependencies. - Rust:
cargo macheteis fast and heuristic (text matching);cargo +nightly udepsis slower and more accurate because it uses the compiler. - Go:
go mod tidyremoves requirements the module no longer needs.
Pick one of these over any cross-language tool when you are checking a package manifest: they know their ecosystem's naming quirks, such as a Python distribution name differing from its import name.
How repowise measures zombie dependencies
repowise detects the internal-package case. In its dead code analysis, a zombie_package is a top-level directory that declares itself a package (it holds its own manifest) and has no import edges coming in from files in any other top-level directory. Third-party imports do not count as importers; a framework wiring anchor does.
The detector skips directories that are not packages: tooling and docs folders, directories with no manifest of their own, packages that ship a runnable program (a CLI entry, a JVM main), and folders with only config or data. It needs at least two top-level packages to compare.
Zombie packages get a fixed confidence of 0.5 and arrive as review findings, because a package can be consumed by a deploy script or another repository. Each finding carries line count, last commit, 90-day commit count and the most common owner, so you can tell a dormant package from one under active work.
- CLI:
repowise dead-code --kind zombie_package;--no-include-zombie-packagesturns the pass off. - MCP:
get_dead_code, one of the 10 repowise MCP tools. - Manifest entries: knip, deptry and cargo-machete (above) diff a manifest against imports, and pair well with repowise's repository-wide view of zombie packages.
See dependency graph for how the import edges are built.