AI coding assistants are surprisingly good at rewriting code.
The difficult part is getting them to change only what you asked them to change.
Ask an AI assistant to "clean up this function" and it may also rename variables elsewhere, replace a utility, change an API signature, reorganize imports, modify error handling, or rewrite surrounding code.
For a small experiment, that may not matter.
Inside an established codebase, it can create unnecessary regressions.
The solution isn't simply to tell the model:
Don't change anything else.
A better approach is to define the refactoring boundary explicitly and make the model prove that it stayed inside that boundary.
The Real Problem With "Refactor This Function"
Consider a function such as:
function calculateTotal(items) {
let total = 0;
for (let i = 0; i < items.length; i++) {
if (items[i].active === true) {
total = total + items[i].price;
}
}
return total;
}
You might want the AI to improve readability without changing behavior.
A typical request would be:
Refactor this function to make it cleaner.
That instruction leaves several important questions unanswered:
- Can the function signature change?
- Can the return type change?
- Can surrounding functions be modified?
- Can a new dependency be introduced?
- Can the definition of
activechange? - Can error handling be changed?
- Can the function's performance characteristics change?
The model has to infer your intentions.
That is where unnecessary changes begin.
Define a Refactoring Contract
Instead of describing only the desired improvement, define a small contract for the operation.
For example:
Refactoring contract
- Modify only
calculateTotal.- Keep the function signature unchanged.
- Keep the return type unchanged.
- Preserve the current behavior.
- Do not add dependencies.
- Do not modify callers.
- Do not modify unrelated functions.
- Return the complete replacement function.
This gives the model a much narrower search space.
Use a Change Boundary
One of the most useful additions to an AI coding prompt is an explicit boundary.
Allowed change: the implementation body of
calculateTotal.Protected: function name, parameters, return behavior, callers, imports, public interfaces, and unrelated functions.
The distinction between allowed and protected areas is more precise than simply asking the AI to be careful.
Separate Behavior From Implementation
A refactoring should normally change the implementation without unintentionally changing the observable behavior.
For example, if the existing function ignores inactive items, the new implementation must continue to ignore them.
Write that behavior down.
Behavior that must remain unchanged:
- Only items where
active === truecontribute to the total.- The function returns the sum of their prices.
- An empty array returns
0.- The function accepts the existing
itemsargument.
This is considerably more useful than saying "don't break anything."
Don't Give the Model More Repository Access Than Necessary
When an AI coding tool has access to an entire repository, it can often inspect and modify many files.
That capability is useful for large tasks but unnecessary for a tightly scoped refactoring.
If your task is only to change one function, provide the model with the relevant context and explicitly restrict the modification.
The goal is not to hide useful information.
The goal is to prevent scope expansion.
Specify What the Model May Inspect and What It May Modify
These are two different permissions.
You might allow the AI to inspect callers so it understands how the function is used while still prohibiting changes to those callers.
You may inspect the function's callers to understand compatibility requirements. Do not modify those callers.
This is a much safer instruction than simply limiting the model's context.
Ask for a Minimal Diff
If your goal is a small refactoring, tell the model to minimize the change surface.
Prefer the smallest implementation change that achieves the requested improvement. Do not perform unrelated cleanup.
This matters because AI coding assistants often identify additional improvements while working.
Those improvements may be valid, but they belong in separate changes.
Why "While You're There" Is Dangerous
Suppose the model notices that another function also uses an outdated naming convention.
It may decide to fix that function as part of the same operation.
That creates a larger change than the original request.
Instead, establish this rule:
If you identify unrelated improvements, list them separately but do not implement them.
This lets the AI remain useful without allowing scope creep.
Use a Pre-Change Inspection Step
For repository-level work, ask the AI to inspect before modifying.
Before proposing code changes:
- Identify the target function.
- Identify its callers.
- Identify any externally visible behavior that must remain unchanged.
- Identify dependencies used by the current implementation.
- State the proposed change boundary.
Do not modify files during this inspection step.
This creates a checkpoint between understanding the code and changing the code.
Make the AI State Its Assumptions
AI-generated code can fail because the model silently assumes something that isn't true.
For example:
Assumption:
itemsis always an array.
If that assumption isn't guaranteed, the proposed refactoring might introduce a new failure mode.
Add:
Before refactoring, list any assumptions that materially affect behavior. Do not silently introduce new assumptions.
This is particularly useful when modifying legacy code.
Ask for Invariants
An invariant is a property that should remain true before and after the change.
For a calculation function, examples might include:
- The same input produces the same output.
- The public function signature remains unchanged.
- Existing error behavior remains unchanged.
- No new external dependency is required.
Give the model these invariants before it writes the refactoring.
Turn the Refactoring Into a Two-Stage Prompt
A reliable workflow is to separate analysis from implementation.
Stage 1: Analyze
Analyze the target function and determine whether it can be refactored without changing observable behavior.
Identify:
- Current behavior
- Dependencies
- Callers
- Potential edge cases
- Proposed refactoring
- Any assumptions
Do not modify code yet.
Stage 2: Implement
Now implement only the approved refactoring.
Modify only the target function. Preserve its signature, behavior, dependencies, and external interface.
Do not perform unrelated cleanup.
The separation makes it much easier to detect an incorrect interpretation before it becomes a code change.
Ask for a Diff-Oriented Response
If you're reviewing AI-generated changes manually, don't ask for a complete rewritten file when you only need one function.
Ask for:
Return only the modified function and a concise explanation of what changed. Do not reproduce unrelated code.
This reduces the amount of generated code you need to inspect.
Use Tests as the Actual Safety Boundary
Prompt instructions are useful, but tests provide a stronger verification mechanism.
If the existing behavior is important, provide the relevant tests to the coding assistant.
For example:
describe("calculateTotal", () => {
test("includes active items", () => {
expect(calculateTotal([
{ active: true, price: 10 },
{ active: true, price: 20 }
])).toBe(30);
});
test("ignores inactive items", () => {
expect(calculateTotal([
{ active: true, price: 10 },
{ active: false, price: 50 }
])).toBe(10);
});
test("returns zero for an empty list", () => {
expect(calculateTotal([])).toBe(0);
});
});
Now the model has concrete behavioral evidence instead of relying entirely on a natural-language description.
Tell the AI to Preserve Existing Tests
For a narrowly scoped refactoring, use a rule such as:
Do not modify existing tests merely to make the refactoring pass. If a test appears incompatible with the requested change, explain the conflict instead.
This prevents a particularly dangerous form of scope expansion: changing the verification system to accommodate the generated code.
Use a Post-Change Checklist
After the refactoring, ask the model to verify the boundary independently.
Post-change verification:
- Was only the requested function modified?
- Did the function signature remain unchanged?
- Did the return behavior remain unchanged?
- Were new dependencies introduced?
- Were callers modified?
- Were unrelated files changed?
- Could any edge-case behavior have changed?
Report each item explicitly.
This creates a second pass focused specifically on unintended changes.
Don't Let the Same AI Be the Only Reviewer
A model can generate a change and then confidently report that the change is safe.
That doesn't make the verification independent.
For important code, use objective checks where possible:
- Run the existing test suite.
- Inspect the actual diff.
- Run static analysis.
- Run type checking.
- Run formatting and linting checks.
- Test important edge cases.
The AI should assist the verification process, not replace it.
A Better Refactoring Prompt
Here is a reusable template for small, high-control refactoring tasks:
ROLE
You are modifying an existing codebase. Treat this as a constrained refactoring task, not a general code cleanup.
TARGET
Modify only:
[file/function].OBJECTIVE
[Describe the specific improvement.]
PRESERVE
- Function signature
- Return behavior
- Public API
- Existing dependencies
- Observable behavior
- Existing error behavior
DO NOT MODIFY
- Callers
- Unrelated functions
- Unrelated files
- Existing tests
- Dependencies
PROCESS
- Analyze the target.
- Identify relevant invariants and edge cases.
- State assumptions.
- Propose the smallest suitable change.
- Implement only that change.
- Review the resulting change against the constraints.
OUTPUT
Return the modified function, followed by a concise change summary and verification checklist.
When This Approach Is Especially Useful
This constrained method is particularly useful when working with:
- Legacy applications
- Large repositories
- Production code
- Public APIs
- Shared utility functions
- Payment or authentication code
- Database access layers
- Code with limited test coverage
The more expensive an unintended change would be, the more valuable a strict change boundary becomes.
When You Shouldn't Use a Strict One-Function Boundary
There are situations where a larger change is genuinely necessary.
For example, changing a public API may require updating its callers, tests, documentation, and types.
In that situation, don't artificially restrict the AI to one function.
Instead, define the complete change surface explicitly.
These files are allowed to change because the API signature is intentionally changing. Do not modify anything outside this list.
The principle isn't "always change one function."
The principle is:
Know the intended change surface before the AI starts modifying code.
Final Takeaway
AI coding assistants are most dangerous when a small request is interpreted as an invitation to improve everything nearby.
You can reduce that risk by turning a vague refactoring request into a defined contract.
Specify the target, define what must remain unchanged, identify protected code, state behavioral invariants, separate analysis from implementation, and verify the resulting diff.
Most importantly, don't rely on a single instruction such as "don't break anything."
Tell the AI exactly what it is allowed to change and what it is not allowed to change.
That makes AI-assisted refactoring considerably easier to review, repeat, and trust.
