Unused code
Find and remove dead files, dependencies, and exports with knip after a change lands.
Unused code
Knip reports files, dependencies, and exports that nothing reaches. It resolves the real import graph per workspace, so it finds the leftovers that a generated feature, a half-finished refactor, or a removed subsystem leaves behind — the kind of code lint rules and the type checker cannot see, because every dead file is internally consistent.
Run it after a feature is built and before the change is declared complete.
Commands
pnpm knip # apps/*, packages/*, and e2e
pnpm knip:fix # apply the safe removals, then re-verifyknip.json at the repository root configures every workspace: its entry points, the files each
workspace analyzes, and the dependencies that are referenced by runtime string rather than by
import.
pnpm knip:fix removes unused dependencies from manifests and unused export keywords from source
files. It does not delete files. Nothing about it is a substitute for verification: run vp check,
vp run -r typecheck, and vp run -r test afterwards, and read the diff before committing it.
What fails, and what only reports
| Reported | Severity | Why |
|---|---|---|
| Unused files | error | A file nothing imports is waste in every deployment artifact. |
| Unused dependencies and dev dependencies | error | Unused packages still carry install time, size, and CVE risk. |
| Unlisted, unresolved, and missing binaries | error | An import with no declared owner breaks a fresh clean install. |
| Unused exports, exported types, namespace members, and duplicate exports | warning | The foundation deliberately ships surface no page calls yet. |
CI runs pnpm knip after vp check, so the error tier is a merge gate and the warning tier is
review information. Warnings are still findings: an export the change itself introduced and never
used is dead code, and it should leave with the same commit.
Declared entry points
Knip treats these as reachable even when nothing imports them, because they are product surface a buyer consumes rather than code this repository calls:
| Path | Reason |
|---|---|
apps/web/src/components/ui/** | The shipped primitive library documented in Web UI. |
**/*.test.ts, scripts/** | Test suites and maintenance scripts are roots, not imported modules. |
packages/* package entry points | Workspace libraries are consumed through their manifest entry. |
Add a primitive to apps/web/src/components/ui and knip stays quiet — record it in
Web UI instead, so the inventory stays the review surface for that directory.
Handling a finding
Work in this order:
- Wire it up. A helper, hook, or component that a feature was supposed to use is a missing call, not a knip problem. Fix the feature.
- Delete it. Nothing plans to call it: remove the file, the export, and the dependency it kept
alive. Prove the deletion with
vp check, typecheck, tests, and a build. - Declare it. It is genuinely reachable outside the import graph — a runtime string reference,
a provider plugin, a buyer-facing primitive. Add an
entry,ignoreDependencies, orprojectpattern to the owningknip.jsonin the same change, and say in the commit why the code cannot be reached statically.
Do not silence a finding by widening ignore patterns because the report is inconvenient. A
directory-wide exemption removes that directory from every future report, which is exactly how the
next generated feature hides its own leftovers.
Known false-positive sources
- Runtime string references.
pino-prettyis named as a transport target string, never imported; it is declared inapps/server'signoreDependencies. - Toolchain aliases. The root
viteentry is the Vite+ core alias thatvpresolves, so it is declared at the root workspace. - Generated content.
apps/docs/.sourceis produced byfumadocs-mdxat build time and is covered by.gitignore, so knip never sees it as source.