Aus dem Quell-Repository gerendert; Überschriften, Beispiele, Code, Tabellen, Links und Bilder bleiben erhalten.
Run TS Cleanup
A TypeScript codebase accumulates. Features get deprecated but their files stay. A refactor lands half-finished and both abstractions survive. An LLM writes cn for the fourth time in a fourth directory. Dependencies outlive the code that imported them. Types get exported "just in case" and never consumed. This skill sweeps all of it into a clean end state:
- Every dead symbol, file, export, type, and dependency is found by a real engine, not by guesswork — and each finding is triaged before deletion, never deleted on the engine's word alone.
- Every deletion lands in a reversible wave behind a passing verification gate, so any single step reverts with one
git revert. - The root cause is addressed, not just the symptom: duplicate helpers get consolidated, circular barrels get untangled, single-use wrappers get inlined — so the same bloat does not regrow.
- The codebase ends more navigable than it started, for the next agent and the next human, without a single behavioural change.
This is a finishing tool. Run it on a green main branch, not mid-feature.
When To Use
Trigger on phrases and repo states like:
- "clean up this TypeScript project", "my codebase is a mess", "remove the dead code"
- "find unused dependencies", "prune unused exports", "delete orphaned files"
- "there's too much AI slop in here", "tighten up the types", "untangle these imports"
- "set up knip", "run knip", "why is this bundle so big"
- Repo state: after a feature deprecation, a framework migration, or a long LLM-driven build-out.
Do NOT use this skill for:
- Pure formatting or style-only changes — run the project's own formatter; this skill deletes code.
- Branch, worktree, and repo-artifact cleanup — use
run-repo-cleanup. - Reviewing a diff or PR — use
run-review. - A dirty working tree — commit or stash first; Phase 0 refuses to start otherwise.
- A red baseline — fix the failing build or tests first. Cleanup on a broken repo cannot tell your breakage from its own.
Modes
Pick the mode explicitly at the start, from the user's ask. Announce which one you picked. Escalating mid-run is fine; silently doing more than was asked is not.
| Mode | Runs | Fires on |
|---|---|---|
| Sweep | Phase 0-1, then lint autofix and Wave 1 only | "tidy this up quickly", "drop the unused deps" |
| Standard | Phases 0-3, then Waves 1-5 | "clean up the dead code" — the default |
| Deep | Standard, plus Phase 4 root-cause work, type hardening, and structural refactor | "the codebase is a mess", "make this maintainable" |
The Engines
No single tool finds everything. Each engine sees one scope and is blind to the others.
| Engine | Finds | Scope |
|---|---|---|
| Knip | Unused files, exports, types, dependencies | Module graph |
| Biome / Oxlint / ESLint / Ultracite | Unused locals, dead imports, correctness lint | File AST |
| `tsc` | Type errors, declaration-emit breakage | Program |
| `type-coverage` | any-creep, as a number | Program |
| `madge` | Circular dependencies | Import graph |
| anti-slop (Oxlint plugin) | LLM-generated antipatterns | File AST |
Knip and the linters are complements, not alternatives: Knip finds a file nothing imports, a linter finds an import nothing uses. Read references/engines/engine-matrix.md for the full matrix and the auto-detection protocol.
Pinned Defaults
Decided once; override per-project only if the repo says otherwise (AGENTS.md / CLAUDE.md / CONTRIBUTING.md win over this skill).
| Key | Default | Why |
|---|---|---|
| the gate | tsc --noEmit → test → build, resolved for the detected package manager | The single verification barrier. Defined once in references/remediation/waves.md; every "Run the gate" in this skill means exactly that. |
| branch | chore/ts-cleanup | Never work on main. |
| config file | knip.jsonc (fallback knip.json) | JSONC carries comments explaining why each entry exists. |
| package manager | Detected from packageManager field, else lockfile | Never hardcode pnpm. See waves.md §1.2. |
| wave order | Deps → Files → Barrels → Exports → Types | Reverse order leaves dangling imports and ghost references. |
| commit unit | One commit per wave | Each wave reverts independently. |
| suppression | Targeted keys only | ignoreDependencies, ignoreBinaries, ignoreExportsUsedInFile, explicit entry. |
Non-Negotiable Safety Rails
- Start green. Verify
git status --porcelainis empty, the typecheck passes, and the test suite passes before touching anything. A cleanup that begins on a red baseline cannot distinguish its own damage from pre-existing damage. - Suppress false positives with targeted keys. Reach for
ignoreDependencies,ignoreBinaries,ignoreExportsUsedInFile, or an explicitentry— each one silences exactly the finding it names. A broad top-levelignoresevers graph edges instead, so files the ignored code imports start reporting as dead and get deleted. - Triage every finding before deleting it. Apply the Three-Question Verification Test to each one. An engine reports what it cannot see a reference to; framework entry points, dynamic imports, and published API surfaces are all invisible to it.
- Delete in wave order. Dependencies, then files, then barrels, then exports, then types. Each wave's output is the next wave's input.
- Run the gate after every wave, before every commit. A wave that fails the gate is reverted or fixed, never committed.
- Prove declaration-emit safety before un-exporting.
tsc --noEmitbuilds the implementation graph and never the declaration graph, so it passes on code thattsc -bwill reject. See references/types/declaration-emit.md. - Fix the cause, then the symptom. Consolidate the four copies of
cnbefore deleting three of them; migrate the call sites before pruning the abstraction.
The Six Phases
dead code · unused deps · AI slop · weak types · tangled imports
│
Phase 0 — Baseline: clean tree, green typecheck, green tests, branch cut
│
Phase 1 — Engine setup: detect the stack, configure each engine, resolve the manager
│
Phase 2 — Survey: run every engine, batch findings, score risk
│
Phase 3 — Triage: every finding gets a verdict — dead, false-positive, or deferred
│
Phase 4 — Root cause: consolidate duplicates, untangle cycles, inline wrappers [Deep]
│
Phase 5 — Waves: delete in order, gate after each, one commit per wave
│
zero dead code · green gate · one revertable commit per wave · a report you trustEach phase gates the next. If you are tempted to skip one, re-survey instead.
Phase 0 — Baseline
Think first: "Is this repo green right now, and can I prove it?"
- Refuse to proceed on a dirty tree:
test -z "$(git status --porcelain)" || { echo "Dirty tree — commit or stash first."; exit 1; }- Cut the working branch:
git checkout -b chore/ts-cleanup - Resolve the package manager and write the gate script once — references/remediation/waves.md §1.2-1.4 gives the detection snippet.
- Run the gate. Record that it passed. This is the baseline every later gate is compared against.
- Capture the starting metrics for the final report: finding counts per engine,
type-coveragepercentage, cycle count.
Gate → Phase 1 when: the tree is clean, the branch exists, the gate passes, and baseline metrics are recorded.
Phase 1 — Engine Setup
Think first: "Which engines does this repo already have, and which does it need?"
- Detect what is installed — Biome, Oxlint, ESLint, Prettier, Ultracite, Knip, madge, type-coverage:
python3 scripts/audit-ts-health.py --target .With no --check-* flag it runs all four audits. Passing any --check-* flag narrows it to just those.
- Generate or review the Knip config:
python3 scripts/init-knip-config.py --dry-run # inspect first
python3 scripts/init-knip-config.py --format jsoncIt detects frameworks and monorepo layout, and stamps a $schema matching the installed Knip major version.
- Resolve configuration hints before trusting any finding. Knip reports config hints ahead of issues; a hint left unresolved produces a cascade of false findings downstream. Tune entry points until the hints are gone — references/engines/knip-configuration.md has the plugin catalog, monorepo patterns, and compiler settings.
- Confirm zero broad top-level
ignorepatterns in the config.
Gate → Phase 2 when: every engine the mode needs is runnable, config hints are resolved, and no broad ignores remain.
Phase 2 — Survey
Think first: "What does every engine see, and how much of it is worth acting on?"
- Run the dead-code engine and batch its output:
python3 scripts/batch-findings.py --run --output markdown --out-file cleanup-plan.mdBatches 1-6 come from the dead-code engine, each with a risk score and a gate rendered for the detected manager.
- Run the remaining engines the mode calls for — lint,
type-coverage,madge. Their findings form batches 7-9. Commands in references/engines/lint-engines.md and references/engines/analysis-engines.md. - Read the batch definitions and risk model in references/detection/finding-classification.md — nine batches, each naming its producing engine and the wave it feeds. Several collapse: batches 4 and 5 both feed Wave 4, 3 and 9 feed Wave 3, 6 and 8 feed Wave 5.
Gate → Phase 3 when: cleanup-plan.md exists, every finding sits in a batch, and every batch carries a risk score.
Phase 3 — Triage
Think first: "Which of these findings would break production if I deleted it?"
Apply the Three-Question Verification Test to every finding:
- Framework lifecycle? Does a convention invoke this without importing it — a Next.js
page.tsx/route.ts, a Remixloader, an Astro content config, a Playwright fixture? - Dynamic invocation? Is it reached through a template-literal
import(), a string-keyed registry, or runtime reflection? - Public contract? Is it exported to consumers outside this repo's static graph, via
exportsortypesVersions?
Verify with the codebase, not from memory:
rg -g '!node_modules' -g '!dist' 'symbolOrPackageName'Codify each confirmed false positive back into config as a targeted key — never as a broad ignore. Framework entry catalogs, dynamic-import patterns, and lint/type false positives are in references/detection/false-positive-triage.md.
Gate → Phase 4 when: every finding in cleanup-plan.md carries an explicit verdict of dead, false-positive, or deferred. Zero blanks. Count them and state the number.
Phase 4 — Root Cause
Think first: "Why did this bloat appear, and what stops it coming back?"
Deep mode only. Sweep and Standard skip to Phase 5.
Deleting the symptom leaves the generator running. Work out which origin produced each cluster, then fix that:
| What you found | Where to go |
|---|---|
Four copies of cn, duplicate formatDate, useless try/catch, phantom null checks, boolean theater | references/detection/code-slop-catalog.md — origins and per-pattern remediation |
| Circular imports, bloated barrels, single-use wrappers, scattered helpers | references/remediation/structural-refactor.md — plan-first structural work |
| Brain methods, deep nesting, long parameter lists, speculative generality | references/remediation/complexity-thresholds.md — numeric thresholds to prioritise by |
any sprawl, missing strict flags, weak inference | references/types/strict-migration.md — the ratchet |
Consolidation is a code change like any other: run the gate and commit it before starting Wave 1.
Gate → Phase 5 when: duplicates are consolidated behind one canonical implementation, call sites are migrated, cycles are broken, and the gate passes.
Phase 5 — Waves
Think first: "What is the smallest deletion I can make, prove, and commit right now?"
Five waves, in order. After each: run the gate, then commit. A wave that fails the gate gets fixed or reverted — never committed. Full protocol, per-wave scope, and rollback in references/remediation/waves.md.
| Wave | Deletes | Additional check | Commit |
|---|---|---|---|
| 1 | Unused dependencies, then refresh the lockfile | — | chore(deps): prune unused dependencies |
| 2 | Orphaned and unreferenced files (git rm) | — | chore: remove unreferenced files |
| 3 | Dead barrel re-exports; repoint consumers at source modules | — | refactor: prune dead barrel re-exports |
| 4 | Test-only leaks and in-file-only exports (strip export) | Declaration emit, then lint autofix | refactor: internalize private exports |
| 5 | Unused types, interfaces, enum members | Declaration emit | refactor: prune dead types |
Wave 4 needs the linter bridge. Stripping an export leaves the importing files holding specifiers that now reference nothing. Run the detected engine's autofix immediately after — recipes in references/engines/lint-engines.md, timing rationale in waves.md §7. Verify declaration emit before stripping anything reachable from a public signature: references/types/declaration-emit.md.
Post-flight
- Re-run every engine. Confirm zero remaining findings, or an explicit deferred list.
- Diff the metrics against the Phase 0 baseline: findings removed,
type-coveragedelta, cycle count delta, dependency count delta. - Remove scratch artifacts:
cleanup-plan.md,.cleanup-gate.sh, any baseline JSON. - Report what was deleted, what was deferred and why, and the metric deltas.
Gate → done when: every wave is committed, the gate passes on the final commit, engines report clean, and the report is delivered.
Common Mistakes
| Mistake | Consequence | Instead |
|---|---|---|
Broad top-level "ignore": ["src/legacy/**"] | Knip stops traversing those files, so their imports report as dead and get deleted | Use ignoreDependencies / ignoreBinaries / ignoreExportsUsedInFile / explicit entry |
| Acting on findings before resolving config hints | Every downstream finding is suspect; you triage noise | Clear hints in Phase 1, then survey |
| One big cleanup commit | No bisect, no partial revert, unreviewable | One commit per wave |
| Deleting exports before pruning dependencies | Dangling imports and confusing compiler cascades | Wave order: deps → files → barrels → exports → types |
Trusting tsc --noEmit before un-exporting | It never builds the declaration graph; tsc -b then fails on TS4023 | Verify declaration emit explicitly |
Hardcoding pnpm in the gate | The gate silently no-ops on npm/yarn/bun repos | Resolve the manager from packageManager or the lockfile |
| Deleting duplicate helpers without migrating callers | Each copy had drifted; consumers depended on the differences | Consolidate, migrate, gate, then delete |
| Skipping Phase 0 because the repo "looks fine" | Pre-existing failures get attributed to the cleanup | Prove green first |
Scripts
Python 3 stdlib only — no dependencies, no network beyond the engines they invoke.
| Script | Phase | Purpose | Mutates? |
|---|---|---|---|
scripts/audit-ts-health.py | 1, 4 | Detects linters and formatters, audits tsconfig.json flags, finds import cycles, flags slop markers. | No |
scripts/init-knip-config.py | 1 | Scans frameworks and monorepo layout; emits knip.jsonc/knip.json with a version-matched $schema. | Yes — --dry-run to preview, --force to overwrite |
scripts/batch-findings.py | 2 | Runs or parses the dead-code engine; batches findings, scores risk, renders gates for the detected manager. | Only with --out-file |
Run any script with --help for its full flag surface.
References
- references/engines/engine-matrix.md — which engine finds which class of problem, at what scope and speed; the auto-detection protocol.
- references/engines/knip-configuration.md — Knip config formats, schema, monorepo workspace isolation, compilers, the plugin catalog, and targeted-vs-broad ignores.
- references/engines/lint-engines.md — Biome, Oxlint, ESLint, and Ultracite recipes; safe formatting rules versus mutations to avoid mid-remediation.
- references/engines/analysis-engines.md —
type-coverage,madge, and the anti-slop Oxlint plugin: install, invoke, read the output. - references/detection/finding-classification.md — the six batches, which engine produces each, the wave each feeds, and the risk model.
- references/detection/false-positive-triage.md — the Three-Question Test, framework entry catalogs, dynamic imports, public contracts, and lint/type false positives.
- references/detection/code-slop-catalog.md — architectural origins of bloat and the slop patterns they produce, with detection commands and before/after remediation.
- references/remediation/waves.md — the gate, package-manager resolution, all five waves, the linter bridge, rollback, and the post-flight metrics gate.
- references/remediation/structural-refactor.md — plan-first structural work: inlining wrappers, breaking cycles, restructuring barrels, co-locating helpers.
- references/remediation/complexity-thresholds.md — cyclomatic and cognitive complexity thresholds and the code-smell index, for prioritising what to refactor first.
- references/types/strict-migration.md — eliminating
any-creep, the type-coverage ratchet, incremental strict-mode migration, type tidying. - references/types/declaration-emit.md — why un-exporting breaks
tsc -b, the TS4023/4081/4082/4060/2742/2883 matrix, and the pre-un-export checklist.
Bottom Line
Prove green → configure the engines → survey every scope → give every finding a verdict → fix the cause → delete in five gated waves → report the deltas.

