← Back to BlogEngineering

How to Refactor With AI Without Breaking Things

A practical guide to refactoring with an AI assistant: pin behavior with tests first, take small reversible steps, keep scope tight, and review every diff yourself.

Refactoring is the moment an AI coding assistant is most likely to hurt you, because it works on code that already runs correctly. A bug in brand-new code gets caught by the simple fact that nothing worked before either, so any failure is visible immediately. A bug introduced during a refactor often ships silently instead: the tests that exist, if any, still pass, the code still compiles, and the change reads like a clean rename or a tidy extraction. Weeks later, an edge case that used to work quietly stops working, and no one can point to the exact commit that broke it, because the diff that broke it looked identical to a dozen other diffs that did not.

The fix is not to refactor less with an AI assistant. It is to refactor with a safety net that catches a behavior change before it ships, not after. That safety net comes down to four habits: pin the current behavior with tests before anything changes, move in small reversible steps, keep the scope of each change tight enough that a behavior change is unmistakably a bug rather than a bonus, and read the diff yourself instead of trusting a summary of it. None of these habits are specific to AI. They are what careful refactoring has always required. AI assistants just make it far easier to skip them, and far more expensive when you do.

Why AI Makes Refactoring Riskier, Not Safer

An AI assistant reading a function has no memory of why it was written that way. It cannot see the incident that led to a defensive null check, the customer workflow that depends on an undocumented ordering of two calls, or the reason a seemingly redundant retry loop is not actually redundant. Ask it to clean up, simplify, or modernize a piece of code, and it will remove anything that looks unnecessary from the code alone, because the code is all it can see. It also presents a full rewrite of a module with the same even, confident tone it uses for a one-line fix. Nothing in that presentation signals how much surface area changed, or how much of that surface area was ever exercised by a test. The output reads as clean, plausible, and finished, which is exactly the presentation that makes a bad change hardest to catch before it merges.

Characterize Behavior With Tests First

Before an assistant touches working code, you need a mechanical way to know whether its behavior changed. That is what characterization tests are for: tests that record what the code actually does today, not what it should do in some ideal version of the design. If a function returns a slightly wrong value for a particular input right now, the characterization test asserts that value as-is; fixing it is a separate, deliberate change, made later, not a side effect of a refactor you never asked for. A useful characterization test covers normal-path inputs and their exact outputs, edge cases like empty or boundary values, what gets thrown or logged on failure, and any ordering or side effects the code depends on, such as what gets written and in what sequence. If tests already cover the code you are about to change, confirm they are green and treat that as your baseline; if they do not exist, write them before you write a single refactoring prompt. On a refactor that spans multiple sessions, this matters even more, because model context resets between conversations: the tests end up pinning behavior while a private, persistent memory layer like MemX can help carry forward notes on why a section was written a certain way, though the tests remain the part that actually catches a regression.

Pro Tip

Run the whole test suite once before writing any refactoring prompt, and keep that run as your baseline. If a test fails after the change and you cannot tell whether it was already flaky beforehand, the baseline is gone and the refactor is not safe to keep going.

Small Steps, Tight Scope

Ask an assistant to clean up an entire module in one prompt, and you invite a large, entangled diff: renamed variables mixed with restructured control flow mixed with a genuinely different algorithm, all in a single commit. When that combined change breaks something, isolating the cause means re-deriving what the assistant actually did, one hunk at a time, after the fact. Ask for one mechanical transformation per step instead (extract this function, inline that variable, rename this identifier, replace this loop with that call), run the tests after each step, and commit each green step on its own. If a step turns the suite red, you know exactly which transformation caused it, and reverting it costs almost nothing. Tight scope matters just as much as step size: ask for an extraction and an assistant may also rename unrelated variables, add a new dependency, change an exception type, or apply an optimization nobody requested, and each of those is a separate decision that deserves its own review. Consider replacing a hand-written data-access layer with a query builder: a single big-bang prompt can quietly change the order two queries run in, breaking a caller that depended on the first one finishing before the second started, with nothing in the diff calling that out. An incremental version catches the same mistake in seconds, because the ordering was pinned by a test before the rewrite began and only one query changed at a time. A spec audit done before any code is generated is one way to make the scope explicit up front, so a refactor that quietly grows into a rewrite is a deviation from a stated plan rather than a judgment call made in the middle of a diff.

Insight

A refactor that changes behavior is not a refactor. It is an untested rewrite wearing the label of a refactor, and it deserves the same review, tests, and sign-off that any behavior change deserves.

Review the Diff Independently

An assistant's own description of what it changed is not a substitute for reading the diff. A summary can claim it only extracted validation logic into a helper function while the actual diff also swaps a strict equality check for a loose one, or drops a guard clause that used to short-circuit on an edge case. Summaries describe intent; diffs describe what happened, and only the diff is binding. Read every changed line, not just the ones the summary calls out, and where possible have a second reviewer, human or a fresh model context with no stake in having written the change, look at it independently before it merges. Treat an AI-authored diff the way you would treat a pull request from someone you have never worked with before: verify it, do not just accept it. An AI code review checklist is a reasonable starting point, and the same discipline is why a convergence gate that blocks shipping until issues hit zero exists in the first place: independent review is only useful if something actually enforces it.

DimensionRisky AI refactorSafe AI refactor
Starting pointAssumes existing tests are enough, without checkingBehavior pinned with characterization tests first
Step sizeOne large prompt rewrites the whole moduleOne mechanical transformation per prompt
ScopeStyle, structure, and behavior change togetherBehavior changes explicitly excluded and flagged
VerificationTests run once at the end, if at allTests run after every step, before the next one
ReviewAssistant summary accepted as accurateFull diff read independently, line by line
RollbackOne commit holds everything; reverting undoes unrelated fixes tooEach step is its own commit; reverting is surgical

Frequently asked questions

01Do you need full test coverage before refactoring with AI?

No. You need coverage of the paths and edge cases the refactor actually touches. Characterization tests for the specific function or module you are about to change are enough to catch a regression there; chasing full-codebase coverage first is a good long-term goal, not a prerequisite for a safe, scoped refactor.

02What if the code has no tests at all?

Write characterization tests before you write a refactoring prompt. Capture what the code actually does now, including behavior you know is wrong, and use that as your baseline. Fix the wrong behavior afterward, as its own labeled change, so a test failure during the refactor always means the refactor broke something rather than fixed something.

03How small should one AI-assisted refactor step be?

Small enough that you can describe it in a single sentence and confirm it is safe with a test run that takes a couple of minutes, not longer. If you cannot summarize a diff that way, split it. A step sized correctly is also easy to revert on its own, without touching anything else committed alongside it.

Ship AI-written code you can trust

TLM Forge is the missing process layer for Claude Code: a spec audit, independent multi-agent review, enforced TDD, and an adversarial red-team gate.

Get TLM Forge