Skip to main content

Command Palette

Search for a command to run...

Your agent keeps deprecated aliases alive forever. Make it pick a cutover date.

AI agents add deprecated aliases 'for backwards compatibility' because breaking changes feel risky in training data. Without a cutover date, those aliases become permanent debt nobody will ever delete. The fix: force the removal PR in the same ticket.

Updated
•4 min read•View as Markdown

The refactor PR looks clean. getUserById becomes fetchUserById, the body moves to a new service file, and the tests pass. At the bottom of the diff there's one extra block:

// @deprecated Use fetchUserById instead. Kept for backwards compatibility.
export const getUserById = fetchUserById;

The agent did this on its own. Nobody asked it to. Six months later, that alias is still there, half the codebase still calls the old name, and nobody can remember whether anything outside the repo depends on it.

Why agents add the shim

Training data is full of "don't break the public API" advice. Senior engineers on open-source projects write deprecation headers, semver release notes, and migration guides. When an agent sees a rename, it pattern-matches "this is a breaking change" and the safe move is to keep both names available. It would rather ship a harmless alias than risk a caller it can't see.

The problem is that most code isn't a public library. It's an internal service with two other repos in the same org. There is no external caller who needs a deprecation window. The agent doesn't know that, because nobody told it the boundary between internal and external.

The alias that never gets deleted

Deprecated aliases have a half-life of forever in practice. Here's what happens:

The alias creates two names for one thing. Every search, every grep, every on-call debug now has to check both call sites. Lint rules that should flag the old name get disabled because "it's deprecated but still works."

Nobody measures whether the old name is still used. The alias exists "just in case," and without telemetry or a scheduled deletion, "just in case" becomes the permanent state. The agent that wrote it has no memory of the decision three months later.

Callers keep using the name they know. New contributors grep for getUserById, find it, and call it. The alias is self-sustaining: the more it's used, the more it feels like it needs to stay.

The two-step rule

I started requiring that any rename include the cutover, not just the alias:

  1. Grep the whole monorepo (and known dependents) for the old name in the same PR. If there are zero external callers after the rename, delete the old name entirely. No alias, no deprecation header.
  2. If there genuinely are external callers, the PR must include a date. "Remove getUserById in release 2.4" — not "kept for backwards compatibility." The ticket to delete it gets filed in the same PR review, so the work doesn't disappear.

When an agent proposes an alias, I paste this back: "Is this a public package or an internal service? If internal, grep and delete. If public, name the release version." About 80% of the time it comes back with a deletion instead of a shim.

The API version of the same problem

Endpoint deprecation is the same failure mode, writ larger. An agent that sees an old endpoint in your OpenAPI spec will add deprecated: true and leave it there, because removing it feels like a breaking change. The useful question isn't "should we mark it deprecated?" — it's "when do we stop routing traffic to it, and how do we know when the last caller moved?"

In local-first API work, that measurement has to happen against the live traffic, not just the spec. We ended up building that check into Powerduck — flag the deprecated endpoint, re-run it against the running service, and confirm zero non-2xx responses before the removal PR. The spec stops being the only source of truth about whether anything still calls it.

The exception

Public npm packages, SDKs, and APIs with real external users do need deprecation windows. That's what semver and release notes are for. The test is the same: if you can't name the caller who needs the window, you don't need the alias. Internal services almost never have that caller, and agents keep inventing one anyway.

The pattern isn't "never rename." It's "don't let the agent rename and hedge at the same time without a deletion plan." A shim without a date isn't safety. It's a todo that will never be done.