Dead Code Removal skill for Claude Code
Delete unused code with two kinds of proof. Claude configures the right analyzer for the stack with real entry points, triages candidates against known false-positive patterns such as dynamic imports, framework conventions and external callers, and requires a second signal like search, coverage or runtime logs before deleting. Removals land in small batches by kind with the full build and test gate after each, and confirmed false positives go into analyzer config.
- name
dead-code-removal - size142 lines · ≈1,800 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Finds and safely deletes unused code, files, exports, dependencies, feature flags, and config with static analyzers such as knip for JavaScript and TypeScript, vulture and deptry for Python, deadcode and staticcheck for Go, and cargo-machete for Rust, then confirms each candidate with dynamic evidence (coverage, logs, flag evaluations), removes it in small reviewable batches, and proves nothing broke. Use when the user asks to remove dead code, unused exports or dependencies, clean up stale feature flags, shrink a bundle, or reduce maintenance surface. Try asking
- Find and remove unused exports, files and dependencies in this TypeScript repo
- Which Python functions in src/ are never called? Delete the ones that are safe
- Clean up feature flags that have been at 100% for months
Use it when
- The codebase has accumulated unused files, exports, or functions
- Dependencies may be unused or the bundle is larger than it should be
- Feature flags have been fully rolled out for weeks
- You want an analyzer like knip wired into CI to keep it clean
Skip it when
- You do not know why the code exists yet — use code-archaeology first
- You want to restructure working code rather than delete it — use safe-refactoring instead
How to check the result
Each deleted item lists two independent signals of non-use, and build, typecheck, lint and the full test suite pass after every batch.
The common mistake it prevents
Trusting the analyzer without configuring entry points, then deleting code reached only through dynamic imports, framework conventions or other repositories.
Removal is done when every deleted item had two independent pieces of evidence that it was unused (a static analyzer plus a search, coverage, or runtime signal), the deletions landed in small batches each passing build, typecheck, tests, and lint, and anything reachable from outside the repo (public API, CLI, routes, jobs, reflection) was checked before deletion, not after.
Workflow
- [ ] 1. Baseline: clean tree, green build and tests, record bundle/dep counts
- [ ] 2. Configure the analyzer with real entry points; run it
- [ ] 3. Triage candidates into: delete / keep (false positive) / ask
- [ ] 4. Confirm each delete with a second signal
- [ ] 5. Delete in batches by kind; full checks after each batch
- [ ] 6. Encode the false positives as config so the analyzer stays clean
- [ ] 7. Report what was removed, evidence, and what was kept and why
2. Analyzers
| Stack | Tool | Finds | Command |
|---|---|---|---|
| JS/TS | knip | Unused files, exports, types, deps, devDeps, unlisted deps, binaries | npx knip · npx knip --production · npx knip --reporter json |
| Python | vulture | Unused functions, classes, variables, imports | vulture src/ --min-confidence 80 |
| Python | deptry | Unused, missing, and transitive dependencies | deptry . |
| Python | ruff | Unused imports/variables (F401, F841) | ruff check --select F401,F841 . |
| Go | deadcode | Functions unreachable from main/tests | go run golang.org/x/tools/cmd/deadcode@latest -test ./... |
| Go | staticcheck | Unused identifiers (U1000) | staticcheck -checks U1000 ./... |
| Rust | compiler + cargo-machete | Dead code warnings; unused crates | cargo build 2>&1 | grep 'never used' · cargo machete |
| Java/Kotlin | IDE inspection, ProGuard/R8 usage report | Unused classes/members | IDE “Unused declaration” scan |
| CSS | PurgeCSS-style content scan, coverage tab | Unused selectors | Browser DevTools Coverage |
ts-prune is deprecated; use knip. Configure entry points before trusting results;
most false positives come from the tool not knowing how the app starts:
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": ["src/main.ts", "src/workers/*.ts", "scripts/*.ts"],
"project": ["src/**/*.ts", "scripts/**/*.ts"],
"ignore": ["src/generated/**"],
"ignoreDependencies": ["@types/node"]
}
Save as knip.json. In monorepos knip reads workspaces; run per workspace with
--workspace <name> while tuning. For vulture, generate a whitelist of confirmed false
positives: vulture src/ --make-whitelist > vulture_whitelist.py, review it, and pass it
on later runs.
3. Triage: false positive patterns
Static analysis cannot see these; check each candidate against the list:
| Pattern | How to check |
|---|---|
| Public API of a published package | Is it exported from the package entry? Semver: removal is a major change |
| Framework conventions (Next.js pages, Django views/admin, Rails callbacks, pytest fixtures) | Configure the analyzer’s plugin, or keep |
Dynamic access: getattr, import() with variables, reflection, DI containers, string-based routing | rg -n "<name>" including strings, YAML, JSON, templates |
| Entry points outside code: CLI scripts, cron, serverless handlers, queue consumers | Check deploy configs, package.json bin, pyproject scripts, infra repo |
| Called from other repos or clients | Search other repos; check API logs for the route |
| Used only in tests | Delete the code and the tests together, or keep if tests document intended API |
| Loaded by name from DB/config (plugins, templates) | Query the config/DB values |
4. Second signal
Pick one per candidate, stronger for riskier code:
- Repo-wide text search including non-code files:
rg -n --hidden -g '!node_modules' -g '!.git' "functionName|function_name". - Coverage from the full test suite and, better, from production-like traffic (coverage
agents,
coverage runin staging) showing zero hits over a representative period. - Runtime logs/metrics: route hit counts, job executions, log lines emitted by the code.
- For risky paths with no signal: add a log or counter (“tombstone”), deploy, wait a full business cycle (including month-end jobs), then delete if zero.
- Git:
git log -S'functionName' --onelineto see if it was recently added (likely in-progress work, ask the author) or abandoned years ago.
5. Delete in batches
Order: unused files → unused exports/functions → unused dependencies → stale flags and config. One kind per commit or PR, under ~400 changed lines, so review is quick and reverts are surgical.
npx knip --fix --fix-type exports,types # removes export keywords; review the diff
npx knip --fix --allow-remove-files # deletes unused files; review carefully
npm uninstall left-pad moment # deps knip reported, after confirming
After each batch run the full gate: build, typecheck, lint, all tests, plus bundle size or dependency count to show the effect. Keep auto-fix output under review; never commit a tool’s bulk deletion without reading the diff.
Stale feature flags
- List flags with their state and last change in the flag service; candidates are 100% on or off for more than ~30 days with no recent evaluations of the other branch.
- Confirm with the flag owner. Remove the flag check, keep the winning branch, delete the losing branch and its tests, then delete the flag in the service after the code deploys (not before, or old code paths break).
Unused dependencies
Remove from the manifest, regenerate the lockfile with the project’s package manager, then build and test. Check for runtime-only use (plugins loaded by config such as Babel, ESLint, PostCSS, pytest plugins) before removing; analyzers often miss these.
Gotchas
- Deleting a public export of a library is a breaking change even if nothing in the repo uses it. Deprecate first, remove in the next major.
- Generated code shows up as unused; exclude generated directories rather than deleting.
- Code reachable only at month-end, year-end, or on rare errors looks dead in a short observation window. Match the window to the business cycle.
- Large “cleanup” PRs mixing deletion with refactoring get rubber-stamped or stall; deletion only.
- Database columns and tables used by removed code need their own expand/contract migration later; do not drop them in the same change.
--fixmodes can leave empty files or barrel exports; run the analyzer again after.
Output
**Removed** (4 PRs, all checks green after each):
| Batch | Items | Evidence | Effect |
|---|---|---|---|
| Unused files | 23 files | knip + rg (0 refs) + coverage 0 hits | -3,410 lines |
| Unused exports | 61 exports | knip; 4 kept as public API | -820 lines |
| Dependencies | moment, lodash.merge, request | knip + rg; no config/plugin use | -2.1 MB node_modules, bundle -38 KB gz |
| Flags | new_checkout (100% on 94 days) | flag service + owner confirmed | -2 code paths |
**Kept (false positives, now in knip.json ignore):** src/plugins/* (loaded by name from config)
**Tombstoned for later:** legacy CSV importer route (log added; review 2026-11-30) About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.