· 6 min read
AI Coding Agent Scope Creep: Put Non-Goals in the Spec
How I stop AI coding agent scope creep with a non-goals list, allowed paths per task and a small git diff check that fails when the agent wanders.
You ask an AI coding agent to add one filter to a screen. The diff comes back with the filter, a renamed helper, a "cleaned up" Cubit, a new dependency and a reformatted file you never mentioned. Each change looks reasonable on its own. Together they turn a ten-minute review into an hour. This is AI coding agent scope creep, and in my experience it is a bigger cost than wrong code. This post shows the three things I put in every spec to keep the agent inside the task: a non-goals list, allowed paths per task, and a diff check that fails when the agent wanders.
Why agents drift outside the task
An agent drifts for the same reasons a keen new teammate does. It sees something that looks wrong and fixes it. It finds two helpers that do almost the same thing and merges them. It runs the formatter on the whole file because that is what the project's tooling does.
None of this is malice or a bug. The agent is trying to leave the code better than it found it. The problem is the cost on the other side:
- Review gets slower. Every extra file is something I have to read and understand.
- Risk spreads. A refactor of shared code can break a screen the task never touched, and the tests for that screen may not exist.
- History gets muddy. When a regression shows up a month later,
git blamepoints to a commit called "add badge filter" that also changed the sync logic.
A spec that only says what to build leaves the edges open. The agent fills the open space with good intentions. So I close the edges in writing.
Write non-goals before tasks
Every spec I write starts with what the change is and why it exists. Right after that comes a short Non-goals section. It lists the things a reasonable person might expect to be part of the work, but that are not.
## Non-goals
- No redesign of the badges screen. Layout and colors stay as they are.
- No schema change. The new filter reads existing columns only.
- No refactor of BadgesCubit beyond the new filter state.
- No dependency changes. pubspec.yaml and pubspec.lock stay untouched.
- No formatting-only changes in files the task does not otherwise edit.
The trick is to write non-goals for the temptations, not for random things. Nobody needs "do not rewrite the app in Kotlin". What you need is the thing that sits right next to the task. If the task adds a filter to a list, the temptation is to also restyle the list. If the task fixes a bug in a repository class, the temptation is to also "simplify" the other methods in it.
A good test: read the task, then ask "what would I be tempted to touch while I am in there?" Each answer is a non-goal.
Non-goals also help me. Writing them forces a decision before any code exists. If I find myself writing "no refactor of X" and feeling uneasy, that is a sign X really needs a refactor. Then it gets its own task, with its own review, instead of hiding inside another change.
Give each task an allowed-paths list
Non-goals describe intent. Allowed paths make it checkable. In the task breakdown, each task names the folders and files it is allowed to change:
### Task 3: Add "want to visit" filter to the badges screen
Touch:
- lib/features/badges/
- test/features/badges/
Do not touch:
- lib/data/
- pubspec.yaml, pubspec.lock
If the task cannot be done inside these paths, stop and explain why.
Do not widen the scope on your own.
The last two lines are the important ones. Sometimes the allowed paths are wrong. Maybe the filter really does need a new query in the data layer. That is fine, and it happens. What I want is for the agent to stop and say so, so I can decide. A note like "I need one new method in TripRepository, here is the signature" takes me a minute to approve. The same change found by surprise in a big diff takes much longer to trust.
Keep the lists short and folder-based. If a task needs a list of fifteen files, the task is too big and should be split.
Check the diff with a script, not with trust
Instructions in a spec are read, but they are also forgotten halfway through a long session. So the allowed paths are also a file the tooling can read. Each task gets a plain list of path prefixes, for example .scope/task-3.txt:
^lib/features/badges/
^test/features/badges/
Then a small script, scripts/check-scope.sh, compares the branch diff against that list. It takes the scope file and an optional base branch:
#!/usr/bin/env bash
set -euo pipefail
scope_file="$1"
base="${2:-main}"
outside=$(git diff --name-only "$base"...HEAD \
| grep -v -E -f "$scope_file" || true)
if [[ -n "$outside" ]]; then
echo "Files changed outside the task scope:"
echo "$outside"
exit 1
fi
echo "Scope check passed."
git diff --name-only lists every file the branch changed compared with the base. The grep -v -E -f keeps only the files that match none of the allowed patterns. If anything is left, the script prints it and exits with an error.
I tell the agent to run this before it reports the task as done, and to paste the output into its hand-off summary. If the check fails, there are only two honest options: revert the extra changes, or explain each one and ask me to widen the scope. Silently leaving them in is not an option.
This fits next to the verification checklist I described in making the agent prove it is done. That checklist asks "does the feature work?". The scope check asks "did anything else change?". Both questions need an answer before I start reading code.
What to do with the good ideas
Shutting the door on extra changes has a cost: sometimes the agent spots something real. A duplicated helper, a missing null check, a test that asserts nothing. I do not want to lose those.
So the spec also asks for a Follow-ups list in the hand-off. Anything the agent noticed but did not change goes there, one line each, with the file and the reason. After review I go through the list. Some items become new tasks. Some I drop. A few turn out to be wrong, which I would rather learn from a one-line note than from a merged refactor.
This changes the agent's role from "fix everything you see" to "fix the task, report the rest". In my experience that split makes reviews calmer and the follow-up tasks better, because each one gets its own spec and its own non-goals.
Takeaways
- Scope creep from AI coding agents is a review and risk problem, even when every extra change looks fine on its own.
- Write a Non-goals section for the temptations that sit right next to the task.
- Give each task an allowed-paths list, and tell the agent to stop and ask if the work does not fit inside it.
- Check the diff with a script that fails on files outside the allowed paths. Make the agent paste its output.
- Collect the agent's good ideas in a Follow-ups list instead of letting them slip into the current diff.