claude-code lastVerified: 2026-05-08

Claude Code Refactor Prompt — Behavior-Invariant Template

A Claude Code refactor prompt that locks behavior. No new features, no API changes, existing tests preserved. For Vite + React + TypeScript codebases.

On this page
  1. Why refactor prompts go wrong by default
  2. The prompt
  3. Why behaviour invariance is the binding criterion
  4. Variants by refactor technique
  5. Where this prompt fits in the loop
  6. Common pitfalls when Claude Code refactors

Why refactor prompts go wrong by default

A refactor is the one task where doing nothing visible is the correct outcome. The public surface of the module looks identical, the existing test suite stays green, the bundle output diffs by a handful of bytes. Claude Code, trained to be helpful, will instead “improve” the code while restructuring it — adding error handling that was not there, renaming exports because the new name reads better, sneaking in a missing memoisation. Each improvement is defensible in isolation; together they make a refactor commit unreviewable. This prompt locks behaviour by elevating “no behaviour change” from a suggestion to the binding acceptance criterion. Use it when you are restructuring code you already trust and want to keep trusting after the diff lands.

The prompt

refactor-prompt.md
# Refactor goal
Restructure <module or file path> using the technique below. The public
exports, function signatures, and runtime behaviour MUST remain identical
before and after this change. Existing tests are the source of truth: if a
test fails, the refactor is wrong, not the test.

# Refactor technique

<one of: split-large-component | extract-hook | extract-utility |
improve-naming | remove-duplication | class-to-function | js-to-ts>

# Hard invariants

- Public exports unchanged (same names, same signatures, same defaults)
- No new runtime dependencies (do not run pnpm add)
- No new feature behaviour added during the move
- No error handling, logging, or memoisation added "while we are here"
- Existing tests remain unchanged or change only in import paths

# Verification

- `pnpm test --run` passes with zero modifications to assertions
- `pnpm exec tsc --noEmit` passes
- `pnpm build` exits 0 and the gzipped bundle diff is under 2 KB
- Manual reproduction of one user-visible code path works identically

# Output format

1. The technique you applied and why it preserves behaviour.
2. The diffs grouped by file.
3. The exact verification commands and their truncated output.
4. A line-by-line confirmation that no public export changed.

Why behaviour invariance is the binding criterion

Refactors fall into two camps: structural and semantic. Structural refactors move code around — a 600-line component becomes three 200-line components, a tangled hook becomes two named hooks, a utility leaves a component file for src/lib/. Semantic refactors change what the code does — adding a cache, normalising an error format, debouncing a side effect. This prompt is for structural work only. Mixing the two in a single commit makes review impossible because the reviewer cannot tell which line of the diff is the move and which is the new behaviour. The hard-invariants block forces the agent to keep the two camps separate; if Claude Code spots a semantic improvement, it should mention it in the output paragraph and stop, not silently land it.

Variants by refactor technique

Each technique has its own preservation pattern — what stays the same, what moves, how to verify nothing leaked.

TechniqueWhat movesWhat must stay frozen
split-large-componentA 500+ line file becomes 2-3 sibling filesDefault export name; rendered DOM structure
extract-hookA useState + useEffect block leavesComponent re-render count; effect run order
extract-utilityPure function leaves the component fileFunction signature and return shape
improve-namingVariable / function identifiers renameExported identifiers; serialized JSON shapes
remove-duplicationTwo near-identical blocks mergeBranch behaviour for both original call sites
class-to-functionA class component becomes a function onePublic ref API; state and lifecycle ordering
js-to-tsA .js file becomes .ts (or .tsx)Runtime behaviour; export names; default args

For class-to-function, ask Claude Code to demonstrate the equivalent useImperativeHandle mapping if a parent grabs a ref. For js-to-ts, the discipline is “type the existing shapes, do not narrow them” — narrowing turns a refactor into a behaviour change because calls that previously accepted string | number now reject the number.

Where this prompt fits in the loop

A safe refactor follows a green test suite and precedes a feature. If the tests are red, fix them with the bug fix prompt first. If you want new behaviour, ship it with the feature prompt before or after the refactor — never inside it. If you want missing tests written before the refactor so the safety net exists, use the test generation prompt. After the refactor lands, ask for a final pass with the code review prompt — that pairing is where teams catch the “improvements” the refactor was supposed to forbid.

Common pitfalls when Claude Code refactors

The agent’s three favourite escape hatches are: adding a try / catch around the moved code “since we are touching it” — counter with the explicit no-error-handling invariant. Renaming an export “for clarity” — counter with the public-exports-unchanged invariant. Tightening a TypeScript signature (“this should be string not string | undefined”) — counter by stating that signatures are frozen until a separate, dedicated commit. Each of these reads as a small win and aggregates into a refactor that no one can review. Establish the constraints in CLAUDE.md too, so the agent encounters them at the start of every conversation, not only when the refactor prompt fires.