Dead Code

Code that passes its tests while nothing calls it, exports that nobody imports, features that were built but never wired in, and the checks that find them before an agent builds on top of them or deletes something live. Start with the pillar post, a reproducible case of a function that passed its test while the application never called it.

Start with The function passed its test. The application never called it.

Dead Code

The function passed its test. The application never called it.

The new function returns the right answer. Its test passes. Running the application still gives the old result.

That is an easy mistake to make when a coding agent handles a feature in pieces. One step creates the implementation, another adds a test, and the change that connects it to a real application path gets lost between them.

I build Pharaoh, and this is one of the structural problems its PR Guard checks. Here is a small example you can run without Pharaoh, followed by what the actual classifier reported when I ran the same wiring pattern through it.

This is a controlled example, with a stubbed main-branch graph. It is not a customer incident or a capture from a live GitHub pull request.

A useful function, sitting on its own

Suppose an application needs a weekly digest total. The new implementation lives in src/digest.ts:

export function formatWeeklyDigest(rows: number[]): number {
  return rows.reduce((sum, row) => sum + row, 0);
}

The name is deliberately specific enough to find again. Its behavior is small enough to check without building a service around it. Put this in src/digest.test.ts:

import assert from "node:assert/strict";
import { formatWeeklyDigest } from
Dan Greer · · 5 min read
Dead code definition for developers: essential guide for engineers
Dead Code

Dead Code Definition: The Essential Guide for Engineers

Dead code is code whose removal preserves the program's observable behavior for the inputs and configurations you support, including its outputs, state changes, exceptions, and external interactions. Code after an unconditional return is one example. Code that runs can be dead too.

The difficult word is observable. A calculation's result might go unread while the call that produced it still rejects a request, writes an audit record, registers a handler, or changes shared state.

I wouldn't approve a deletion from a missing-reference result alone. First I'd want to know what the search covered.

Dead, unreachable, and unused mean different things

Term What it tells you What still needs checking
Unreachable code Execution cannot reach it under the conditions being considered. Whether those conditions cover the supported configurations.
Unused value Nothing reads that value. Whether evaluating its initializer has effects.
Unused function candidate An analysis found no relevant caller. Entry points and consumers outside the analysis.
Dead code Removing it preserves observable behavior within the stated scope. Whether the evidence supports that conclusion.

This distinction also exists in compiler work. LLVM's dead-instruction check considers both use and side effects, while its global elimination pass works on unreachable internal globals, a scope

Dan Greer · · 5 min read
Diagram showing 11 ways PRD alignment with actual code enhances AI project outcomes
Dead Code

11 Ways PRD Alignment With Actual Code Improves AI

Suppose a PRD asks for billing reminders. In this hypothetical build, sendBillingReminder() exists, its unit test passes, and the review looks ready, but the helper was attached to a scheduled job that's no longer enabled while the application now starts that flow from a billing event. The function can work perfectly when you call it directly.

The customer still gets nothing.

When I'm checking PRD alignment with actual code, I want the trigger established before anyone starts editing, the requirement compared with the current behavior, and a record of what could actually be verified. The reminder example makes that task specific enough to check.

Before the build: check the PRD's assumptions

1. Turn the requirement into an observable result

“Send billing reminders” leaves too much open. For this example, write down which accounts qualify, the trigger that starts processing, when a reminder should arrive, and what should happen if the same work runs again. These are decisions for whoever owns the feature. Don't let the agent fill them in silently.

2. Find the trigger the application uses today

Start from the repository root with ripgrep's documented search syntax:

rg -F 'sendBillingReminder' .
rg -i 'billing|reminder' .

rg searches the current checkout

Dan Greer · · 5 min read
Dashboard showing tools and metrics to find unused code in TypeScript project efficiently
Dead Code

How to Find Unused Code in Your TypeScript Project Fast

An unused export and an unused function are different things. The function might still run inside its own file, in which case removing the export could be enough.

That distinction gets harder when a helper comes through a barrel, a test imports it, another package exposes it, and the report still says unused. I'd rather investigate one finding properly than approve a large deletion because the output looked convincing.

Start with Knip. Get a report, check what the analysis includes, then decide what to remove.

Run Knip before changing the configuration

From the repository root, follow Knip's manual installation instructions:

npm install -D knip typescript @types/node
npx knip

The current Knip v6 requires Node.js 20.19.0 or later. Its docs also list Bun as an option.

knip reports unused files, exported symbols, and dependencies. Start with its defaults, look at the reported paths, check a few findings against actual callers, and only add configuration where the default analysis needs help.

TypeScript can also report unused locals and parameters.

Turn on noUnusedLocals and noUnusedParameters in tsconfig.json to enable those checks.

For a reported export, check whether the declaration is still used within its file, whether another workspace

Dan Greer · · 5 min read
Dead code detection MCP concept showing agent optimizing code for faster intelligence development
Dead Code

Dead Code Detection MCP: Boosting Agent Intelligence Fast

In the captured result below, PermissionContainer sits under Dead, usernameHandler sits under Possibly unused, and the latter comes with three files to inspect before anyone deletes it. Both lack a detected path from an entry point. They need different follow-up work.

I build Pharaoh, a hosted MCP server for TypeScript and Python, and I want an agent to stop at that second label, open the files behind it and explain what it found. A deletion proposal should include the evidence that makes it reasonable. This post walks through that check, including the branch limit that matters when you're working on an unmerged change.

MCP carries the result; the server does the analysis

For tool delivery, the MCP tools specification describes how servers expose tools to a model, how clients discover and call them, and what a tool result contains, leaving the analysis itself to each tool's implementation. Connecting an MCP server doesn't establish that its dead-code detector is accurate.

For Pharaoh, the useful result is a classification backed by repository structure, with separate treatment for code that no entry point reaches and no other file names and whose file has no production import, code that is unreached but still named

Dan Greer · · 6 min read