Back to core workflows Fix dependency resolution Tune package metadata Validate before publishing

Finding Unused Dependencies and Exports with Knip

Every dependency you declare is something to install, audit, update and trust, and in a mature codebase a surprising share of them are no longer used. Unused exports and files accumulate the same way. Knip analyses a project — or every package in a monorepo — and reports unused dependencies, undeclared ("unlisted") dependencies, unused files, unused exports and unused types, using plugins that understand the configuration files of more than a hundred tools. This guide sets Knip up for a workspace, explains how to read and trust its report, and turns it into a CI gate that stays green.

Why dependency hygiene matters

Unused dependencies are not harmless clutter. Each one adds install time and disk usage, appears in npm audit results you then have to triage, receives update pull requests from your bots, and is one more package that can be compromised in a supply-chain attack. The reverse problem — imports of packages that are not declared — produces the phantom dependency failures that break installs under strict package managers. The wider auditing workflow is covered in Dependency Auditing and Automated Updates.

A typical first Knip run on an older repository looks like this:

$ npx knip
Unused files (6)
src/legacy/formatters.ts
src/utils/old-date.ts
Unused dependencies (4)
moment          packages/api/package.json
lodash.merge    apps/web/package.json
Unused devDependencies (3)
@types/jest     packages/ui/package.json
Unlisted dependencies (2)
qs              packages/api/src/http.ts
date-fns        apps/web/src/lib/time.ts
Unused exports (11)
formatCurrency  function  packages/utils/src/money.ts:14:17

How Knip works

Knip starts from entry files — your main and exports targets, test files, scripts referenced in package.json, and files that tools load by convention, such as vite.config.ts or .eslintrc.cjs. It builds the module graph from those entries, then compares what it found with what you declared. Anything declared but never imported is unused; anything imported but not declared is unlisted; any file not reachable from an entry is unused.

How Knip finds unused and unlisted dependencies Knip collects entry files from package.json and tool plugins, builds the import graph, and compares imported packages with declared dependencies. entry files exports, bin, scripts, tests plugins vite, eslint, vitest, next configs import graph every reachable module compare declared vs imported, files vs reached
Knip's accuracy depends on finding every entry file — plugins supply the ones tools load by convention.

Plugins are what make Knip practical. A dependency like @vitejs/plugin-react is never imported by application code; it is imported by vite.config.ts. Knip's Vite plugin knows to treat that config as an entry, so the dependency counts as used. Plugins activate automatically when the corresponding tool is a dependency.

Setting it up in a workspace

npm install -D knip
{
  "$schema": "https://unpkg.com/knip@5/schema.json",
  "workspaces": {
    ".": {
      "entry": ["scripts/*.ts"],
      "ignoreDependencies": ["@acme/tsconfig"]
    },
    "apps/*": {
      "entry": ["src/main.tsx", "src/routes/**/*.tsx"],
      "project": ["src/**/*.{ts,tsx}"]
    },
    "packages/*": {
      "project": ["src/**/*.ts"]
    }
  },
  "ignoreExportsUsedInFile": true
}

Save it as knip.json at the repository root. Knip reads each workspace's package.json and checks each package against its own dependencies, so a dependency declared in apps/web but used only in packages/ui is correctly reported as both unused in one and unlisted in the other.

  • entry adds entry files Knip cannot infer, such as route files loaded by a file-system router or scripts invoked by CI.
  • project limits which files count as part of the project, so generated or vendored files do not show up as unused.
  • ignoreDependencies suppresses known false positives — for example, a shared configuration package referenced only from tsconfig.json extends (which Knip usually understands, but not in every setup).
  • ignoreExportsUsedInFile avoids reporting exports that are used within the same file.

Reading the report and avoiding false positives

Treat the first report as a list of questions, not a list of deletions. Common false-positive sources:

Report Usual cause Fix
Unused dependency used via CLI in a script Binary used from scripts but Knip missed it Check the script; add to entry or ignoreBinaries
Unused file loaded dynamically import() with a computed path Add the pattern to entry
Unused export used by another repository Public API of a published package Knip treats exports entries as public; check the map
Unused dependency that is a peer Required by a plugin at runtime Declare it; add to ignoreDependencies with a comment
Triage for each item in the Knip report Each reported item is either removed, declared correctly, marked as an entry, or ignored with a documented reason. Item in the Knip report check how it is really used Remove it dependency, file or export truly unused Declare it in the package that imports it unlisted Add an entry dynamic import, router, CI script loaded indirectly Ignore with reason ignoreDependencies + comment tool quirk
Every ignore should carry a reason; most items are real and can simply be removed or declared.

Unused exports deserve extra care in libraries. An export that nothing inside the repository imports may still be public API used by consumers. Knip considers exports reachable from the package's exports map to be used, so internal modules' unused exports are reported while your public surface is not. If you see a public function reported, check that the exports map actually exposes the module it lives in.

Worked example: a first cleanup pass

A team runs Knip on a twelve-package repository for the first time and gets 140 findings. They split the work: dependencies first, because removing them has the largest effect on install and audit noise. Of 31 unused dependencies, 26 are genuinely dead (including moment, replaced by date-fns two years earlier but never removed), three are false positives from a custom build script Knip could not see (fixed with an entry pattern), and two are peers required by an ESLint plugin (declared and ignored with comments). The eight unlisted dependencies are declared in the right packages. Unused files go next — 19 legacy modules deleted. Unused exports are left for a later pass, since removing them from library packages needs a changelog decision. The cleanup removes 180 packages from the lockfile and shortens npm audit output by a third.

Findings from a first Knip run by category Example counts of Knip findings in a twelve-package repository, split into unused dependencies, unlisted dependencies, unused files and unused exports. unused dependencies 31 unlisted dependencies 8 unused files 19 unused exports and types 82
Dependency findings are the quickest wins; unused exports in libraries need a versioning decision.

Unused exports and types in library packages

Unused exports are the most numerous and the least urgent finding, but in a monorepo they carry real cost. An exported function nobody imports is still compiled, type-checked, documented and maintained; it is also API surface that someone may start depending on tomorrow. For internal packages — those marked "private": true and consumed only inside the repository — Knip's unused-export report is trustworthy: if nothing in the repository imports it, nothing anywhere does. Remove those exports freely.

For published packages, split the work. Exports reachable through the exports map are public and Knip treats them as used. Exports that are unused and not reachable from the map are internal helpers that happen to be exported, usually for tests; mark them with a JSDoc @internal tag, which Knip can be configured to accept, or stop exporting them and test through the public API.

Types need a separate decision. Knip reports unused exported types and interfaces, and in libraries those are often intentionally exported so consumers can annotate their own code — ClientOptions, ButtonProps. Configure "ignoreExportsUsedInFile" and, if needed, the types category's rules so that types used in public signatures are not flagged, and review the remainder by hand.

Keeping the analysis fast

On large repositories, a full Knip run can take a minute or more because it builds a complete module graph. Three settings keep it quick enough for every pull request. Limit project globs to source folders so generated code, fixtures and build output are never parsed. Use --workspace <name> in local development to analyse one package and its dependencies. And cache the run: Knip supports --cache, which stores parsed module information between runs and makes repeat runs in CI significantly faster when combined with a cache step keyed on the lockfile.

CI integration

Once the report is clean, keep it clean. Run Knip in CI with only the categories you have fixed, and expand as you go:

- run: pnpm install --frozen-lockfile
- name: Dependency hygiene
  run: pnpm exec knip --include dependencies,unlisted,files

Knip exits non-zero when it finds issues, so the job fails on any new unused dependency or unlisted import. For large repositories, --production mode analyses only production entry points and dependencies (excluding tests and dev tooling), which is useful as a separate, stricter check on what ships.

Prevention and CI/CD guardrails

  • Make Knip a required check for the categories you have cleaned up.
  • Document every ignore in knip.json with a comment-style note or a linked issue.
  • Run it on dependency-bot pull requests too, so an update that removes a transitive need is visible.
  • Pair it with import/no-extraneous-dependencies in ESLint for editor-time feedback on unlisted imports.

Frequently Asked Questions

How is Knip different from depcheck? depcheck focuses on dependencies and has fewer tool integrations. Knip also finds unused files and exports, understands workspaces natively and has a large plugin ecosystem, which makes its dependency results more accurate in modern setups.

Does Knip support monorepos with pnpm, Yarn and npm workspaces? Yes. It reads the workspace configuration of each package manager and analyses each package against its own manifest.

Is it safe to delete everything Knip reports? Not blindly. Dynamic imports, framework conventions and public APIs can produce false positives. Review each category, add entries or ignores where needed, and run the full test suite after removals.

Can Knip fix issues automatically? knip --fix removes unused dependencies from manifests and unused exports from source files. Use it on a branch and review the diff; it is best for the dependency category, where changes are mechanical.

Related

Dependency Auditing and Automated Updates