Skip to content
Back to skills

Dead Code Cleanup

ASecurity

Scans a repo for likely-unreferenced top-level symbols (JS/TS exports, Python def/class, Go exported func), reports them as removal candidates, and writes a structured `dead-code-report.md` (summary + high/low-confidence candidate tables). Use when asked to find dead code, unused exports, or unreferenced functions, or to clean up a codebase. Advisory-first — never deletes without explicit confirmation.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 6, 2026
developmentpythonrustgonodeexpresstestinggitapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 6, 2026

npx -y skills add sunitghub/canon-skills --skill dead-code-cleanup --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dead Code Cleanup?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dead Code Cleanup
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/sunitghub-dead-code-cleanup/badge)](https://www.skillsdirectory.com/skills/sunitghub-dead-code-cleanup)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dead-code-cleanup
description: Scans a repo for likely-unreferenced top-level symbols (JS/TS exports, Python def/class, Go exported func), reports them as removal candidates, and writes a structured `dead-code-report.md` (summary + high/low-confidence candidate tables). Use when asked to find dead code, unused exports, or unreferenced functions, or to clean up a codebase. Advisory-first — never deletes without explicit confirmation.
category: dev
tags: [cleanup, code-quality, dead-code, verification]
---

# Dead Code Cleanup

Finds code that nothing calls and reports it — it does not delete anything on its own. The scan
edits no existing file; it writes a structured `dead-code-report.md` (confirm-first) and, only on
per-candidate confirmation, removes code as a normal gated edit.
Complements `mutation-test` (which checks whether *existing* tests have teeth): this skill checks
whether the code those tests cover is still reachable at all.

**Advisory-first, manual-trigger only.** No scheduling, no auto-PR, no auto-delete. Removal, once
the user confirms a candidate, is a normal code edit that goes through the sprint's existing
review/eval gates like any other change — this skill never bypasses them.

## Scope

- Default scope is the repo root (`git rev-parse --show-toplevel`). A user-supplied path or glob
  narrows it.
- Always exclude `node_modules`, `.git`, `dist`, `build`, `__pycache__`, and any vendor directory.
- If scope resolution fails (not a git repo, or `git rev-parse` errors), report that and stop —
  never fall back to scanning an unbounded filesystem path.

## The scan (follow exactly)

1. **Extract candidate declarations.** Grep-based, one pattern set per extension present in
   scope:
   - JS/TS (`.js`, `.ts`, `.jsx`, `.tsx`): `export (function|const|class) <Name>`
   - Python (`.py`): top-level `def <name>` / `class <Name>` not prefixed with `_`
   - Go (`.go`): `func <Name>(` where `<Name>` starts with an uppercase letter (exported),
     excluding `Test*`/`Benchmark*`/`Example*` (test-runner convention, not a static reference —
     see Gotchas)

   Skip any extension not in this list — do not guess a pattern for an unfamiliar language.

2. **Count references.** For each candidate symbol, `grep -rnw` the symbol across the scope,
   excluding the declaration's own line. Zero other matches anywhere in scope → dead-code
   candidate.

3. **Classify confidence.**
   - **High** — zero other references, and the declaring file is not an entry-point pattern.
   - **Low** — zero other references, but the declaring file matches an entry-point pattern:
     `index.*`, `__init__.py`, `main.go`, `cli.*`, or any file a `package.json`/shebang/build
     config names directly. These are likely public API surface or process entry points invoked
     by name from outside grep's reach — flag them, don't bury them, but don't recommend removal.

4. **Report.** The scan itself edits no file. Print the Summary inline, then — **confirm-first**,
   mirroring `context-check`/`context-doctor` — write a structured `dead-code-report.md` at the
   repo root: ask `Write dead-code-report.md to the repo root? (y to confirm)`, overwrite if present
   (a point-in-time snapshot, not a log). Structure:

   ```markdown
   # Dead Code Report

   _Scope: <path> · Generated: <ISO date>_

   ## Summary

   | Metric | Count |
   |---|---|
   | High-confidence candidates | N |
   | Low-confidence (entry-point) | M |
   | Languages scanned | js, py, go |

   > Advisory only — nothing removed. Confirm candidates to remove; removals go through the sprint's gates.

   ## High-confidence candidates

   | File:line | Symbol | Kind | Reason |
   |---|---|---|---|
   | src/foo.js:12 | oldHelper | function | no references found in scope |

   ## Low-confidence — verify manually

   | File:line | Symbol | Kind | Reason |
   |---|---|---|---|
   | cli/index.py:1 | main | function | entry-point file — likely invoked by name |

   ## Caveats

   - Dynamic dispatch/reflection, barrel re-exports, string-only references, CLI entry points, and
     test-runner conventions can hide real callers — spot-check before removing (see Gotchas).
   ```

   Render `None.` under a candidate section with no rows. If **both** tables are empty, still write
   the report (both `None.`) and say so — a clean result, not a no-op. The `Kind` column is the
   declaration type (`function`/`class`/…); confidence is expressed by which table a row lands in.

## Confirm-then-remove loop

After the report:

1. Ask the user which candidates (if any) to remove — accept a batch selection, not just one at
   a time. **Before applying any removal, surface the working-tree state** — check
   `git status --porcelain`; if there are uncommitted changes, tell the user the removals will be
   added to their current diff and offer **commit-first / proceed-anyway / cancel**. This is an
   *informed confirmation, not a hard clean-tree gate*: git makes every removal revertible
   per-file (`git restore <file>`) whether or not the tree was clean, so never block on a dirty
   tree — just make the undo choice visible (t-2201). A clean tree only buys a tidier isolated
   diff, which the user may or may not want.
2. For each confirmed candidate, remove the declaration (and now-orphaned code it alone made
   reachable, if obviously scoped to it) with a normal edit.
3. The removal becomes part of whatever sprint is active. It is graded by that sprint's own
   acceptance criteria and gates — this skill does not run its own separate close process.
4. If no sprint is active when removal is confirmed, say so and suggest `sprint start` before
   editing — don't edit outside an active sprint's diff.

## Gotchas

- **Dynamic dispatch and reflection** (`getattr(obj, name)`, `eval`, string-keyed routing
  tables) call a symbol by name at runtime — grep finds zero static references even though the
  symbol is live. Treat any candidate near a router/registry/dispatch table with suspicion; ask
  before removing.
- **String-only references** — a symbol name mentioned only in docs, a skill's own prose, a YAML
  config, or a CI workflow file counts as a reference for this heuristic's purposes (the grep
  pattern is name-based, not import-graph-based) but can still hide a real caller `grep -w`
  missed due to word-boundary quirks (e.g. template-literal interpolation). Spot-check a sample
  of "zero reference" results before trusting the count at scale.
- **Barrel/re-export files** (`export * from './foo'`) make a symbol look referenced by the
  barrel file itself even when nothing imports it *from* the barrel — this heuristic will
  under-report in that case, not over-report; treat "zero candidates" from a heavily-barrelled
  codebase with caution.
- **Test-only fixtures** (helpers used only inside `*.test.*`/`*_test.*` files) will show a
  reference and won't be flagged — correct behavior, since the code is still reachable from the
  test suite, but worth knowing if the goal is trimming test-only cruft too (out of scope here).
- **CLI entry points invoked only by name** from `package.json` `bin`/`scripts`, a shebang, or a
  Makefile are real usages grep won't see as a symbol reference — this is exactly what the
  low-confidence entry-point classification exists to catch; don't loosen that filter.
- **Go `Test*` functions are called by the test runner via naming convention, not by any textual
  reference** — `func TestFoo(t *testing.T)` matches the exported-func pattern (uppercase start)
  and will show zero other references, since nothing ever calls it by name in source. Live-checked
  against canon's own `tools/cockpit-daemon/main_test.go`: `TestPreviewRootFor` has exactly one
  match repo-wide — its own declaration. Always exclude `^func Test[A-Z]` (and `^func Benchmark[A-Z]`
  / `^func Example[A-Z]`) from Go candidates before counting references, rather than relying on
  the entry-point low-confidence filter to catch them — they aren't entry-point files, they're a
  distinct convention-dispatched class.

Files in this skill

  • SKILL.md7.9 KB
  • evals/evals.json3.4 KB
  • skill-eval-result.md3.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…