When you've been running a personal app for a few years, there comes a day when you open a file and realize: I can't read my own code anymore. Comment-outs layered on top of each other, functions called from somewhere unknown, logic duplicated in five places with subtle variations. If that sounds familiar, this post is for you.
That was exactly my situation. Running two apps simultaneously while continuously adding features had left my codebase in what I call the "works but untouchable" state — functional, but too fragile to refactor safely.
So at the start of 2026, I committed three months to seriously tackling technical debt using Claude Code. This is the record of what I did, in what order, what worked, and where Claude hit its limits.
Step One: Build a Debt Map — Put Numbers on the Problem
The most common reason refactoring stalls before it starts is the question "where do I even begin?" Without a concrete map, you work on whatever annoys you most rather than what matters most.
The first thing I did was ask Claude Code to generate a technical debt inventory. Running this from the project root:
claude "Analyze this entire repository and produce a Markdown report covering:
1. All TODO/HACK/FIXME/XXX comments with file locations
2. Duplicate code patterns (50+ lines of similar logic)
3. High-complexity functions (5+ levels of nesting or conditionals)
4. Circular module dependencies
5. Files with zero test coverage (if test config exists)
For each item, add a 1-5 star rating on two axes: 'blast radius if it breaks' and 'difficulty to fix'."Claude spent a few minutes scanning the whole repo and produced a surprisingly specific report. My numbers:
- TODO/HACK comments: 47 instances (23 of them over two years old)
- Obvious duplicate code: 8 patterns (~1,200 lines)
- Circular dependencies: 3 module pairs
- Files with zero test coverage: 67% of the codebase
Something shifts when you move from "my code is kind of a mess" to "I have 3 circular dependencies and 8 duplicate patterns." The problem becomes bounded, approachable. That map made it possible to prioritize without getting overwhelmed.
Prioritization: The Urgency × Impact Matrix
With a map in hand, the next challenge is deciding what to tackle first. My mistake in previous attempts was starting with whatever looked the ugliest. Aesthetically offensive code isn't always the most dangerous code.
The key to getting useful prioritization from Claude is including your real-world constraints:
claude "Using the debt report we just created, help me prioritize the work.
Constraints:
- I have 1-2 days per week available for refactoring
- I cannot break live functionality (this is a production app)
- I'm planning to add Feature X in three months, and its surrounding modules need to be clean first
Given these constraints, please categorize items into: 'this week', 'this month', and 'within three months'."The output gave me something concrete: tackle the 8 duplicate code patterns first (this week and next week), skip the circular dependency resolution until month two because the blast radius is too large to handle quickly.
Crucially, telling Claude about the upcoming Feature X caused it to flag which debt items lived in that feature's neighborhood — and those jumped to the front of the queue.
Removing Duplicate Code: Safe Refactoring with Subagents
The highest-priority items were duplicate error handling patterns scattered across 8 API files. Each file had its own slightly different version of the same try/catch logic.
This is where Claude Code's subagent capability with --headless and git worktrees becomes genuinely powerful. You can experiment without touching your main branch at all:
# Create an isolated working environment
git worktree add /tmp/refactor-api-errors -b refactor/api-error-handling
claude --headless "
Working directory: /tmp/refactor-api-errors
Task: Consolidate the duplicated error handling logic found in these 8 files:
src/api/users.ts, src/api/products.ts, src/api/orders.ts, ... (etc.)
Steps:
1. Analyze the error handling patterns across all 8 files
2. Identify what can be shared vs. what must stay per-file
3. Create src/utils/apiErrorHandler.ts as a shared utility
4. Refactor all 8 files to use the new module
5. Verify no TypeScript errors remain
6. Output a final diff of all changes
"The result: 8 patterns collapsed into one shared utility plus thin per-file wrappers. ~340 lines became ~95 lines.
Important caveat: I never merged the output directly. I always reviewed the diff, checked that type definitions were accurate and error messages hadn't changed, then decided whether to merge. The worktree approach means your main branch is never at risk while you're exploring.
Resolving Circular Dependencies: Understand Before You Fix
Circular dependencies took nearly a month — the longest stretch. That's because they're not really a code problem; they're a design problem. Module A importing module B which imports module A means the original separation of concerns was unclear.
Asking Claude to "just fix it" here would have been a mistake. Resolving circular dependencies requires redesigning module responsibilities — a judgment call that belongs to the developer, not the AI.
Instead, I asked Claude to analyze why the dependency existed:
claude "There's a circular dependency between src/store/userStore.ts and src/components/UserProfile.tsx.
Analyze why this pattern emerged, explain the design flaw, and suggest three approaches to resolve it.
Include trade-offs for each approach."The three options it produced (event bus, context separation, dependency injection) gave me the vocabulary to make an informed decision. I chose, then asked Claude to implement. That "analyze → decide → implement" flow is the right one for structural problems.
Adding Tests to Untested Code: The Incremental Approach
With 67% of files having zero test coverage, trying to test everything at once is a trap. I used a narrowly scoped approach:
claude "Write tests for src/utils/priceCalculator.ts.
Don't try to cover everything — instead, identify the 3 functions in this file where a bug would most impact users, and write focused tests for those 3 functions using vitest.""The 3 most impactful functions" rather than "100% coverage" was the prompt that made this sustainable. Over three months, coverage went from 0% to 38%. Not perfect — but the core user-facing paths are now protected.
Three Months Later: The Numbers
At the end of month three, I ran the same analysis that generated the original debt map. Results:
| Metric | Start | Month 3 | Change |
|---|---|---|---|
| TODO/HACK comments | 47 | 19 | −60% |
| Duplicate code patterns | 8 | 2 | −75% |
| Circular dependencies | 3 | 0 | −100% |
| Test coverage | 0% | 38% | +38pt |
| Maximum function complexity | 18 | 7 | −61% |
The numbers look good. But the most meaningful change wasn't quantitative — it was that adding new features stopped feeling scary. The "works but untouchable" state became "imperfect but workable."
What I Learned About Claude Code and Technical Debt
A few things that shaped my approach, worth keeping in mind:
Don't ask Claude to "just fix everything." Debt reduction requires design decisions. Claude is excellent at generating options, implementing a chosen approach, and writing tests — but which approach to take is yours to decide.
The worktree pattern is your safety net. Running subagents in isolated worktrees means zero risk to your production code while you experiment. This alone makes Claude Code worth using for refactoring work.
Run the health check weekly. Re-running the same debt analysis once a week takes minutes and tells you immediately if you're sliding backward. I've since automated this via a Claude Code hook.
Done is better than perfect. Technical debt never reaches zero. The goal is moving from "works but untouchable" to "touchable and improving." Once you reach that state, the velocity gains compound on their own.
The next step is automating ongoing quality monitoring — a hook that runs on every PR, flags any metrics that are worsening, and leaves a comment. Building that system is the logical continuation of this three-month effort.