Refactoring is changing the structure of code without changing what it does. Martin Fowler’s definition comes as a noun and a verb. A refactoring is one change to a program’s internals that makes it easier to read and cheaper to change. To refactor is to make a series of them, with the program behaving exactly as before after each one. His book, Refactoring: Improving the Design of Existing Code, came out in 1999. The second edition followed in 2018.

A coding agent doesn’t change the method, but it makes the size of each step matter more. Pin the behaviour with tests before anything moves. Ask the agent for one named refactoring at a time. After every step, run your tests, type checker and linter, and commit only what passes.

In a 2026 study of language models refactoring whole files in one go, the best setup, Claude Code, still broke about one file in twenty. The rest of this article covers what refactoring is, when to do it, the steps with an agent and a worked example in five languages.

What refactoring means

Observable behaviour is what a caller or a user can see: return values, output, errors and side effects. If any of those changes, the change isn’t a refactoring, however small it is.

Refactoring Behaviour change
Rename a variable so it says what it holds Fix a bug
Extract part of a long function into its own Add a case to a conditional
Replace nested conditionals with guard clauses Reword an error message a user sees
Move a function to the module that calls it most often Change a default value

Kent Beck coined the term code smell, a quick-to-spot sign that usually points to a deeper problem, when he was helping Fowler with the book. Fowler’s Code Smell (2006) records it.

Fowler also passes on Beck’s two hats in the book and explains them in An example of preparatory refactoring (2015). Programming has two modes, adding function and refactoring. You work in one at a time. When you’re refactoring, every change keeps the tests passing. Adding function means writing new tests and sometimes breaking old ones.

For an agent, that means each request is a refactoring or a feature. A request that mixes them puts both kinds of change in one diff. When a test then fails, it can’t tell you which one caused it.

When to refactor code

Fowler’s Opportunistic Refactoring (2011) says to refactor whenever you see code that isn’t as clear as it should be, and only while the tests are passing. He describes two moments in particular:

  • Before a change, restructure the code so the feature you’re about to add is easy to add. Fowler calls this preparatory refactoring, and credits Kent Beck with the shortest summary of it.
  • During or after a change, clean up the duplication or the awkward interface you’ve just met before you call the work done.

P6 Leave it Better Than You Found it covers how much to tidy and where to stop. If the code came from an agent in the first place, cleaning up a vibe-coded app covers a whole codebase written that way.

Code with no tests isn’t ready to refactor, whoever does the refactoring. Write the tests first, as the steps below show.

AI code refactoring: what the research found

In Code for Machines, Not Just Humans (2026), Markus Borg, Nadim Hagatulah, Adam Tornhill and Emma Söderberg asked language models to make code more maintainable.

They used 5,000 Python files from programming competitions, then ran each problem’s tests and counted a file as broken if any test failed. They split the files by CodeHealth, CodeScene’s maintainability score from 1 to 10, and counted 9 and above as healthy.

Setup Tests passed, healthy code Tests passed, unhealthy code
Claude Code 2.0.13, with Claude Sonnet 4.5 96.2% 94.8%
Claude Sonnet 4.5, single prompt 86.8% 84.0%
Qwen3-Coder-30B-A3B, single prompt 80.7% 72.2%
Granite-4.0-H-Small, single prompt 46.5% 37.2%

In all, the paper tested five open-weight models. Each broke significantly more unhealthy files than healthy ones. Sonnet and Claude Code showed no significant difference between healthy and unhealthy code. The agent did far better than any single prompt, yet it still broke about one file in twenty, on files that each stood on their own. The researchers had also turned off its Bash tool, so it never ran a test before handing the code back.

Two things in that setup are in your hands. Each request covered a whole file, which is a big step, and nothing checked the result until the researchers ran the tests afterwards.

Checking every edit has been measured too. In SWE-agent (2024), John Yang and colleagues gave their agent an edit command that ran a linter after each edit. Any edit that introduced an error such as bad syntax or an undefined name was turned back. With GPT-4 Turbo on SWE-bench Lite, the agent resolved 18.0% of tasks with that check and 15.0% without it. Those were GitHub issues to resolve, so the paper says nothing about refactoring as such, but it does show a check after each edit raising the success rate.

How to refactor code with a coding agent

  1. Check that tests cover the behaviour you’re about to change, through its public surface, as TS6 Behaviour Testing asks. If they don’t, write them and watch them pass against the code as it is. Michael Feathers calls tests like these characterisation tests, in Working Effectively with Legacy Code (2004).
  2. Commit, so the last good state is one command away.
  3. Name one smell to fix. Code smells covers the common ones.
  4. Ask for one refactoring by its name, and say that the behaviour must stay the same and no test may change.
  5. Run the tests, type checker and linter that make up the gate.
  6. If everything passes, commit the step on its own. If anything fails, revert to the last commit and ask for a smaller step.
  7. Go back to step 3, or stop when the code reads well.
Tests pass on a clean commit Name one smell One refactoring asked for by name Run the gate tests, types and lint fail Revert smaller step pass Commit the step on its own
Each refactoring is one step between two passing runs of the gate. A step that fails is reverted.

Only the commands and their exit codes count as the gate. Ask the agent to run them and show you the output, or run them yourself. When a step fails, revert it. A fix on top of a broken refactoring puts two changes in one diff. From then on, the tests can’t show which one broke things.

If a refactoring seems to need a test changed, stop. Either the behaviour changed, or the test was checking internals. TS6 rules out tests like that. For new code, P1 Test-First Development keeps the same order, with the failing test written before the code.

Claude Code can also run the gate for you. Its hooks guide describes hooks as shell commands that run at set points in a session, whatever the model decides. A PostToolUse hook runs after each successful edit. According to the hooks reference, if it exits with code 2, Claude sees its error output, though the edit has already been made.

All three Claude Code pages were checked on 12 October 2026. Pre-commit hooks covers checks that run before each commit.

Refactoring techniques to ask for by name

A named refactoring is a small change with known mechanics, so you can check the diff against them. Fowler’s online catalogue shows each one with a before and an after. Extract Function is there with Extract Method as its alias, the name it had in the first edition.

When you see Ask for Tenet
A function doing two jobs Extract Function, cut where the jobs part F1 Single Responsibility Principle
A helper whose name says no more than its body Inline Function S5 Inline-First
Preconditions nested around the main path Replace Nested Conditional with Guard Clauses S4 Guard Clauses
More than three parameters Introduce Parameter Object S3 Parameter Object
A name that doesn’t say what it holds Rename Variable F2 Intention-Revealing Names

If a smell needs several refactorings, ask for them one at a time, with the gate between each.

A worked example with guard clauses

Here’s a function from a hedgehog rescue’s records. It sits inside the Prickles packs’ limits, with a cyclomatic complexity of 3 and a ternary nested inside an if. Even so, the main path, the note for a hedgehog that’s ready to go, sits two levels deep.

Viewing: TypeScript.
export type Hedgehog = Readonly<{
  name: string;
  isFeeding: boolean;
  isVetCleared: boolean;
}>;

export const releaseNote = (hedgehog: Hedgehog): string => {
  if (hedgehog.isFeeding) {
    return hedgehog.isVetCleared
      ? `${hedgehog.name} is ready for release.`
      : `${hedgehog.name} is waiting for the vet.`;
  }
  return `${hedgehog.name} isn't feeding yet.`;
};

Before the agent touches it, pin what it does. The tests only ask for the note, so they don’t depend on how it’s worked out inside.

Viewing: TypeScript.
import assert from "node:assert/strict";
import { test } from "node:test";
import { releaseNote, type Hedgehog } from "./release-note";

const bramble: Hedgehog = { name: "Bramble", isFeeding: true, isVetCleared: true };

test("a feeding hedgehog the vet has cleared is ready for release", () => {
  assert.equal(releaseNote(bramble), "Bramble is ready for release.");
});

test("a feeding hedgehog waits for the vet", () => {
  assert.equal(releaseNote({ ...bramble, isVetCleared: false }), "Bramble is waiting for the vet.");
});

test("a hedgehog that isn't feeding stays in care", () => {
  assert.equal(releaseNote({ ...bramble, isFeeding: false }), "Bramble isn't feeding yet.");
});

test("a hedgehog neither feeding nor cleared is reported as not feeding", () => {
  assert.equal(
    releaseNote({ ...bramble, isFeeding: false, isVetCleared: false }),
    "Bramble isn't feeding yet."
  );
});

Between them, the first three tests cover the three notes. A fourth covers a hedgehog that’s neither feeding nor cleared, where the order of the two checks decides which note comes back. Then comes the request, naming the refactoring from Fowler’s catalogue:

Refactor releaseNote using Replace Nested Conditional with Guard Clauses.
Keep its behaviour exactly the same, and don't change any test.
Then run the tests, the type checker and the linter, and show me the output.

Replace Nested Conditional with Guard Clauses turns each precondition into an early return, the shape S4 Guard Clauses asks for. The type stays as it was, so only the function changes:

Viewing: TypeScript.
export const releaseNote = (hedgehog: Hedgehog): string => {
  if (!hedgehog.isFeeding) {
    return `${hedgehog.name} isn't feeding yet.`;
  }
  return hedgehog.isVetCleared
    ? `${hedgehog.name} is ready for release.`
    : `${hedgehog.name} is waiting for the vet.`;
};

Both versions pass every test. A version with the two checks the other way round passes the first three and fails the fourth. It tells a hedgehog that’s neither feeding nor cleared to wait for the vet. The first three tests alone would have let that through, so the edge case is pinned before the agent is asked for anything.

Where to split a long function

The Prickles packs cap cyclomatic complexity at 3 and function length at 15 lines, as S2 Cyclomatic Caps sets out. Caps like these push code into smaller functions. An agent asked to get under them has to cut somewhere.

Where it cuts matters. In Does Code Cleanliness Affect Coding Agents? (2026), Priyansh Trivedi and Olivier Schmitt of Sonar built pairs of repositories that match in architecture and behaviour. The two sides of each pair differ only in static-analysis findings and cognitive complexity.

Over 660 runs of Claude Code with Claude Sonnet 4.6, agents on the cleaner side used 7 to 8% fewer tokens. They also went back to files they’d already opened 34% less often, with no change in the pass rate. The study used one model, the authors built every pair themselves and they report no significance tests.

One track of the study went the other way. On code with complexity hotspots, the cleaner versions had their logic extracted into more, smaller methods across more files. There the agent opened 11.2% more files to reach the same pass rate. Its input tokens were 1.8% higher, essentially unchanged in the authors’ view.

extraction redistributes complexity across more methods rather than eliminating it

— Priyansh Trivedi and Olivier Schmitt, Does Code Cleanliness Affect Coding Agents?, 2026

A line limit tells you a function may be doing too much. It can’t tell you where the seam is. F1 Single Responsibility Principle can, by giving each piece one reason to change.

When the linter flags a long function, ask the agent to extract a named responsibility, such as parsing a request or wording a message. Then check that each new function has a reason to change of its own. If a new helper does no more than rename the line inside it, ask for Inline Function on the next step, as S5 Inline-First asks.