You asked the assistant to fix a null check in one function. The diff that comes back touches eleven files. Two of them you recognise, the rest are tests, a shared utility, a type definition and something in a config directory you have not opened in weeks. Nothing is obviously wrong. That is the uncomfortable part. AI assistant scope creep rarely looks like sabotage; it looks like a diff that is plausible everywhere and verified nowhere.
The reflex is to either revert the whole thing or skim it and commit. Both are bad trades. A wide diff is not a verdict on the assistant, it is a sorting job, and it has a repeatable shape: separate the edits that follow necessarily from what you asked, the edits that are correct but unasked, and the edits that exist because the model decided your task was bigger than you said it was. Each category gets a different action.
Start from the task, not the file list
Before opening the diff, write down in one sentence what you actually asked for. Not what you meant, what you typed. "Handle the case where user.profile is null in renderHeader." That sentence is the boundary you measure against.
Then list the files you expected to change. Two is fine. Zero is fine if you genuinely did not know. The point is to commit to an expectation before the diff shapes it, because reading the diff first anchors you. Once you have seen that src/lib/format.ts changed, your brain starts constructing a reason it should have.
Now run the diff and bucket every changed file into one of three piles: expected, consequential, or unexplained. Consequential means "I did not expect it, but the change I asked for forces it." Unexplained means exactly that.
Where AI assistant scope creep actually comes from
It helps to know what is producing the extra files, because the causes are different and so are the fixes.
The first is legitimate propagation. You asked for a null check, the function signature changed, and now four call sites and a test fixture need updating. The assistant is not expanding scope here, you underestimated the blast radius. This is the most common source of a wide diff and the least worrying.
The second is the helpfulness gradient. Models are trained to produce complete, polished work. If the assistant sees a lint violation two lines below your bug, or a typo in a comment, or an any it could narrow, fixing it reads as better output. It has no cost model for your review time or your reviewer's.
The third is goal reinterpretation, and it is the one to watch. You asked for a null check; the assistant concluded that the real problem is that profile should never be nullable, and went to make that true across the type definition, the API client and the three places that construct the object. It solved a better-defined problem than the one you posed. Sometimes it is right. It is still a different task, and it got merged into yours without a decision point.
The fourth is context drift in long sessions. Twenty messages in, the model is carrying earlier requests that you consider closed. The refactor you mentioned at message four is still, as far as it can tell, on the table. Starting a new session instead of pushing harder cuts this one off at the source.
Three signs the extra changes are real work
Sorting the consequential pile from the unexplained pile gets easier with a few concrete tests.
It would not compile or pass without the change. The strongest signal. If you revert the extra file and the build or the test suite fails, that edit is not scope creep, it is part of the change you asked for. Try it: stash the unexplained files and run the suite. This takes two minutes and resolves most of the pile.
git stash push -- src/lib/format.ts src/types/user.ts
pnpm test
If everything still passes, those edits were optional. That does not make them wrong, it makes them separable.
The edit follows a rename or a signature change you approved. If you accepted a renamed function, every call site has to move. Check that the follow-through is mechanical: same rename, no behaviour changes smuggled into the same file. A file that contains both a rename and a reworked conditional is two changes wearing one hat.
It is a fix to shared code that your change exposed. The assistant touched a helper because your null case revealed the helper never handled null either. That is real work, and often work you want. It is still a second change with its own test requirements, and it deserves its own commit so that when it breaks in three weeks you are not bisecting into a commit titled "fix header null check."
Everything that fails all three tests is the real thing: a reformatted file, a dependency bump nobody asked for, a "while I was here" refactor of code unrelated to the path you touched.
Keep, revert, or split
Once the piles are clear, the decisions are mostly mechanical.
| Category | Action | Why |
|---|---|---|
| Required for the change to work | Keep in the same commit | It is one logical change |
| Correct, unasked, small, tested | Split into a second commit | Reviewable, revertable, bisectable |
| Correct but unasked and large | Revert, open a note or an issue | Needs its own review, not a ride-along |
| Unrelated formatting or config | Revert without discussion | Pure noise in the history |
| You cannot tell | Ask the assistant why it changed that file | Faster than reverse-engineering intent |
That last row matters more than it looks. The cheapest way to classify an unexplained file is to ask: "Why did you change src/types/user.ts? I only asked about the null check in renderHeader." You usually get one of two answers: a concrete dependency you missed, or a rationalisation that falls apart when you read it. Both are useful. A general pre-commit pass is covered in how to review assistant changes before you commit; this is the narrower question of whether the change belongs in this commit at all.
When you split, split with intent. The follow-up commit needs a message that says what it is and why it was separated, because "also fixed the formatter helper" six months later is indistinguishable from an accident. If the split change is big enough to defer, write the reasoning into your project memory rather than trusting yourself to remember which file you were going to come back to.
The instruction line that stops the recurrence
Most of this is preventable with a scope rule in your instruction file. Not a vague one. Assistants follow instructions that describe an observable behaviour far better than ones that describe an attitude.
Weak:
Stay focused on the task. Don't make unnecessary changes.
Workable:
## Scope
Change only the files required for the task I named. If you find a
bug, a lint error or a refactor opportunity in a file you were not
asked to change, do not fix it. List it at the end of your response
under "Noticed but not changed" with the file and one line of why.
If the task requires changing a function signature, say which call
sites you will update before you edit them.
The second version works because it gives the model somewhere to put the impulse. The urge to fix the thing it noticed does not go away; redirecting it into a list is more reliable than suppressing it. You still get the information, and you get it as a to-do rather than a diff. Why guardrails fail and how to rewrite them goes deeper on the difference between rules that hold and rules the assistant drifts past.
One caveat: a strict scope rule will occasionally stop the assistant from making a change that genuinely was required, and you will get a compile error instead. That is a good trade. A build failure is visible in ten seconds. An unreviewed edit in a file you did not read is not.
When the diff outlives the session
The hard version of this problem shows up later. A wide commit lands, three weeks pass, and someone asks why a type definition changed in a commit about a header bug. The diff tells you what changed. It does not tell you what was asked, and intent is the thing you need to judge whether the edit was reasonable.
This is the reason we built PrjLab to keep sessions beside the code rather than in a separate pile. Push the transcript along with the files, and the conversation that produced a commit stays openable next to the version it produced: the prompt you actually typed, the point where the assistant decided the type should change, and whether you approved it or missed it. Reconstructing that from the diff alone is guesswork. Tracing a regression back to its root cause covers the same lookup from the other direction, starting from a bug instead of a commit.
For the mechanics of pushing a project with its sessions attached, the getting-started guide walks through it.
Wide diffs are not going to stop. The models are getting more capable and more willing to act on what they infer, which means the gap between what you asked and what you get will keep showing up in file counts. The habit worth building is not suspicion, it is the sort: expected, consequential, unexplained, then keep, split or revert. Ten minutes of sorting beats an afternoon of bisecting.