Try this first
Go back to Lesson 1.3. The model asks. Your code acts.
That means the permission layer is not a feature you have to negotiate with the model. It is
an if statement in a function you own, on a code path the model cannot reach.
The simplest version that works
ALWAYS_ALLOW = {"read_file", "list_files", "search_files"}
remembered = set()
def may_run(name, tool_input):
if name in ALWAYS_ALLOW or name in remembered:
return True
print(f"\nRover wants to run: {name}")
print(f" with: {tool_input}")
answer = input(" allow? [y]es / [n]o / [a]lways: ").strip().lower()
if answer == "a":
remembered.add(name)
return True
return answer == "y"
And in the loop, in front of the tool call:
if not may_run(block.name, block.input):
output, failed = "The user did not allow this. Do not try it again.", True
else:
output, failed = run_tool(block.name, block.input)
That is the whole mechanism. Thirty lines, and it is the same mechanism the agent products
you have used are running.
The four decisions
| Answer | What happens | When you want it |
|---|---|---|
| Allow once | Runs this time only | Anything with a side effect you want to see each time |
| Deny | Does not run, model is told | It proposed something wrong |
| Always allow | Runs now and skips the prompt later | Reads, searches, listing — things that cannot hurt |
| Deny and explain | Does not run, model is told why | It has the right idea and the wrong command |
That last row is the one people forget, and it is the most useful.
Denial is a message, not a silence
A denied tool must still return a result. Look at the wording above:
"The user did not allow this. Do not try it again."
Compare with what happens if you say nothing useful. The model sees a failure with no
explanation, assumes something went wrong mechanically, and tries again — sometimes the exact
same command, sometimes a variation. You end up denying the same thing five times.
Better still, let yourself say why:
answer = input(" allow? [y]es / [n]o / [a]lways / [e]xplain: ").strip().lower()
if answer == "e":
reason = input(" tell Rover why not: ")
return False, f"The user declined: {reason}"
Now "Do not delete the folder — use git clean -n first to see what would go" reaches the
model, and its next proposal is usually right.
A denial is a turn in the conversation. Say why, and the next proposal improves. Stay
silent, and it tries again.
Read-only is not automatically safe
ALWAYS_ALLOW above includes the read tools, and that is a reasonable default for a personal
tool on your own machine.
It is not a universal rule. Reading is how data leaves. An agent that can read any file and
also reach the network can move your credentials somewhere else, without ever writing to
disk. In that setting, “reads are free” is wrong.
The right default depends on what else the agent can do. Name the assumption, do not inherit
it.
Try this before the next lesson
Add the permission layer. Ask Rover to do something with several steps, like “tidy up this
folder and write a summary”.
Say no to one step and explain why. Watch how it adapts. Then say no to the same class of
thing with no explanation, and watch it retry. The difference is the lesson.