Refactoring a large codebase with an AI coding assistant sounds straightforward until it isn’t. Claude Code’s Plan Mode addresses these failures directly by enforcing a read-first workflow that separates the reasoning phase from the execution phase.
Table of Contents
Why Complex Refactors Break AI Assistants
Refactoring a large codebase with an AI coding assistant sounds straightforward until it isn’t. Developers know the failure modes well: partial understanding of dependency chains leads to hallucinated imports, the assistant edits files in isolation without regard for downstream consumers, and it duplicates or orphans shared utilities. The result is a frustrating cycle of undo, redo, and reprompt that burns through context windows and developer patience alike.
Claude Code’s Plan Mode addresses these failures directly. By enforcing a read-first workflow, Plan Mode separates the reasoning phase from the execution phase, preventing the tool from modifying files before it has fully mapped the codebase’s structure and dependencies. Rather than letting the assistant “just start editing,” Plan Mode requires it to read, analyze, and propose a structured plan that the developer reviews and approves before a single file is touched.
Rather than letting the assistant “just start editing,” Plan Mode requires it to read, analyze, and propose a structured plan that the developer reviews and approves before a single file is touched.
This article walks through a complete React and Node.js refactor using the Plan Mode workflow, from initial prompt through execution, with concrete code examples at each stage.
Prerequisites
Before following this workflow, ensure you have:
- Claude Code CLI installed (verify with
claude --version; flag syntax may vary across versions) - Node.js 18+ and npm/yarn
- Vite 4+ with a React project (
npm create vite@latest) - Express.js 4+ for the backend routes
- Vitest + React Testing Library for tests
module-alias(ortsconfig-paths) for Node.js path alias resolution — details in the walkthrough below
What Is Plan Mode and How Does It Work?
Plan Mode vs. Default Mode
In its default operating mode, Claude Code reads and writes in a single flow. It ingests context, reasons about changes, and begins producing edits in one continuous pass. For small, well-scoped tasks, this works fine. For complex multi-file refactors, it creates problems: the assistant may start modifying early files before it fully understands later dependencies.
Plan Mode enforces a two-phase cycle. In the first phase, Claude reads files, traces imports, identifies coupling, and produces a structured plan. In the second phase, after the developer has reviewed and approved the plan, Claude executes the proposed changes.
Two Ways to Enter Plan Mode
There are two entry points, each suited to different workflows.
The first is at launch via the CLI flag --permission-mode plan, which starts the entire session in Plan Mode. The second is mid-session using Shift+Tab in the terminal interface, which toggles into Plan Mode on the fly. The launch flag is the better choice when the developer knows upfront that the task involves a complex refactor. The mid-session toggle is useful when a task that seemed simple reveals unexpected complexity and the developer wants Claude to stop, regroup, and plan before continuing.
Note: Shift+Tab is specific to the Claude Code terminal (TUI) interface. IDE extension users (VS Code, JetBrains) should consult the extension’s documentation for the Plan Mode toggle.
claude --permission-mode plan --model <model-id> /path/to/project
claude --permission-mode plan
When Plan Mode is active, the interface displays a visual indicator confirming that Claude is operating in the read-and-plan phase. This serves as a persistent reminder that no file writes will occur until the plan is explicitly approved.
The Read-First Workflow, Step by Step
Step 1: Scope the Refactor with a Clear Prompt
The quality of Plan Mode’s output depends heavily on the quality of the input prompt. An effective refactor prompt includes the goal (what the codebase should look like after), the constraints (what must not break, what conventions to follow), and the scope (which files or directories are involved).
Consider this scenario: a React and Node.js application currently organized by technical role (all components in one folder, all routes in another, all utilities in a third) needs to be reorganized into a feature-based architecture where each feature directory contains its own components, routes, utilities, and tests.
Prompt submitted to Claude in Plan Mode:
"Refactor this React + Node.js application from a role-based monolithic structure
to a feature-based folder architecture. Each feature (auth, dashboard, settings)
should contain its own components, API routes, utils, and tests. Preserve all
existing functionality and ensure no circular dependencies are introduced."
Current folder structure:
src/
├── components/
│ ├── LoginForm.jsx
│ ├── SignupForm.jsx
│ ├── DashboardHeader.jsx
│ ├── DashboardChart.jsx
│ ├── SettingsPanel.jsx
│ ├── SettingsToggle.jsx
│ └── SharedButton.jsx
├── routes/
│ ├── authRoutes.js
│ ├── dashboardRoutes.js
│ └── settingsRoutes.js
├── utils/
│ ├── validateEmail.js
│ ├── formatDate.js
│ └── apiHelpers.js
├── tests/
│ ├── LoginForm.test.jsx
│ ├── DashboardChart.test.jsx
│ └── SettingsPanel.test.jsx
└── index.js
Step 2: Let Claude Read and Map Dependencies
With the prompt submitted in Plan Mode, Claude reads the specified files, traces import statements across the codebase, and identifies coupling between modules. It proposes no edits during this phase. Instead, it produces a dependency map and impact analysis: which files import from which, where shared utilities are consumed, and what ripple effects a restructuring would create.
This phase becomes significantly more effective when the project includes a CLAUDE.md file at the repository root. This file anchors Claude’s reasoning by specifying project conventions, architectural rules, and patterns that might not be obvious from the code alone.
The path aliases used throughout this project map as follows:
| Alias | Resolves To |
|---|---|
@features/ | src/features/ |
@shared/ | src/shared/ |
# CLAUDE.md — Project Context for AI Assistants
## Architecture Rules
- Feature folders must be self-contained: components, routes, utils, and tests co-located
- Shared utilities used by 3+ features belong in `src/shared/`
- No barrel export files (index.js re-exports) — they add bundle analysis complexity (project convention — tree-shaking impact depends on bundler configuration and sideEffects settings)
- All imports use path aliases: `@features/auth/`, `@shared/`
## Naming Conventions
- React components: PascalCase (e.g., LoginForm.jsx)
- Route handlers: camelCase with "Routes" suffix (e.g., authRoutes.js)
- Test files: co-located, same name with `.test` suffix
## Test Patterns
- Testing library: Vitest + React Testing Library
- Test files must live alongside their source files, not in a separate `tests/` folder
- All API route tests use the `supertest` wrapper in `@shared/testUtils`
## Constraints
- Do not modify the Express server configuration in `index.js` beyond updating import paths
- `apiHelpers.js` is consumed by all three feature areas — it must move to `src/shared/`
Step 3: Review and Refine the Plan
After reading and mapping the codebase, Claude outputs a numbered, ordered plan of file moves, renames, and edits. Each step specifies what happens and why. This is the critical review point: the developer reads the plan, asks questions, and requests revisions before anything is executed.
Claude's Plan Output:
Plan: Migrate to feature-based architecture (7 steps)
1. Create feature directories: src/features/auth/, src/features/dashboard/,
src/features/settings/, src/shared/
2. Move shared utilities:
- src/utils/apiHelpers.js → src/shared/apiHelpers.js
(consumed by authRoutes, dashboardRoutes, settingsRoutes)
- src/utils/validateEmail.js → src/features/auth/utils/validateEmail.js
(only consumed by LoginForm and authRoutes)
- src/utils/formatDate.js → src/features/dashboard/utils/formatDate.js
(only consumed by DashboardChart)
3. Move auth files:
- LoginForm.jsx, SignupForm.jsx → src/features/auth/components/
- authRoutes.js → src/features/auth/routes/
- LoginForm.test.jsx → src/features/auth/components/
4. Move dashboard files:
- DashboardHeader.jsx, DashboardChart.jsx → src/features/dashboard/components/
- dashboardRoutes.js → src/features/dashboard/routes/
- DashboardChart.test.jsx → src/features/dashboard/components/
5. Move settings files:
- SettingsPanel.jsx, SettingsToggle.jsx → src/features/settings/components/
- settingsRoutes.js → src/features/settings/routes/
- SettingsPanel.test.jsx → src/features/settings/components/
6. Move SharedButton.jsx → src/shared/components/SharedButton.jsx
(consumed by auth and settings features)
7. Rewrite all import paths to use @features/ and @shared/ aliases.
Update path alias configuration in vite.config.js.
⚠ Note: validateEmail.js is imported by both LoginForm.jsx AND SignupForm.jsx.
Confirm placement in auth/utils/ since both consumers are in the auth feature.
Developer follow-up prompt:
"Step 6 — SharedButton is also used by DashboardHeader. Does your plan account
for that? Also, add a step to update the Express route registration in index.js."
Updated Plan (steps 1–5 and 7 unchanged; step 6 revised; step 8 added):
6. [REVISED] Move SharedButton.jsx → src/shared/components/SharedButton.jsx
(consumed by auth/LoginForm, settings/SettingsToggle, AND dashboard/DashboardHeader)
8. [NEW] Update src/index.js to import route handlers from new
@features/*/routes/ paths. No changes to Express middleware or server config.
This iterative refinement is where Plan Mode delivers its primary value. Each question sharpens the plan before execution, catching issues that would otherwise surface as broken imports at runtime.
This iterative refinement is where Plan Mode delivers its primary value. Each question sharpens the plan before execution, catching issues that would otherwise surface as broken imports at runtime.
After the plan is finalized, ensure your vite.config.js has the alias configuration that the refactored imports depend on:
import { defineConfig } from 'vite';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig({
resolve: {
alias: {
'@features': path.resolve(__dirname, 'src/features'),
'@shared': path.resolve(__dirname, 'src/shared')
}
}
});
Step 4: Execute the Plan
Important: Before executing the plan, commit your current state to version control (git add -A && git commit -m "pre-refactor checkpoint"). Plan execution moves and rewrites files — having a clean rollback point is essential.
Once the plan is approved, Claude carries out the edits in the specified order. During execution, it follows the permission model: each file write triggers a confirmation prompt unless you pre-configured permissions (see Claude Code documentation for --permission-mode options and how to configure them per-project). If something looks wrong mid-execution — for example, an import path pointing to a non-existent directory — pressing Shift+Tab (in the TUI) drops back into Plan Mode immediately, halting further writes and re-entering the reasoning phase.
This interplay between planning and execution means the developer maintains control throughout, approving each phase transition rather than hoping the assistant gets everything right in one pass.
Practical Example: Refactoring a React and Node.js App
Before: The Monolithic Structure
Here is a representative React component and Node.js route handler from the pre-refactor codebase. Note the deep relative imports and the cross-cutting dependency on shared validation logic.
import React, { Component } from 'react';
import { validateEmail } from '../utils/validateEmail';
import SharedButton from './SharedButton';
class LoginForm extends Component {
state = { email: '', error: null };
handleSubmit = (e) => {
e.preventDefault();
if (!validateEmail(this.state.email)) {
this.setState({ error: 'Invalid email address' });
return;
}
this.props.onLogin(this.state.email);
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
value={this.state.email}
onChange={(e) => this.setState({ email: e.target.value })}
/>
{this.state.error && <span>{this.state.error}span>}
<SharedButton label="Log In" type="submit" />
form>
);
}
}
export default LoginForm;
const express = require('express');
const router = express.Router();
const { validateEmail } = require('../utils/validateEmail');
const { sendJson } = require('../utils/apiHelpers');
router.post('/login', (req, res) => {
try {
if (!req.body || typeof req.body !== 'object') {
return sendJson(res, 400, { error: 'Request body missing or malformed' });
}
const { email, password } = req.body;
if (!email || !password) {
return sendJson(res, 400, { error: 'Email and password are required' });
}
if (!validateEmail(email)) {
return sendJson(res, 400, { error: 'Invalid email' });
}
sendJson(res, 200, { token: '/* generated JWT — see auth implementation */' });
} catch (err) {
console.error('[authRoutes] /login error:', err);
return sendJson(res, 500, { error: 'Internal server error' });
}
});
module.exports = router;
After: Feature-Based Architecture via Plan Mode
After Plan Mode execution, the same files live in their feature directories with updated import paths.
import React, { Component } from 'react';
import { validateEmail } from '@features/auth/utils/validateEmail';
import SharedButton from '@shared/components/SharedButton';
class LoginForm extends Component {
state = { email: '', error: null };
handleSubmit = (e) => {
e.preventDefault();
if (!validateEmail(this.state.email)) {
this.setState({ error: 'Invalid email address' });
return;
}
this.props.onLogin(this.state.email);
};
render() {
return (
<form onSubmit={this.handleSubmit}>
<input
value={this.state.email}
onChange={(e) => this.setState({ email: e.target.value })}
/>
{this.state.error && <span>{this.state.error}span>}
<SharedButton label="Log In" type="submit" />
form>
);
}
}
export default LoginForm;
const express = require('express');
const router = express.Router();
const { validateEmail } = require('@features/auth/utils/validateEmail');
const { sendJson } = require('@shared/apiHelpers');
router.post('/login', (req, res) => {
try {
if (!req.body || typeof req.body !== 'object') {
return sendJson(res, 400, { error: 'Request body missing or malformed' });
}
const { email, password } = req.body;
if (!email || !password) {
return sendJson(res, 400, { error: 'Email and password are required' });
}
if (!validateEmail(email)) {
return sendJson(res, 400, { error: 'Invalid email' });
}
sendJson(res, 200, { token: '/* generated JWT — see auth implementation */' });
} catch (err) {
console.error('[authRoutes] /login error:', err);
return sendJson(res, 500, { error: 'Internal server error' });
}
});
module.exports = router;
Note for Node.js routes: require() does not resolve Vite-style @-prefixed path aliases natively. You must configure a Node.js alias resolver. The simplest approach is to install module-alias:
npm install module-alias
Then add alias mappings to your package.json:
{
"_moduleAliases": {
"@features": "./src/features",
"@shared": "./src/shared"
}
}
And add require('module-alias/register'); as the first line of src/index.js. Without this setup, all require('@features/...') and require('@shared/...') calls will throw MODULE_NOT_FOUND at runtime.
New folder structure:
src/
├── features/
│ ├── auth/
│ │ ├── components/
│ │ │ ├── LoginForm.jsx
│ │ │ ├── LoginForm.test.jsx
│ │ │ └── SignupForm.jsx
│ │ ├── routes/
│ │ │ └── authRoutes.js
│ │ └── utils/
│ │ └── validateEmail.js
│ ├── dashboard/
│ │ ├── components/
│ │ │ ├── DashboardHeader.jsx
│ │ │ ├── DashboardChart.jsx
│ │ │ └── DashboardChart.test.jsx
│ │ ├── routes/
│ │ │ └── dashboardRoutes.js
│ │ └── utils/
│ │ └── formatDate.js
│ └── settings/
│ ├── components/
│ │ ├── SettingsPanel.jsx
│ │ ├── SettingsPanel.test.jsx
│ │ └── SettingsToggle.jsx
│ └── routes/
│ └── settingsRoutes.js
├── shared/
│ ├── apiHelpers.js
│ ├── testUtils.js
│ └── components/
│ └── SharedButton.jsx
└── index.js
What Claude Caught That Manual Refactoring Misses
During the read phase, Claude identified three issues that would have gone unnoticed in a manual move-and-rename workflow:
- Circular dependency check on
apiHelpers.js: As an example of Plan Mode’s detection capability, ifapiHelpers.jsimported a utility that re-exported fromapiHelpers.js, Plan Mode would surface this chain. In this refactor, Claude confirmedapiHelpers.jshad no such cycles before moving it toshared/. - Barrel export suppression: The
CLAUDE.mdconstraints explicitly prohibited barrel export files, and Claude’s plan omitted creatingindex.jsre-export files in each feature directory, respecting the project convention against barrel exports. - Test import path updates: All three test files in the original
tests/directory used relative paths to their source components. Plan Mode’s plan included updating these paths as explicit steps rather than leaving them as an afterthought.
Tips, Pitfalls, and Best Practices
When to Use Plan Mode (and When Not To)
Plan Mode is designed for multi-file refactors, architectural migrations, and working with unfamiliar codebases where the dependency graph is not immediately obvious. It produces a dependency map and sequenced edit plan for tasks that involve more than three or four interconnected files.
For single-file bug fixes, simple renames, or quick prototyping where speed matters more than precision, skip it. The overhead of producing and reviewing a structured plan does not pay off when the task is trivially scoped.
Common Pitfalls
Start with your prompt. Vague prompts are Plan Mode’s worst enemy. Because it amplifies the prompt into a detailed plan, ambiguity in the input becomes amplified ambiguity in the output. “Clean up the codebase” produces nothing useful. “Migrate from role-based to feature-based architecture following the conventions in CLAUDE.md” gives Plan Mode enough to work with.
Don’t skip the plan review phase. Auto-approving everything turns Plan Mode into a slower version of default mode with no safety benefit. The review phase is where you catch incorrect assumptions and missing edge cases.
Watch your test files. Forgetting to include them in the refactor scope is another common oversight. Tests have their own import paths, and a restructured codebase with unrelocated test files will throw MODULE_NOT_FOUND errors the moment you run npx vitest run.
Power User Tips
Combining Plan Mode with --allowedTools provides fine-grained control over what Claude does during the planning phase, restricting which tool types Claude invokes (e.g., disabling Bash execution while allowing file reads). For directory scoping, use your working directory and .claudeignore (if supported by your version — check claude --help or current documentation to confirm availability).
Using a .claudeignore file (analogous to .gitignore) excludes build artifacts, node_modules, and other irrelevant directories from the read phase, reducing noise and keeping Claude focused on the actual source code. Check whether your Claude Code version supports this feature before relying on it.
For very large refactors, chaining Plan Mode sessions works well: Phase 1 restructures the file system, Phase 2 optimizes imports and removes dead code. Each phase gets its own plan, review, and execution cycle.
Implementation Checklist: The Read-First Refactor Workflow
- Write or update
CLAUDE.mdwith project conventions, naming rules, and architectural constraints - Commit your current state to version control as a rollback checkpoint
- Enter Plan Mode (
claude --permission-mode planorShift+Tabmid-session in the TUI) — verify flag syntax withclaude --helpif unsure - Submit a scoped refactor prompt with explicit goals, constraints, and file scope
- Review Claude’s dependency map and impact analysis — verify that every moved file’s consumers are accounted for and that no circular dependencies exist in the proposed structure
- Iterate on the plan by asking targeted questions: “Which files consume this utility?”, “Does moving X break Y’s import?”, “Are test files included in step N?”
- Approve the finalized plan and execute
- Toggle back to Plan Mode (
Shift+Tabin the TUI) immediately if anything looks wrong during execution - Run the full test suite (
npx vitest run) and verify all import paths resolve with zero errors post-refactor - Commit with a meaningful message referencing the plan’s scope and rationale
Why the Read-First Workflow Pays Off
With Plan Mode active, Claude Code reads the codebase and maps dependencies before proposing any edits for human review. The read-first workflow reduces wasted tokens spent on incorrect edits, eliminates hallucinated imports caused by incomplete context, and prevents broken builds from misreading the dependency graph.
The read-first workflow reduces wasted tokens spent on incorrect edits, eliminates hallucinated imports caused by incomplete context, and prevents broken builds from misreading the dependency graph.
For any refactor that touches more than a handful of files, the overhead of producing and reviewing a plan pays for itself on the first avoided rollback.

