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.

  • namedead-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.

~/.claude/skills/dead-code-removal/SKILL.md

--- name: dead-code-removal description: Finds and safely deletes unused code, files, exports, dependencies, feature flags, and config with static analyzers such as knip for… license: MIT ---

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

StackToolFindsCommand
JS/TSknipUnused files, exports, types, deps, devDeps, unlisted deps, binariesnpx knip · npx knip --production · npx knip --reporter json
PythonvultureUnused functions, classes, variables, importsvulture src/ --min-confidence 80
PythondeptryUnused, missing, and transitive dependenciesdeptry .
PythonruffUnused imports/variables (F401, F841)ruff check --select F401,F841 .
GodeadcodeFunctions unreachable from main/testsgo run golang.org/x/tools/cmd/deadcode@latest -test ./...
GostaticcheckUnused identifiers (U1000)staticcheck -checks U1000 ./...
Rustcompiler + cargo-macheteDead code warnings; unused cratescargo build 2>&1 | grep 'never used' · cargo machete
Java/KotlinIDE inspection, ProGuard/R8 usage reportUnused classes/membersIDE “Unused declaration” scan
CSSPurgeCSS-style content scan, coverage tabUnused selectorsBrowser 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:

PatternHow to check
Public API of a published packageIs 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 routingrg -n "<name>" including strings, YAML, JSON, templates
Entry points outside code: CLI scripts, cron, serverless handlers, queue consumersCheck deploy configs, package.json bin, pyproject scripts, infra repo
Called from other repos or clientsSearch other repos; check API logs for the route
Used only in testsDelete 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 run in 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' --oneline to 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

  1. 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.
  2. 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.
  • --fix modes 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.

esc