Dead code
Find the code nothing uses, and know what is safe to delete.
Repowise walks your import and call graph to find files nothing imports, exported symbols nothing references and packages no other package uses. Git history sets how sure each finding is, and the ones ready to delete are marked.
Run repowise dead-code, or ask your agent. No API key needed.
At a glance
- 0 to 1
- confidence on every finding
- Set by the finding's kind and git history, then lowered by any sign of runtime loading.
- 0.7
- deletion-ready floor
- Unused exports at or above it, on a path with no runtime-load risk, are marked safe to delete.
- 1 year
- untouched file starts at confidence 1.0
- A file edited in the last 90 days starts at 0.4, because recent work is often unfinished.
- 14
- languages with dynamic-import markers
- importlib, require.context, Class.forName, dlopen and peers lower the confidence of nearby files.
Sound familiar?
Dead code stays because nobody can prove it is dead.
- 01
Nobody removes the old module because nobody is sure what still calls it.
- 02
The unused-code report flags every Django view and pytest fixture.
- 03
Agents read files that no longer run and copy their patterns.
- 04
A cleanup PR broke a plugin that was loaded by name.
What makes it different
What every run gives you.
- Unreachable files
Files no other file imports.
Entry points, build files, tests and framework routes are set aside before scoring, so what remains is code no import reaches.
- Unused exports
Public symbols nothing references.
No import, call, inheritance or type reference in the repository. Methods and fields are reached through their class and never reported.
- Zombie packages
Top-level packages nothing else imports.
In a monorepo, the package everyone forgot, found from the same graph.
- Safe to delete
The deletion-ready pile, marked.
Start with these, then work down the review tier.
- Owners
Who to ask before you delete.
Each finding names the file's primary owner from git blame, and the dashboard groups findings by owner.
- Triage that carries over
Decide once.
A finding you resolve, acknowledge or mark as a false positive stays out of the open list on later updates.
In depth
How each finding earns its number.
On a real repository
On a 44-file Django app: two findings, both genuinely unused.
We indexed gothinkster/django-realworld-example-app, a Django project with 44 Python files, and ran repowise dead-code. It reported one file nothing imports, conduit/apps/profiles/exceptions.py, and the exception class inside it. Nothing in the project raises ProfileDoesNotExist, so both findings are real. Both sit at 40 percent, the review tier, so the summary reports 0 cleanup-candidate lines and a person confirms them with one search.
- Nothing else flagged
- None of the project's views, URL modules, migrations or settings were reported.
- Reason on every row
- No importers for the file; no importers for the symbol.
- Review tier shown
- Medium-confidence findings stay in the default report.
- Owner attached
- The JSON row names the file's primary owner from git blame.
| Finding | Confidence | Ready |
|---|---|---|
| conduit/apps/profiles/exceptions.pyunreachable_file, 6 linesFile has no importers (in_degree=0) | 40% | review |
| ProfileDoesNotExistunused_export, 3 linesPublic symbol 'ProfileDoesNotExist' has no importers | 40% | review |
Summary line
Cleanup-candidate lines: 0. Findings by confidence: medium 2.
Confidence
Git history decides how sure a finding is.
An unreachable file nobody has touched in a year starts at 1.0, at 180 days 0.9, at 90 days 0.8. One edited in the last 90 days starts at 0.4. Unused exports start at 1.0 when the file has importers and none of them takes the symbol, and at 0.6 when that check is impossible. Deprecated exports start at 0.3. Then caps apply, and each one writes its reason into the evidence.
- Runtime loaders
- A loader in the same directory caps its neighbours at 0.4.
- Config and bootstrap
- Paths that look like startup code are capped too.
- Name written elsewhere
- An export whose name appears in another file is held for review.
- Unindexed files
- A name seen in a file repowise could not parse counts as a possible use.
| Finding | Confidence | Ready |
|---|---|---|
| formatLegacyDateunused_export | 100% | ✓ safe |
| toCsvRowunused_export | 100% | ✓ safe |
| src/legacy/parser.tsunreachable_file | 100% | review |
| config/feature_flags.pyunreachable_file | 40% | review |
Safe to delete
2 unused exports, 24 lines. High confidence, no runtime-load path.
Monorepos and workspaces
Checked against every repository that might use it.
A file that looks dead in its own repository may be the surface another repository calls. In a workspace, repowise checks each finding against cross-repository contracts, co-change and package dependencies before it assigns a tier. A contract link cuts confidence to 30 percent of its value and names the consumer; the other two add a note. A finding with a note is never marked safe to delete.
- Zombie packages
- Workspace packages no other package imports.
- Contract links
- HTTP routes and generated clients that point at a file.
- One repository at a time
- Pick one with --repo, or skip the check with --no-workspace.
- All at once for agents
- get_dead_code(repo="all") merges every repository and tags each finding.
$ repowise dead-code --safe-only# deletion-ready findings only$ repowise dead-code --kind zombie_package# packages no other package imports$ repowise dead-code --repo api --format json# one workspace repo, checked against the others get_dead_code(repo="all", tier="high", safe_only=True)# the same answer for an agent, over MCPIn plain words
Every finding comes with a number for how sure it is.
Dead code is code that nothing in the program reaches: a file no import loads, a function no caller runs. A graph can prove that nothing imports a file; a plugin loader can still read it by name. So each finding starts from a confidence set by its kind and its git history, anything that hints at runtime loading lowers it, and the evidence records why. You read the number and the reason, then decide.
How to read it
How to read the tiers
- High, 0.7 and up
- No references found anywhere in the repository. A strong cleanup candidate.
- Medium, 0.4 to 0.7
- Probably unused, but something indirect may reach it: a loader in the folder, a config path, the name written elsewhere. Review first.
- Low, under 0.4
- Plausibly used at runtime through reflection, a dynamic import or a published API. Shown when you ask for it.
- Safe to delete
- An unused export at 0.7 or higher on a path with no runtime-load risk. Start the cleanup here.
How it works
Built from data the index already holds.
- 01 / Exempt
Set aside what a framework calls
Entry points, routes, decorated handlers, tests, generated and vendored code. - 02 / Walk
Find what nothing reaches
Files, symbols and packages with no inbound import, call, inheritance or type edge. - 03 / Score
Start from git history
A file quiet for a year starts at 1.0; one edited in the last 90 days at 0.4. - 04 / Lower
Hold back what might load at runtime
Loaders, config paths and name matches cap a finding at 0.4, and the evidence names the cap.
Where it shows up
Shows up where the work already happens.
CLI
The report as a table, JSON or Markdown, filtered by kind, confidence or the deletion-ready flag.
repowise dead-code --safe-only
Coding agents
The agent asks for the high tier before a cleanup and gets each finding with its evidence and owner.
get_dead_code
Dashboard
Tiers, a safe-to-delete pile, an owner leaderboard and per-finding status, with each module's dead-code share.
VS Code
The extension's Findings view lists the repository's dead code by confidence tier, beside health, hotspots and ownership.
Questions
What people ask before they try it.
How does repowise decide that code is dead?
It walks the dependency graph the index already holds. A file with no importer that is not an entry point is an unreachable file. A public, top-level symbol that no import, call, inheritance or type reference reaches is an unused export. A top-level package no other package imports is a zombie package. Each finding then gets a confidence from 0 to 1: git history sets the starting value, and signs of runtime loading lower it. The analysis runs as part of repowise init and repowise update.
What does safe_to_delete mean?
A finding is marked safe to delete when its confidence is 0.7 or higher and its path carries no runtime-load risk factor, such as config, environment, bootstrap, database, script or served-asset paths. In practice that is an unused export with strong evidence. Unreachable files, zombie packages and unused internals stay review candidates whatever their confidence, because a build script or loader can read a file by path. In a workspace, a finding with a cross-repository note is never marked safe.
Will it flag my Django views, Flask routes or pytest fixtures?
No. Code a framework calls by convention is exempt before scoring: entry points such as __main__.py, main.go and files with a shebang, build and CI files and anything they name by path, test files, generated code, vendored trees, framework routes such as Next.js page files, and symbols registered by a decorator or annotation, including pytest fixtures, Flask and FastAPI routes, Celery tasks and Spring stereotypes. Names that follow dispatch conventions such as *Handler, *Plugin, register_* and on_* are treated as used.
Does it work in a monorepo or across several repositories?
Yes. Inside one repository, zombie packages are the top-level packages nothing else imports. Across a repowise workspace, each finding is checked against the other repositories before it is tiered: when another repository's HTTP route or generated client points at the file, confidence drops to 30 percent of its value and the finding names the consumer, and co-change with files in other repositories adds a note. Run it per repository with repowise dead-code --repo, or ask an agent for get_dead_code(repo="all").
Which languages does dead code detection cover?
It runs on the dependency graph, so it covers the languages repowise resolves imports for, from Python, TypeScript and JavaScript to Go, Ruby, PHP and Rust. Findings on Java, C#, C, C++, Kotlin and Swift are served as a preview with the measurement behind each one. Dynamic-import markers such as importlib.import_module, require.context, Class.forName and dlopen lower the confidence of nearby files in 14 languages. Rust private items are left to the compiler's own dead_code lint.
How do I use it from Claude Code, Cursor or Codex?
Through the get_dead_code MCP tool, which is on by default. get_dead_code(tier="high", safe_only=True) returns the deletion-ready findings, each with its confidence, reason, evidence and the file's primary owner. You can filter by kind, directory or owner, group results by directory or owner, and open one finding by id. The agent sees the same tiers as the CLI and the dashboard, so a finding lands in the same place on every surface.
Why is a file I know is unused only at 40 percent?
It changed recently, it has little history, or a cap fired. A file with a commit in the last 90 days starts at 0.4, because recent work is often unfinished; a file under 30 days old starts at 0.55 and one with no git history at 0.5. Otherwise a cap holds it at 0.4: a runtime loader in the same directory, a path that looks like config or bootstrap code, the symbol's name written elsewhere in the repository, or a name that appears in a file repowise could not index. Each cap adds a line to the finding's evidence, so the report tells you which one applied.
Last reviewed: October 2026
Get started
See what your repository can delete.
Index a public repository free and open its dead code report, tiered by confidence, with the deletion-ready findings marked. For a team rollout across private repositories, book a demo.