How I use Claude Code to refactor legacy code safely — a complete workflow
Legacy code is the elephant in every engineering team's room. It works — mostly — but nobody wants to touch it. The tests are sparse, the documentation is nonexistent, and the original author left three years ago.
I've been using Claude Code to tackle legacy refactoring systematically, and it's changed how I approach these projects. Here's the exact workflow I use.
The problem with legacy refactoring
The risk with legacy code isn't the refactoring itself — it's the unknown dependencies. Change one function and something breaks three modules away. The classic pattern:
- Identify the code that needs improving
- Make the change
- Break something unexpected
- Scramble to understand why
- Revert and start over
Claude Code helps break this cycle by building context before touching anything.
Phase 1: Understand before you change
Before writing a single line of new code, I ask Claude Code to map the territory:
Read these files and tell me:
1. What does this module actually do?
2. What calls it (find all usages across the codebase)?
3. What does it call?
4. What would break if I changed the function signature of X?
Files: src/legacy/payments.js, src/legacy/invoicing.js
This step alone saves hours. Claude Code will trace the dependency graph and flag hidden coupling you didn't know existed.
Phase 2: Write characterization tests first
Legacy code without tests is a minefield. Before refactoring, I use Claude Code to generate characterization tests — tests that capture what the code currently does, not what it should do:
Write characterization tests for payments.js.
Don't worry about whether the behavior is correct —
capture exactly what the code does today so we can
detect regressions after refactoring.
Use Jest. Cover the main code paths.
These tests become your safety net. If they pass after refactoring, you haven't broken existing behavior.
Phase 3: Incremental refactoring with explicit constraints
This is where most people go wrong — they try to refactor everything at once. I give Claude Code explicit constraints:
Refactor payments.js with these constraints:
1. Do NOT change any public function signatures
2. Do NOT change the module's exports
3. Do NOT add new dependencies
4. Focus only on: extracting helper functions, reducing nesting, adding JSDoc comments
5. After each change, confirm the characterization tests still pass
The explicit constraints prevent Claude Code from over-engineering the solution. It's tempting to modernize everything in one shot — that's how you end up with a 3-week refactoring project instead of a 3-hour one.
Phase 4: Validate at each step
I never let Claude Code make more than 2-3 file changes before validating:
You've changed payments.js and payments.utils.js.
Before continuing:
1. Run the characterization tests
2. Run the full test suite
3. Show me the diff summary
4. Confirm no public interfaces changed
This creates natural checkpoints. If something breaks, you know exactly which change caused it.
Phase 5: Document as you go
Legacy code stays legacy because nobody documents the changes. I use Claude Code to generate documentation while the context is fresh:
For each function you refactored in this session:
1. Add a JSDoc comment explaining what it does
2. Note any non-obvious behavior or edge cases
3. Add a @since comment with today's date
This is the documentation that will save the next engineer who has to touch this code.
The rate limit reality
Legacy refactoring sessions are long. You're loading multiple files, running test suites, iterating on complex changes. These sessions routinely hit Claude Code's rate limits.
When that happens mid-refactoring, the context window that Claude Code built — all those dependency mappings, the understanding of the codebase — is gone.
I've started using SimplyLouie as my ANTHROPIC_BASE_URL for exactly this reason. It's $2/month and acts as a pass-through to the Anthropic API with built-in rate limit handling. When I configure it once:
export ANTHROPIC_BASE_URL=https://simplylouie.com/api
export ANTHROPIC_API_KEY=your-key
My Claude Code sessions stop getting interrupted mid-refactoring. The context stays intact. The refactoring session completes.
A real example
Here's a condensed version of a recent session on a 5-year-old Express.js codebase:
Session start prompt:
I need to refactor the authentication middleware in src/middleware/auth.js.
This file was written in 2019 and uses callbacks. I want to:
1. Convert to async/await
2. Improve error handling
3. Add TypeScript types (we're mid-migration)
Before touching anything:
- Map all routes that use this middleware
- Identify any behavior we need to preserve exactly
- Write characterization tests
What Claude Code did:
- Found 23 routes using the middleware across 8 route files
- Identified 3 edge cases in error handling that weren't obvious from the code
- Generated 11 characterization tests
- Proposed the refactoring plan with explicit steps
Total session: ~90 minutes. Zero regressions. The characterization tests all passed on the first attempt.
Key principles
- Understand before changing — map the dependency graph first
- Characterization tests before refactoring — capture current behavior as your safety net
- Explicit constraints — tell Claude Code what NOT to do
- Small increments — 2-3 file changes max before validating
- Document while context is fresh — don't skip this step
Legacy code doesn't have to be untouchable. With Claude Code and the right workflow, it becomes just another engineering problem — one with a clear process.
What's your approach to legacy refactoring? I'd love to hear what's worked (and what hasn't) in the comments.
Top comments (0)