A phantom dependency is a package your code imports but never declares in package.json, so it only works because another package pulled it in. pnpm's symlinked node_modules layout makes the import fail instead, which exposes the gap. Declare what you import, or the version can change without notice.
How phantom dependencies happen
With a flat node_modules layout, a package installed for one reason can be resolved from anywhere in the project. Your code imports it, tests pass, and nobody notices that package.json never listed it.
The risk shows up later. The package that brought it in updates, drops it, or moves to a different major version. Your build breaks, or worse, it keeps working on a version you never chose and never scanned deliberately. Your declared dependency list no longer matches reality, so any inventory built from package.json misses it.
What pnpm does about it
The pnpm documentation describes a layout where each package's dependencies are symlinks inside its own folder under node_modules/.pnpm. A package can only resolve what it declares, and the project root links only your direct dependencies. In the docs' words, only packages that are really in the dependencies are accessible.
There is a compatibility valve. The same page says all dependencies are hoisted into node_modules/.pnpm/node_modules by default, which helps packages that use undeclared dependencies of their own. Setting hoist to false disables that. The pnpm settings page also lists shamefullyHoist and publicHoistPattern, which can expose more packages at the top level. Use them only when a tool forces you to.
How to find and fix phantoms
- Install with pnpm in a clean checkout and run your tests. Errors saying a module cannot be found are phantoms.
- Add each missing package to package.json at the version range you intend.
- Run a dependency checker that flags imports missing from the manifest, such as Knip, which its site says finds unused dependencies, exports and files.
- Regenerate the lockfile and review the diff.
- Scan the full resolved tree, not only the declared list.
| Layout | Can code import an undeclared package? |
|---|---|
| Flat node_modules (default npm layout) | Often yes |
| pnpm default | No for your own code, with a hoist valve for dependencies |
| pnpm with shamefullyHoist | Yes, by design |
Quick check
Is a phantom dependency a vulnerability?
Not by itself. It is a hygiene gap that makes versions unpredictable and inventories incomplete, which makes vulnerabilities easier to miss.
Does pnpm remove phantom access for installed packages too?
Not fully. By default it hoists dependencies into a hidden folder so packages that rely on undeclared imports keep working. You can turn that off with the hoist setting.
Safeguard scans the resolved dependency tree from your lockfile into an SBOM, which covers packages you never declared directly. The free scan is at gold.safeguard.sh/scan. See also pnpm and modern Yarn lockfile security.
Sources
Last reviewed: 19 October 2026.