The Agent Removed a 'Dead' Export That Was Still Imported Through a Path Alias
Quick answer
Do not let an agent delete an "unused" export just because a static analysis pass stopped referencing it. Modern codebases resolve imports through path aliases, barrel files, and dynamic import(), and a lot of dead-code tools model only the paths they can statically resolve. A common failure mode is that the agent runs a pruning pass, trusts a tool that reports an export as unreachable, and removes it. The repository still type-checks and the unit tests still pass, but a consumer that reaches the module through a path alias or a runtime string import gets a crash or a missing bundle chunk. Treat every automated deletion of an exported symbol as a breaking change until you have proven there is no aliased or dynamic reference to it.
How the false "dead" export happens
Consider a TypeScript project that uses a path alias so feature code can import from a stable name instead of a long relative path.
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@billing/*": ["src/services/billing/*"]
}
}
}
A module exports a helper that is consumed through that alias, and also loaded lazily through a registry that maps a string to an import.
// src/services/billing/receipt.ts
export function formatReceipt(order: Order): ReceiptLine[] {
return order.items.map((item) => ({
sku: item.sku,
total: roundMoney(item.price * item.quantity),
}));
}
// consumed via the alias somewhere in the feature layer
import { formatReceipt } from "@billing/receipt";
// and loaded lazily through a runtime key
const receiptLoaders: Record<string, () => Promise<unknown>> = {
default: () => import("@billing/receipt"),
};
Now point an agent at a pruning tool such as knip or ts-prune. Some configurations, especially ones that do not fully honor paths or that treat the argument of a dynamic import() as opaque, will mark formatReceipt as having no resolved caller. The agent reads "unused export," deletes it, and reports a clean tree-shaking win. The feature layer that imports through @billing/receipt is the first thing that breaks, and the lazy registry fails later at runtime when the chunk no longer contains the symbol.
Why the agent is confident and wrong
The agent is optimizing for the signal the tool gave it. A dead-code report is a list of symbols, and the measurable goal is to make the entries disappear by deleting code. The tool itself is usually honest about its limits, but those limits live in documentation, not in the output. A resolver that only understands relative import statements cannot see an aliased or string-driven reference, so it reports nothing rather than reporting uncertainty. The agent does not distinguish "proven unreachable" from "I could not resolve the reference." Both look identical in the report, and the agent picks the interpretation that produces the requested edit.
| Reference type | Can a basic resolver see it? | Deletion risk |
|---|---|---|
| Relative static import | Yes, reliably | Low |
Path alias (@billing/...) | Only if the tool reads tsconfig paths | High when misconfigured |
Barrel re-export (export * from) | Sometimes, depending on traversal depth | Medium |
Dynamic import(variable) | Rarely, the specifier is a runtime string | Very high |
| Reflection or registry lookup | No | Effectively invisible |
A guard routine before deleting anything
The goal is not to ban pruning. It is to make deletion evidence-based rather than report-based. A few minutes of checking turns a silent runtime break into a non-event.
- Confirm the tool understands your aliases. For TypeScript, point it at the same tsconfig the compiler uses and verify on a known aliased import that it resolves instead of reporting a false positive.
- Search the raw source for the symbol name and every path segment that could reach it, not just the import specifier the tool understands. Grep for the export name, the alias prefix, and the file basename.
- Look for dynamic specifiers. Any
import(whose argument is built from a variable, a registry, or a configuration object is a hard stop for automatic deletion. - Check published packages separately. If the package is consumed outside this repository, an export can be a public API even if nothing inside the monorepo imports it.
If you want the agent to prune safely, give it the constraint explicitly: "Delete only exports that have zero references after resolving tsconfig path aliases and dynamic imports. Do not delete any symbol reachable through a barrel file or a runtime registry, and list every reference you checked." That forces the agent to produce evidence instead of trusting a single tool's output.
When the export really is dead
Plenty of exports genuinely are unreachable, and deleting them is worthwhile. The reliable signal is a closed loop rather than a single report: the resolver understands your aliases, a text search across the whole repository and any published contract finds nothing, and the build plus an integration run stay green after the deletion. If you can meet all three, the export is dead and the removal is safe. If any one of them is uncertain, the correct move is to keep the symbol or mark it deprecated for a release cycle instead of deleting it in the same pass.
FAQ
Why did the build pass if a real consumer was broken?
Because the build only proves the files that remain are internally consistent. A dynamic import that resolves to a missing chunk or a removed export is not always detectable at compile time, especially when the specifier is a runtime string. The type check and unit tests can be fully green while a lazy-loaded path fails only when a user reaches it.
Should I stop using path aliases to make pruning reliable?
No. Aliases are valuable for keeping imports stable as files move. The fix is to configure the pruning tool to read the same path mapping as the compiler, not to abandon aliases. Verify the tool resolves one known aliased import correctly before trusting any of its "unused" results.
How do I protect a public package export from an internal pruning pass?
Treat the package's entry point as a public contract and exclude its entry exports from automatic deletion, or drive pruning from an explicit API surface such as package.json exports. Internal unreachability does not mean external consumers do not depend on the symbol.
Is deprecation better than immediate deletion?
For uncertain cases, yes. Marking the export deprecated with a visible warning keeps existing consumers working while you gather real usage data over a release or two. Once telemetry and text search both confirm zero callers, deletion becomes a safe, evidence-based follow-up rather than a guess.
This is part of the The AI Coding Agent Guide — start there for the full picture of how agents break code after the repository appears to be healthy.