Manual removal suits narrow scope, while agent-driven removal suits broad scope; scope decides between them. Every engineering team with a codebase older than three or four years has dead code. How to remove it depends on the scope and risk profile. I have worked with enough codebases to say the approach for a 200-function backend module cleanup is not right for a 180,000-line repository audit.
Here is how I view the four main approaches, what each does well, and where each falls short.
Approach 1: Manual Grep and Review
Most teams begin with manual removal. An engineer suspects a module or function is dead, searches for references, finds none or very few, then deletes it. This is quick for targeted removal of specific known-dead things. No tooling is needed, and the work can fit in a paragraph in a PR description.
Manual grep fails through an illusion of precision. A codebase search for a function name can miss reflection, dynamic dispatch, or string-based invocation. Zero grep hits do not prove zero callers. They prove only zero literal occurrences of that search string in the source files searched.
Manual removal is unprioritized by nature. Engineers remove what they know about, rather than what is truly dead. Suspected dead functions are usually ones an engineer recently worked around or ones with obvious names like old_feature_handler. Functions quietly dead for four years in an overlooked utility module rarely emerge from manual investigation.
Use manual removal for targeted cleanup of specific things you know with high confidence, cleanup during other refactoring, and dead code in files already open and being modified for other reasons.
Approach 2: Linter Rules and Static Checkers
Most language ecosystems provide linters for unused imports, unused variables, and unreferenced private functions. TypeScript's noUnusedLocals and noUnusedParameters flags, Python's Pyflakes and Flake8, and Go's built-in unused import checker run quickly and reliably catch this specific dead code category.
Their limitation is scope. Linter rules work within a narrow visibility window, such as an unused local variable or unused private method. They do not analyze a whole-repository call graph. A public function in a utility module that no other module in the repository calls will pass every linter check because, to the linter, it is exported and any consumer could call it. The linter cannot know that no consumer actually does.
Linters are deliberately conservative when something may have external callers. That trade-off suits a fast-feedback CI tool, but not a serious dead code audit.
Use linter rules to enforce ongoing cleanliness after a major cleanup, catch trivially unused locals during development, and provide a lightweight first pass before deeper analysis.
We are not calling linters insufficient for their intended purpose. They are excellent at what they were designed to do, but not for cross-module reachability analysis.
Approach 3: Coverage-Guided Deletion
A code coverage run that identifies zero-coverage files and functions is genuinely useful. Files with zero coverage across a full test suite run are either dead or deeply undertested, so they deserve examination. Functions with zero coverage in a well-tested module are candidates for removal review.
Coverage-guided deletion has two meaningful limitations. First, test coverage does not equal production reachability. A function called in production but absent from every test will appear dead in coverage reports, and removing it will break production. This is the coverage-dead-code overlap problem: both signals are needed, not coverage alone.
Second, coverage-guided deletion does not create actionable batches. It reveals candidates, but a human must review each one, decide why it has zero coverage, dead or just untested?, and make the removal decision individually. For a codebase with 400 zero-coverage functions, that is significant manual effort. Coverage shows where to look; it does not automate the decision.
Use coverage-guided deletion for moderate-scale audits, up to a few hundred candidates, for ranking which modules to audit first, and as input to a more systematic process.
Approach 4: Agent-Driven Removal
Agent-driven removal, which is what YAGNI does, combines call graph analysis, runtime signal cross-validation, and automated PR generation for the parts of dead code removal that do not scale through manual effort. It targets cases too large for manual review and too complex for simple lint rules.
Unlike the other three approaches, the agent makes the removal and proposes it as a verified PR instead of merely surfacing a candidate for human review. For high-confidence findings, when static analysis and runtime traces agree that code is unreachable, the agent produces a diff, runs the test suite against it, and opens a pull request with the analysis attached. The engineer reviews the PR, rather than a candidate list.
This matters because, at scale, the bottleneck is not finding dead code candidates. It is the engineering time to evaluate each one, remove it, run tests, and submit a change. For the high-confidence subset, agent-driven removal takes that work off the human's plate.
Agent-driven removal does not replace the other three approaches in their domains. It will not catch unused locals that a linter catches in milliseconds, and it is not faster than manually deleting a specific function you already know is dead. Its value is in the 10,000-line-to-remove range, where the other approaches are not practical at the required accuracy.
Combining Approaches
In practice, the strongest dead code removal programs combine these approaches instead of selecting one. Ongoing linter enforcement keeps the accumulation rate low. Coverage-guided review during planning identifies the modules most worth examining. Agent-driven removal conducts the systematic audit of the backlog. Manual deletion covers targeted cleanup while a module is being actively developed.
Which approach to use depends on the problem's scope and the confidence required before deletion. A single function with high manual confidence can be deleted directly. A codebase-wide pattern of unused imports calls for a lint rule. For a 150,000-line codebase with years of accumulation, run the full analysis pipeline and commit the backlog in batches.
Use YAGNI on your codebase. Free plan, no credit card required.
Get Early Access