mrkeyoor.com_
Thu 06 Aug 15:41 UTC
npmCLI & Toolingupdated 06 Aug 2026

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.

Verdict

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.

API stability3/5The knip.json schema and CLI flags have stayed largely compatible across majors, but v6 dropped the classMembers issue type, --include-libs, --isolate-workspaces and --experimental-tags, changed the JSON reporter shape, and thirty-three minor releases landed in the first five months of v6
Docs4/5knip.dev is a real documentation site with a page per CLI flag, per configuration option and per plugin, plus troubleshooting and known-issues pages that name specific errors and workarounds; the getting-started page is out of date on peer dependencies, which is the kind of thing that costs a newcomer an hour
Maintenance5/5Pushed 2026-08-06 with 9 open issues (13 including PRs) against 11.9k stars, a release most weeks, and a public blog documenting the reasoning behind each major; it is essentially one maintainer, funded through sponsorship
Ecosystem5/5About 12.6M downloads a week, plugins for roughly a hundred tools, a VS Code extension on both the Marketplace and Open VSX, an official language server and MCP server, and sarif plus codeclimate reporters for code-quality platforms

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
Skip it if

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 --dependencies

Zero-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-files

Run 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 failed

The 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 --debug

This 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 errors

The 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 --cycles

By 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

PackageRegistryPick it when
depchecknpmYou only want unused and missing dependencies in package.json, with no config file and no export analysis
ts-prunenpmYou only want unused exports in a single TypeScript package and prefer a tool with almost no surface area
unimportednpmYou want a lighter unused-files and unresolved-imports scan without plugins or workspace configuration