knip
Knip is a command-line linter that finds the code you forgot to delete. It walks your project from its entry files, follows every import, and then reports what nothing reached: unused files, unused exports and exported types, unused enum and namespace members, dependencies in package.json that nothing imports, imports of packages that are not in package.json, unresolved specifiers, duplicate exports and circular dependencies. It knows about roughly a hundred tools through plugins, so it understands that vitest.config.ts pulls in your test files and that an ESLint flat config references its own plugins, instead of flagging them as dead. Most of what it reports it can also delete for you with --fix. Version 6 threw out the TypeScript backend and parses with oxc instead, which made it two to four times faster.
The most complete dead-code and dependency linter for JavaScript and TypeScript, and after the v6 oxc rewrite it is fast enough to run on every pull request in a large monorepo. Treat the first report as a list of questions rather than a list of deletions, and expect to spend real time on configuration before you turn it into a CI gate.
Use it if
- You inherited a codebase where nobody is sure what is still wired up, and you want one command that lists the dead files, exports and dependencies rather than three separate tools
- You want dependency hygiene enforced in CI: unused dependencies, phantom imports of packages that are not declared, and binaries used in scripts that no dependency provides
- You work in a monorepo, where per-workspace entry and project patterns, workspace filtering with -W, and strict mode that treats each workspace in isolation are the whole reason a tool like this is worth configuring
- You want to actually remove the findings, not just read them, since --fix can strip unused exports and prune package.json, and --allow-remove-files lets it delete files too
- You care about the speed on a large repo, because the v6 oxc rewrite cut runs on projects like sentry from around 11 seconds to around 4
- You expect the first run to be actionable. On a real codebase it prints hundreds of items, and a meaningful chunk of them are false positives from dynamic imports, convention-loaded files and references living in templates or config that Knip does not parse. Budget a day of tuning entry, project and ignore patterns before the output is worth trusting
- You plan to delete whatever it lists without reading it. An import kept purely for its side effects, a route file the framework loads by filename, or an export consumed by a downstream package all look identical to dead code from a static graph, and --fix --allow-remove-files will happily remove them
- You relied on classMembers. Version 6 dropped that issue type entirely because it depended on the TypeScript language service, which is going away in the Go rewrite, and there is no replacement
- Your project is one small package. depcheck plus tsc with noUnusedLocals covers most of the same ground with no configuration file, whereas Knip's value comes from the plugin and workspace machinery you do not need yet
- You want a settled tool. Thirty-three minor releases shipped between 6.0.0 in March 2026 and 6.32.0 in August 2026, and the docs have not fully caught up: the getting-started page still tells you to install typescript and @types/node as peer dependencies, but 6.32.0 declares no peer dependencies at all
- You are on Node 18 or on Windows with a memory-constrained CI runner. Node ^20.19.0 or >=22.12.0 is required, and on Windows the oxc raw transfer path can fail with an array buffer allocation error until you set KNIP_DISABLE_RAW_TRANSFER=1
Setup reality
npm init @knip/config scaffolds a config and a script, or you can just run npx knip and see what happens. The install pulls thirteen runtime dependencies including oxc-parser and oxc-resolver, which ship platform-specific native binaries, so lockfiles need the right optional packages for every OS in your matrix. Node ^20.19.0 or >=22.12.0 is enforced by the engines field. The work is not the install, it is the configuration: on any non-trivial repo the first run reports files that are entry points you never declared, dependencies that are only referenced from a Dockerfile or a GitHub Action, and exports consumed by a sibling workspace. You fix that by tightening entry and project globs per workspace, enabling or disabling specific plugins, and only then reaching for ignore. Plugins load your webpack, vite or cypress config files with jiti, so a config that needs environment variables or TypeScript path aliases can throw before analysis even starts, which is what --debug and NODE_OPTIONS="--import tsx" are for.
Patterns
Run it once with no configurationfirst-run
npx knip
# narrow the noise on the first pass
npx knip --include files,dependencies
npx knip --dependenciesZero-config works because Knip infers entry files from package.json main, bin, exports and its plugins. Start with --include files,dependencies rather than the full report: unused exports are the noisiest category and the least urgent.
Add a config file and a scriptscaffold-config
npm init @knip/config
// knip.json
{
"$schema": "https://unpkg.com/knip@6/schema.json",
"entry": ["src/index.ts", "src/cli.ts", "scripts/*.ts"],
"project": ["src/**/*.{ts,tsx}", "scripts/**/*.ts"]
}entry is where the graph starts; project is everything Knip should consider owned by you. Files in project that entry never reaches are reported as unused, so widening project without widening entry is what creates most false positives.
Report only what shipsproduction-mode
knip --production
knip --strict
# in config, mark an ignore as production-only with a trailing !
{
"ignoreDependencies": ["@types/.+!"]
}Production mode drops test files, config files, stories and devDependencies from the analysis. --strict also isolates each workspace and only counts direct dependencies, which is how you catch a package importing something it never declared but gets hoisted from a sibling.
Let it delete the dead codeauto-fix
knip --fix --format
# only prune package.json, touch no source
knip --fix --fix-type dependencies
# and allow file deletion, once you trust the report
knip --fix --allow-remove-filesRun this on a clean branch and read the diff. Files are only removed with --allow-remove-files, which is opt-in for good reason. --format runs your local Prettier or Biome afterwards so the fix does not produce a formatting-only second diff.
Configure entry and project per workspacemonorepo-workspaces
{
"$schema": "https://unpkg.com/knip@6/schema.json",
"workspaces": {
".": {
"entry": ["scripts/*.ts"],
"project": ["scripts/**/*.ts"]
},
"packages/*": {
"entry": ["src/index.ts"],
"project": ["src/**/*.ts"]
},
"apps/web": {
"entry": ["app/**/page.tsx", "app/**/route.ts"],
"next": true
}
},
"ignoreWorkspaces": ["packages/legacy"]
}The root workspace is the key "." and workspace keys accept globs. include, exclude, workspaces and ignoreWorkspaces are root-only options; everything else can be set per workspace. Use knip -W apps/web while iterating so each run takes seconds.
Ignore what Knip cannot seesilence-false-positives
{
"ignoreDependencies": ["hidden-package", "@org/.+"],
"ignoreBinaries": ["docker-compose", "pm2-.+"],
"ignoreUnresolved": ["virtual:.+"],
"ignoreFiles": ["src/generated/**"]
}Reach for these last. ignoreFiles only suppresses the unused-files category and keeps the file in the graph, while ignore suppresses every issue type for it and is the blunt instrument the docs warn against. Values are matched as regular expressions, so @org/.+ works but @org/* does not.
Mark exports that are meant to staytag-intentional-exports
/**
* Public API surface consumed by downstream packages.
* @public
*/
export const createClient = () => {};
// knip.json
{
"tags": ["-public", "-internal"]
}Tagging beats ignoring because the reason lives next to the code. The minus prefix excludes tagged exports from the report and the plus prefix narrows the report to only those, which is useful for auditing everything marked @internal. Tags work on enum and namespace members too.
Fail a pull request on new dead codeci-gate
# .github/workflows/knip.yml
- run: npx knip --reporter github-actions --no-progress
# ratchet down from an existing backlog
- run: npx knip --max-issues 40
# exit 0 = clean, 1 = lint issues, 2 = Knip itself failedThe github-actions reporter emits annotations on the offending lines instead of a wall of text. --max-issues lets you gate an existing codebase at its current count and lower it over time. Distinguish exit 1 from exit 2 in your workflow: exit 2 means a config file failed to load, not that your code is dirty.
Ask why something is or is not reportedtrace-a-finding
knip --trace-export createClient
knip --trace-dependency lodash
knip --trace-file src/utils/legacy.ts
# and when a plugin config file blows up
knip --debugThis is the tool for arguing with the report. Tracing shows every file that imports the symbol, which usually reveals the entry pattern you forgot rather than a bug. --debug is what you run when Knip errors out loading vite.config.ts or cypress.config.ts.
Force, disable or repoint a pluginplugin-overrides
{
"playwright": true,
"webpack": false,
"mocha": {
"config": "config/mocha.config.js",
"entry": ["**/*.spec.js"]
},
"vite": { "config": [] }
}Plugins are auto-enabled when Knip spots the dependency, so setting one to true only matters when detection misses it. Setting config to an empty array stops Knip loading that file at all, which is the workaround when a config needs environment variables it will not have; add it as an entry file afterwards so it still gets analyzed statically.
Cache, watch and profilespeed-up-runs
knip --cache # 10-40% faster on repeat runs
knip --watch # re-report as you edit
knip --performance # per-function timing table
knip -u # total running time, no instrumentation
KNIP_DISABLE_RAW_TRANSFER=1 knip # Windows array buffer errorsThe cache lives in node_modules/.cache/knip and keys on file mtime and size, so a newly added .gitignore is not picked up until you delete it. Watch mode tracks imports and exports only; changes to package.json or node_modules may not refresh the report.
Audit a private repo more aggressivelyentry-exports-and-cycles
{
"includeEntryExports": true,
"rules": { "cycles": "error" }
}
# or from the CLI
knip --include-entry-exports
knip --cyclesBy default unused exports in entry files are not reported, on the assumption they are a public API. Turn includeEntryExports on for a private app and the report gets noticeably longer and more useful. Circular dependencies are off by default and only warn when enabled, until you promote the rule to error.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| depcheck | npm | You only want unused and missing dependencies in package.json, with no config file and no export analysis |
| ts-prune | npm | You only want unused exports in a single TypeScript package and prefer a tool with almost no surface area |
| unimported | npm | You want a lighter unused-files and unresolved-imports scan without plugins or workspace configuration |