Codescythe is a focused TypeScript and JavaScript dead-code analyzer and remover inspired by Knip, scoped to entry/project graph analysis and unused source exports/files. It intentionally avoids Knip's framework plugin surface.
It exists for TypeScript-heavy JavaScript codebases that want a smaller, more predictable cleanup tool: start from known entry points, follow the import/export graph, and identify project files or exported symbols that nothing reachable uses. Many dead-code tools grow into broad framework integration layers; Codescythe chooses a narrower contract so the analysis is easier to reason about, test, and run as part of automated cleanup.
The goal is not to replace Knip for every framework-aware audit. Codescythe is for the common package and monorepo maintenance job where the project boundary is already known and the useful answer is deterministic: which source files and exports are unused, and which of those removals can be applied safely.
Codescythe takes a deliberately smaller slice of Knip's problem space.
| Knip | Codescythe | |
|---|---|---|
| Primary scope | Broad JavaScript and TypeScript project hygiene: unused files, exports, dependencies, binaries, unresolved imports, and related issue types. | Focused TypeScript/JavaScript dead-code analysis: unused project files, unused exports, unresolved imports, and supported removals. |
| Project discovery | Infers more from package metadata, workspaces, scripts, framework config, and built-in plugins. | Starts from explicit entry and project config, then follows the import/export graph. |
| Framework awareness | Designed for framework and tool integrations through plugins and compilers. | Intentionally avoids a framework plugin surface. |
| Best fit | Comprehensive audits where framework config, dependency hygiene, and workspace conventions matter. | Deterministic cleanup jobs where the source boundary is already known and repeatable graph behavior matters more than integration breadth. |
The benchmark suite runs Codescythe and Knip against pinned real-world TypeScript-heavy repositories fetched through Bazel. Representative local runs produced:
| Fixture | Benchmarked files | Codescythe | Knip |
|---|---|---|---|
microsoft/vscode |
9,398 | 1.11s | 4.22s |
grafana/grafana |
8,358 | 833.2ms | 9.51s |
elastic/kibana |
90,931 | 13.61s | 43.04s |
renovatebot/renovate |
2,456 | 154.5ms | 900.5ms |
Counts reflect each fixture's generated benchmark config after excludes. Run
pnpm benchmark to measure the same fixtures locally.
The canonical config schema lives at root codescythe.schema.json. Bazel keeps
the crate-local crates/codescythe/codescythe.schema.json copy in sync with
write_source_file, and that crate-local copy is compiled into the core crate.
Config can be provided as:
codescythe.jsonin the project root.codescythe.jsoncin the project root, whencodescythe.jsonis absent.- A
codescytheobject inpackage.json. - An explicit
.jsonor.jsoncpath passed with--config.
Supported config fields are entry, project, testFilePatterns, ignore,
aliases, unresolvedImports, includeEntryExports, and
ignoreExportsUsedInFile. Codescythe automatically discovers .gitignore files
in every traversed directory.
Files matching testFilePatterns are treated as leaf files. By default this
includes **/*.test.*: those files are kept out of production usage marking,
but --fix can remove them when they import a project file or export that
Codescythe is removing. When a matching test imports live production source, its
project-file imports are also kept out of the unused-file report. .spec.*
files are not matched by default; model detached end-to-end specs as entries
instead.
Exports annotated with a leading JSDoc @internal tag are the exception to the
test leaf rule. If a matching test imports an @internal export, Codescythe
keeps that export and its reachable dependency graph. If the @internal export
is not used by production code or tests, it is still reported as unused. Verbose
analysis and --explain-export show test importers that kept an internal export
alive, and codescythe doctor lists internal exports preserved by tests.
Importer and explain reasons are serialized as { code, description } objects
with fixed code values so JSON consumers can branch on stable reason codes
instead of parsing display text.
Use --verbose --json when validating config changes or comparing runs. Verbose
analysis includes the Codescythe version, config path, project and entry counts,
package import keys, ignored unresolved-import patterns, source-alias ignore
warnings, and explanations for unused exports. Ignored unresolved imports are
grouped under ignoredUnresolvedImportsByPattern with sample specifiers and
importer files, so generated-import suppressions are visible instead of being
silent.
The source graph includes static imports and re-exports, string-literal dynamic
imports, destructured require("./module") calls, and import.meta.glob
patterns. import.meta.glob marks the matched project files and their exports
as used; computed patterns and non-literal dynamic imports remain outside the
supported graph.
Run Codescythe with --fix to apply supported removals. The fix pass removes
unused project files and removes unused export declarations from reachable files.
The JSON fix report includes removedFiles, changedFiles, removedExports,
and the original analysis result.
--fix refuses source-like unresolved-import ignore patterns that overlap
package imports or configured source aliases unless --force is provided.
Extensionless and JS/TS-family patterns can hide real source imports, while
non-JS/TS asset patterns such as *.svg?raw still warn but do not block
--fix. When ignored unresolved imports create alias-namespace uncertainty for
a file, Codescythe skips export edits for that file and reports it in
skippedExportFiles.
Fixing is a single analysis-and-edit pass. Removing a dead file can make more files or exports unreachable, so repeated cleanup jobs should run Codescythe again after a fix pass when a completely stable tree is required.
Use --explain-export <file>:<symbol> to ask why one export is dead or alive:
codescythe --explain-export src/constants.ts:getServerIdUse doctor to check config risk before running destructive fixes:
codescythe doctor --config codescythe.jsoncThe doctor flags broad unresolved-import ignores under local aliases, unresolved imports, entry patterns with zero matches, project scopes that appear much broader than entry coverage, and generated ignore patterns that also match checked source files. When unresolved imports are present, JSON doctor output includes sampled resolver diagnostics with matched aliases, expanded targets, candidate files, and whether each candidate exists in the project.
Use query to inspect dependency paths through the same source graph:
codescythe query somepath src/main.ts src/module.ts
codescythe query somepath src/main.ts src/features/
codescythe query allpaths src/main.ts src/runtime.ts:initRuntime --json
codescythe query allpaths src/main.ts src/runtime.ts:initRuntime --output mermaid
codescythe query allpaths src/main.ts src/runtime.ts:initRuntime --output svg > graph.svgSelectors can point at files, directories, or exported symbols written as
<file>:<symbol>. Relative selectors are resolved from the analysis root chosen
by -C or --config.
somepathreturns one shortest path per reachable matched target. File and export targets usually match one target, while directory targets can match many.allpathsreturns the subgraph of every node and edge that lies on a path from the source selector to the target selector.
Text output is optimized for terminal inspection. JSON output includes stable
file/export nodes and typed import or re-export edges. Mermaid output renders
the same query graph as a flowchart LR diagram, and SVG output renders that
Mermaid source with the pure-Rust mermaid-rs-renderer crate.
somepath uses breadth-first search with visited nodes, while allpaths
intersects forward reachability from the source with reverse reachability from
the target. That makes dependency cycles finite without enumerating every
possible walk through the graph.
Fixture-backed Mermaid examples:
codescythe query somepath \
-C tests/fixtures/test-file-usage \
--output mermaid \
src/main.ts \
src/module.ts:usedflowchart LR
n0["src/module.ts:used"]
n1["src/main.ts"]
n1 -->|"named import ./module:used"| n0
codescythe query somepath \
-C tests/fixtures/oxc-resolution \
--output mermaid \
app/index.ts \
app/flowchart LR
n0["app/aliased.ts:aliased"]
n1["app/extension.ts:extension"]
n2["app/internal.ts:internal"]
n3["app/aliased.ts"]
n4["app/extension.ts"]
n5["app/index.ts"]
n6["app/internal.ts"]
n0 -->|"defined in file aliased"| n3
n1 -->|"defined in file extension"| n4
n2 -->|"defined in file internal"| n6
n5 -->|"named import @/aliased:aliased"| n0
n5 -->|"named import ./extension.js:extension"| n1
n5 -->|"named import #internal:internal"| n2
codescythe query allpaths \
-C tests/fixtures/knip-export-basics \
--output mermaid \
index.ts \
my-namespace.ts:yflowchart LR
n0["index.ts"]
n1["my-module.ts"]
n2["my-module.ts:myExport"]
n3["my-namespace.ts:y"]
n2 -->|"defined in file myExport"| n1
n0 -->|"named import ./my-module.js:myExport"| n2
n1 -->|"namespace member ./my-namespace.js:y"| n3
codescythe query somepath \
-C tests/fixtures/runfiles-fixture \
--output mermaid \
workspace/frontend/apps/client/platform/platformRuntime.ts \
protobuf/generated/client.ts:clientflowchart LR
n0["protobuf/generated/client.ts:client"]
n1["workspace/frontend/apps/client/platform/platformRuntime.ts"]
n1 -->|"named import #bazel_generated/client:client"| n0
See CONTRIBUTING.md for the repository layout, architecture, build graph, benchmarks, release artifacts, and local validation commands.