Dead Code Removal skill for Codex
Delete unused code with evidence, not hope. Codex treats a knip, vulture or deadcode report as a list of suspects, then checks each one for reflection, string lookups, framework conventions and outside callers before deleting anything. Removals land in themed batches (dependencies, files, exports, symbols) with a full build and test run after each, and every kept false positive gets a documented ignore.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- dead-code-removal Installs to ~/.agents/skills/dead-code-removal/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install dead-code-removal The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project dead-code-removal Installs to .agents/skills/dead-code-removal/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/dead-code-removal.zipunzip -o dead-code-removal.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) dead-code-removal PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $dead-code-removal, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Finds and safely deletes unused files, exports, functions, dependencies and stale feature-flag branches using knip for JavaScript and TypeScript, vulture and deptry for Python, deadcode and staticcheck for Go, and cargo-machete for Rust, then proves nothing dynamic still reaches the code before removing it in small reviewable commits. Use when asked to clean up unused code, remove dead exports or unused npm or pip packages, shrink a bundle or dependency list, or delete a module everyone thinks is unused.
Use it when
- A codebase has accumulated unused files, exports or packages
- You want to shrink install size, bundle size or attack surface
- A feature was retired and its code may still be lying around
- Lint reports unused symbols and you need to decide which are real
Use something else when
- You are restructuring live code without deleting it: use safe-refactoring
- The goal is a smaller JavaScript bundle for users: use bundle-size-reduction
How to check the result
Rerun the detector after the last batch and get only documented ignores, with build, typecheck, tests and app start all passing.
The mistake it prevents
Deleting a function that static analysis called unused but a plugin registry, route convention or cron job loads by name at runtime.
Deleting code is safe when three things are true: a tool reported it unused, a search for dynamic references came back empty, and the full test suite plus build pass after removal. A tool report alone is a list of suspects, not a verdict.
Before you start
- Read
AGENTS.mdfor the test, lint and build commands, and run them once on a clean tree so you know the baseline (pre-existing failures are not yours). - Ask what counts as public: a library’s exported API is used by code you cannot see. Never remove exports from a published package without a deprecation cycle (see semantic-versioning).
- Work on a branch; deletion PRs are easy to review only when they are small and themed.
Tool choice
| Ecosystem | Default tool | Finds | Command |
|---|---|---|---|
| JS / TS | knip | unused files, exports, types, deps, devDeps, unlisted deps | npx knip |
| Python code | vulture | unused functions, classes, variables, imports | vulture src/ --min-confidence 80 |
| Python deps | deptry | unused, missing and transitive deps vs pyproject.toml | deptry . |
| Go | deadcode | functions unreachable from main (whole-program) | go run golang.org/x/tools/cmd/deadcode@latest -test ./... |
| Go (lint) | staticcheck | unused identifiers (U1000) | staticcheck ./... |
| Rust | compiler + cargo-machete | dead_code warnings; unused crates | cargo build, cargo machete |
| CSS | PurgeCSS report, or coverage in browser DevTools | selectors never matched | project-specific |
npx, pip install and go run ...@latest download packages, which needs network; in a
Codex sandbox with network off, ask for approval or use the versions already in the
lockfile. Prefer adding the tool as a dev dependency when the team will rerun it.
Workflow
- [ ] 1. Baseline: tests, build, typecheck green (or failures recorded)
- [ ] 2. Configure the tool with the real entry points
- [ ] 3. Run it; save the raw report
- [ ] 4. Classify every finding (table below)
- [ ] 5. Verify "unused" candidates against dynamic references
- [ ] 6. Delete in themed batches: deps, then files, then exports, then symbols
- [ ] 7. After each batch: build, typecheck, tests, bundle or start the app
- [ ] 8. One commit per batch with the evidence in the message
Step 2: configure entry points
False positives come mostly from entry points the tool does not know about. For knip,
add a knip.json (or knip.ts) only when the auto-detected plugins miss something:
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": ["src/index.ts", "scripts/*.ts", "src/workers/*.ts"],
"project": ["src/**/*.{ts,tsx}", "scripts/**/*.ts"],
"ignore": ["src/generated/**"],
"ignoreDependencies": ["@types/node"]
}
Useful knip runs:
npx knip # everything
npx knip --production # only production code paths (skips tests, devDeps)
npx knip --include files,dependencies
npx knip --reporter json > knip-report.json
For vulture, mark known-dynamic names in a whitelist rather than lowering confidence:
vulture src/ --min-confidence 80
vulture src/ --make-whitelist > vulture_whitelist.py # review it, then:
vulture src/ vulture_whitelist.py --min-confidence 80
Step 4: classify findings
| Finding | Usually | Action |
|---|---|---|
| Unused dependency, not imported anywhere | Real | Remove from manifest, regenerate lockfile |
Unused dependency used by a CLI or config (eslint-config-*, @types/*, babel plugins) | False positive | Ignore in config with a comment |
| Unused file with no imports and no recent commits | Real | Delete after step 5 |
| Unused export, used inside its own file | Half-dead | Drop the export, keep the symbol |
| Unused export in a published library | Public API | Deprecate, do not delete |
| Unused function referenced by name in strings, decorators, routes, DI | False positive | Whitelist with a reason |
| Code behind a flag that is 100% on (or off) for weeks | Stale branch | Remove the losing branch and the flag together (see feature-flags) |
| Test-only helpers for deleted code | Real | Delete with the code |
Step 5: hunt dynamic references
Static tools miss anything resolved at runtime. Before deleting generateInvoicePdf
or legacy_export.py, search for the name in every form it could take:
rg -n "generateInvoicePdf|generate_invoice_pdf|generate-invoice-pdf" --hidden -g '!node_modules'
rg -n "legacy_export" -g '*.{yml,yaml,json,toml,ini,cfg,html,sql,md}' # configs, templates, cron
Check specifically:
- Reflection and registries:
getattr,importlib,require(variable), dynamicimport(), Spring component scanning, Rails autoloading, plugin entry points inpyproject.toml. - Framework conventions: file-system routes, Django management commands, Celery tasks registered by decorator, migrations (never delete applied migrations).
- Out-of-repo callers: other services, infra scripts, scheduled jobs, notebooks, SQL views, public URLs and webhooks. Ask the user when a symbol might be called from outside.
- Production evidence when available: route hit counts, logs, coverage from a canary. “No calls in 90 days of logs” is stronger than any static report.
If a candidate is uncertain, do not delete it. Add a one-release tombstone instead (log or metric on entry) and list it for the user.
Step 6 and 7: delete in batches, verify each
Order matters: dependencies first (smallest blast radius), then whole files, then exports, then individual symbols. After each batch:
pnpm install && pnpm typecheck && pnpm test && pnpm build # adapt from AGENTS.md
npx knip # new orphans appear after deletes
Deleting one file often orphans its imports; rerun the tool until the report stops
shrinking. knip can apply removals (npx knip --fix, plus --allow-remove-files to
delete files); review its diff like any other change rather than trusting it blindly.
Hard rules
- Never delete applied database migrations, public API endpoints, or exported library symbols as “dead code”; those need deprecation and a migration path.
- Never combine dead-code removal with behaviour changes or refactors in the same commit.
- Do not lower tool confidence or add blanket ignores to make the report clean. Each ignore gets a comment saying why the code is live.
- Keep deletions reversible: one theme per commit so
git revert <sha>restores it. - Ask before deleting anything outside the requested scope, even if it is obviously dead.
Commit and report
chore: remove unused invoice PDF v1 pipeline
knip and rg found no imports or string references to src/invoice/v1/**.
Route /api/invoices/v1/pdf had 0 requests in 90 days (dashboard link in PR).
Removes 1,240 lines and the pdfkit dependency.
Report to the user:
## Dead code removal
| Batch | Removed | Evidence | Verified by |
|---|---|---|---|
| deps | pdfkit, left-pad | not imported (knip), build green | pnpm build, tests |
| files | src/invoice/v1/** (14 files) | no imports, no string refs, 0 route hits | tests, typecheck |
### Kept on purpose (false positives)
- `src/plugins/*.ts` - loaded by glob in plugin-loader.ts:12 (added to knip entry)
### Needs a human decision
- `exportLegacyCsv` - referenced in a cron config outside this repo?
Verification
- Tool report rerun after the last batch shows only documented, ignored items.
- Build, typecheck, tests and app start pass; bundle or install size before and after is reported when the user cares about size.
- Every remaining ignore has a reason comment.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Dead Code Removal"
short_description: "Delete unused code with evidence, not hope"
default_prompt: "$dead-code-removal Find unused exports and npm packages in this TypeScript repo and remove them safely" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.