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
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 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.
| Technique | What moves | What must stay frozen |
|---|---|---|
| split-large-component | A 500+ line file becomes 2-3 sibling files | Default export name; rendered DOM structure |
| extract-hook | A useState + useEffect block leaves | Component re-render count; effect run order |
| extract-utility | Pure function leaves the component file | Function signature and return shape |
| improve-naming | Variable / function identifiers rename | Exported identifiers; serialized JSON shapes |
| remove-duplication | Two near-identical blocks merge | Branch behaviour for both original call sites |
| class-to-function | A class component becomes a function one | Public ref API; state and lifecycle ordering |
| js-to-ts | A .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.