It Did Things I Never Asked For — Binding an Agent's Task Scope With a Contract
Ask it to fix a button color and you get a refactor, renames, and a dependency bump too. This is a scope problem, not a permission one. Here is a contract that stops at the scope boundary and asks.
I thought I had only asked to "fix the button color." The returned changes included the color fix plus a refactor of a nearby function, a variable rename, and a dependency version bump for good measure. All of them well-meant, and all of them work. But what I wanted to see was a single line of color change; the rest only swelled the review and planted unintended diffs.
When you run several apps in indie development, this "over-helpfulness" quietly adds up. Each instance is small, but stacked together you lose track of which change you actually intended. What I want to bind here is not permission. The write access is fine. What I want to bind is acting beyond what was asked.
This Is a Scope Problem, Not a Permission One
Talk of reining in a runaway agent tends to start with permissions: what can it write to, what can it execute. That matters too, but this problem sits on a different layer. Having permission to fix the color is fine. The problem is doing other things while fixing the color — being over-helpful beyond the task's scope.
Binding with permissions reduces what the agent can do. Binding with scope leaves what it can do unchanged, and limits only "what is allowed this time." The latter is what I wanted.
Hardening Permissions Leaves the Scope Accidents Behind
This distinction is not abstract — it shows up in the tooling. Antigravity CLI 1.1.3, released on July 16, 2026, closed two holes in the permission layer.
The first was headless execution (-p). When it hit a tool that required confirmation, you got one of two outcomes: it hung there, or it silently auto-approved. After the fix it soft-denies, and the allow rule name required to permit the operation is printed to stderr. The second was always-proceed mode, where file writes outside the workspace were being auto-approved by mistake.
As someone who leaves tasks running unattended overnight, the second one gave me a chill. An auto-approval that can write outside the workspace is a permission-layer bug, and it deserved exactly the fix it got.
But once it was fixed, I looked back at my own "button color" incident and realized none of it had been addressed. The writes back then landed inside the workspace, and the permissions were correctly granted. The refactor, the rename, the dependency bump — every one of them is legal from a permission standpoint.
Layer
Shape of the question
Does the 7/16 fix help?
Accident left over
Permission
May it write there?
Yes — blocks auto-approved writes outside the workspace
—
Scope
May it do that this time?
No — the permission stays legitimate
The while-I'm-at-it refactor and rename
The more the permission layer hardens, the more the remaining accidents concentrate in the scope layer. I do not read this as "the tool is safe now, so I can relax." I read it as: the tool got safer, and that made it clear which layer is mine to design.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A task-scope contract, distinct from permissions, that stops over-helpful changes
✦How to write an acceptance rule that proposes — without executing — when it wants to cross the boundary
✦How to carry the contract into headless runs and verify drift from the diff afterward
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
So I put a short contract that states the scope per request into the rules or prompt. It has only three parts.
State what to do (in-scope) in one sentence.
List what not to do (out-of-scope), only the things that tend to happen.
Declare that if it wants to cross the boundary, it proposes without executing and stops.
## Scope of this task- Do: change the submit button color to brand-primary.- Do not: refactor nearby code, rename variables/functions, bump dependency versions, apply the formatter wholesale, ripple into other files.- If tempted to cross the boundary: do not execute. List it as a one-line "proposal" and hand the decision to the human.
The third part is the crux. Banning all "while I'm at it" stops genuinely necessary ripple too. Instead of that, change it to "if tempted, leave a proposal rather than doing it." This keeps the noticing while confining only the execution to scope.
Decide the Behavior at the Boundary
A contract alone is weak, so write the behavior at the boundary as an acceptance rule.
Request
In scope
Tempting out-of-scope
Behavior when crossed
Change button color
The one color line
Nearby refactor, renames
Record as a proposal, do not execute
Fix one bug
The cause and necessary tests
Clearing unrelated warnings wholesale
Carve out as a separate task
Edit copy
The target string
Surrounding formatting, import cleanup
Report the count only, do not touch
Add one dependency
That dependency and minimal wiring
Updating other dependencies together
Stop and confirm with the human
Build this table once and "what to do when crossed" becomes standardized per request type. You no longer write a contract from scratch each time — you pick a type and paste it.
Carrying the Contract Into Headless Runs
While you are working interactively, you notice the moment it steps outside. The hard case is -p. Nobody is watching, so you have to establish after the fact whether the contract held.
I keep the contract out of the request text and put it in the rules file. What -p receives is only the request; the contract gets read from the same place every time. This avoids the failure where a long request buries the contract in its own middle.
# Keep the scope contract resident in the rules file; pass only the request to -pantigravity -p "Change the submit button color to brand-primary" \ 2> "$HOME/agy-scope.err"# Check what was soft-denied, and which allow rule would be needed to permit itgrep -i 'allow' "$HOME/agy-scope.err"
From 1.1.3 onward, a denied operation tells you which allow rule would be required to permit it, on stderr. There is one thing I forbid myself here: do not add the rule it names just to make the message go away.
Adding a rule without reading why it stopped delegates the decision to widen scope to an error message. Confirm the rule is genuinely required by this task's in-scope before adding it. If it is not required, do not add it — leave the operation out of scope. What makes soft-deny useful is that it leaves the material for that judgment in your hands.
Catch the Drift in the Diff
A contract is a declaration, not an enforcement mechanism. Whether it held is visible in the diff.
# The files you declared in scope for this requestcat > "$HOME/scope-allow.txt" <<'EOF'src/components/SubmitButton.tsxEOF# Any file changed outside the declaration prints its namegit diff --name-only | grep -vxF -f "$HOME/scope-allow.txt"
Empty output means the contract held. Non-empty output is itself the record of the drift. Had I run the opening "button color" incident through this, it would have been 1 declared file against 4 in the diff — a mismatch I could have caught on the count alone, before ever opening the review.
Keep these few lines next to your work log and the pattern of which request types drift accumulates on its own. In my case the drift clustered almost entirely in the "tidy up the surroundings while fixing something" type. Once you know where drift happens, the out-of-scope list gets specific instead of generic.
Granularity So It Does Not Stop Too Much
Make the scope too strict and it stops to ask for everything. That is draining in its own way. The key is to fix the granularity of allowed ripple in advance.
I default to a line: "formatting that stays within the same function is in scope; ripple that crosses files is out of scope." If the indentation gets messy around the line you fixed, fixing it within that function is fine. But going off to fix the same kind of spot in another file wholesale is out of scope.
That line also meshes with the diff check above. If cross-file ripple is out of scope by default, judging drift reduces to "is the filename in the declared list?" Granularity design and ease of verification tend to move together.
I vary the granularity by the weight of the request. For a throwaway small fix the scope is narrow; for design-bearing work I allow some ripple. In my own work I take the narrowest scope for requests with external effects — like AdMob-related config — and allow wider ripple for internal test work, running it as two tiers. Treat scope not as a fixed value but as a dial you select per request, and you find the middle between stopping too much and running too wild.
Run Without Discarding Proposals
Instead of letting it execute out-of-scope, having it leave proposals turns those into seeds for good next tasks. "Noticed while fixing the color: this function would be better split" is a line you do not want executed now, but it has value as a separate task.
I transcribe proposals to the end of my work log and review them on the weekend. As the un-executed observations accumulate, they become a list of the next places worth touching. Binding scope is not discarding the agent's powers of observation. Receive the observation, confine only the execution to scope. That separation protects both a light review and the clarity of change intent at once.
Permissions on the tool side will keep hardening, as the July 16 fix shows. That leaves the design of scope — how much of what you actually asked for — as the part only the requester can write.
As a next step, pick one request type you issue most often and write out just three of its "do not" items. Adding three out-of-scope lines alone cuts over-helpful changes considerably.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.