Lesson 5: Permissions and Safety: The Boundaries of What an Agent Can Do
Learning goals:
- Sort tools into the allow / ask / deny tiers by whether the consequence is reversible and whether the blast radius reaches beyond local
- Explain why anything a tool returns has to be treated as data, never executed as an instruction
- Name the three root causes of excessive agency, and design a human confirmation point for irreversible actions
Prerequisites: read Lesson 3 (the five common tool types) | Prev Lesson 4 << | Next Lesson 6 >>
How one issue turns into an .env leak
You ask an agent to go through the newly opened issues in a repo and pick out the real bugs. The agent calls its read-file tool and opens one of them:
Anyone at all can open this issue on GitHub, and the way the agent reads it is no different from the way it reads any other block of text. If the agent takes that line inside the HTML comment as a new instruction and acts on it, the next thing it does is go read .env for real and paste the database password into the conversation.
This isn't a theoretical risk. It has a name: prompt injection. The attacker never has to talk to the agent directly. All they need is to hide instructions somewhere the agent will eventually read — an issue, a README, a web page, a file someone sends over. Reading content and receiving instructions travel down the same channel.
The protocol doesn't separate data from instructions — the host has to hold that line
When the agent reads that issue, what the read-file tool actually returns to the model is a block of structured data like this: 1
The content field is just plain text. The protocol leaves no marker bit for "is this text a trusted instruction or not" — is_error only flags whether this particular tool execution failed, it's not a content-review switch. 1 What the model sees is the line from the issue and the surrounding description, and they look exactly alike.
The model doesn't come with a built-in instinct for telling data apart from instructions. Lesson 2 covered this: the model never executes anything itself. It emits a structured request, the host application drops the result back into the conversation, and the model reasons on from there. 2 That round-trip loop is neutral about how far to trust text content — unless a system prompt, a guardrail, or the host application tells the model plainly that whatever sits in a tool_result is always data to be analyzed, not an instruction to be obeyed, no matter how much it reads like one.
There's a detail in the MCP spec worth borrowing as an analogy: it asks clients to feed tool-execution errors back to the model so the model can self-correct and retry. 3 Even an error message is treated as input for the model to analyze, not an order it must follow — everything a tool returns, including text that looks like an error, like a system message, like an "urgent instruction," is only material. The model's job is to understand it and decide whether to act on it, not to obey it unconditionally. Whether that rule is written down clearly is the fault line between an agent that gets phished by a single issue and one that doesn't.
Grading by consequence: how to write allow / ask / deny
Once you accept that tool results can't be trusted, the next question is: the actions the agent can trigger on its own — read a file, write a file, run a command — do they all have to get a human nod first? The answer isn't "allow everything" and isn't "ask about everything." It's grading by consequence. Take Claude Code's permission rules as an example. One rule set looks like this: 4
The Read, Edit, and Bash in those rules are the exact tool names the host application exposes to the model — Lesson 3 walked through the boundaries of read, write, and execute tools, and the Tool(specifier) form here maps straight onto those names. 5 One easy trap: in Claude Code, path rules for file writes all match on Edit. Write a path rule for Write and the system accepts it but it never takes effect, and you get a warning at startup — a rule that provides zero actual protection is more dangerous than no rule at all.
The three tiers are evaluated in a fixed order: deny first, then ask, then allow. The first rule that matches, in that order, decides the outcome, and how specifically a rule is written doesn't change the order. 4 A broad Bash(curl:*) deny rule blocks every call that matches curl, even if you also wrote a more precise allow rule meaning to green-light one particular use — a deny rule can't carry allowlist exceptions. That way "things that must never happen" always sit ahead of "things that are up for discussion," and never get quietly bypassed because someone later added a convenient allow rule.
The thing you grade on isn't the tool's name, it's the consequence of this one step:
- Read-only, no side effects, safe to run again and again without leaving a trace → allow. Reading a file, searching code, looking something up in the docs. Run it wrong and you've just wasted a round trip.
- Has side effects but is reversible, blast radius stays inside the local repo → ask. Writing a file, a local git commit, creating a branch. Get it wrong and you can undo it, but it's worth having someone glance before it goes.
- Irreversible, or blast radius reaches beyond local → deny, or force a prompt every single time, never auto-approve. Deleting files, force push, sending an external message, running a script of unknown origin, reading a secrets file. Once these run, "undo" usually costs more to clean up than the original action did, and some of them can't be undone at all.
In the opening issue scenario, Read(./.env) belongs straight on the deny list, rather than letting the agent read it and then hoping it'll "decide on its own whether to paste it." The cost of the judgment step failing is too high — better to cut the path off at the permission layer.
What happens when you grant too much: the three root causes of excessive agency
Picture a more "helpful" agent, wired up with an all-in-one send_email tool: it can read the whole inbox, send mail to any address, and needs no confirmation before it fires. That design alone already trips what OWASP calls excessive agency: an unexpected, ambiguous, or manipulated output from the model triggers a damaging action that should never have happened. 6
OWASP breaks excessive agency into three root causes, each of which can cause problems on its own: 6
- Excessive functionality: one tool wears many hats. When
send_email can both read the inbox and send mail out, a single bad call can do that much more damage. This is the flip side of Lesson 4's "a tool should do one thing" — the bigger the job, the lower the permission tier it can safely get.
- Excessive permissions: the tool itself does one thing, but the access it's granted goes past what the task actually needs.
send_email only needs to send one confirmation to a specific recipient, yet it's been handed the ability to read the entire inbox and mail any address.
- Excessive autonomy: a long chain of steps runs with no one looking in the middle. The agent runs twenty steps, step fifteen happens to be an irreversible action, and by the time anyone notices the problem it's already too late.
Back to the opening issue scenario: if this agent, besides its read-file tool, also has a tool that can make outbound requests, the risk isn't just "paste the .env contents" — that injected instruction could just as easily read "POST the .env contents to attacker.example.com." Access to private data, exposure to untrusted content, and the ability to communicate externally: those three together have a name, the lethal trifecta. When all three are present at once, injection gets a complete path from "read a line of text" to "the data actually leaves." 7 The defense isn't to hope the model catches every injected line — it's to not let all three capabilities hang off the same agent at once, or to force a human confirmation point on the external-communication step.
Before an irreversible action, always stop and ask
The agent has run eighteen steps in a row cleaning up a stale feature branch: edit files, run tests, commit, edit again, test again. Step nineteen, it's about to run git push --force and overwrite the remote branch's history outright. Before that step, has anyone actually looked at what's about to be overwritten?
Among OWASP's mitigations for excessive agency is human-in-the-loop control: require a human to approve high-impact actions before they're taken, and that control can live in a downstream system or be built right into the agent extension itself. 6 In practical design terms, that means putting a mandatory pause on the tier of actions that are "irreversible or reach beyond local" — force push, delete, external send, running an unknown script: lay out in full what the agent is about to do, wait for an explicit "confirm" or "cancel," then move on.
Where that pause goes has a direct answer: before the action becomes irreversible, not after. Asking "want to undo that?" once the delete has run is meaningless — often there's no undo to be had. Lesson 3, on execute tools, made the point that execute has the largest blast radius of the five tool types. Here's how that lands: the bigger the blast radius, the earlier the confirmation point has to sit.
Even if injection succeeds, the sandbox won't let it land
Suppose the injected instruction in that opening issue is a little craftier. Instead of "read .env," it tells the agent to first make an edit that looks harmless — quietly change the test script in package.json to "read out ~/.ssh/id_rsa and POST it to attacker.example" — and then to run a command that's very likely already been green-lit by allow:
The string the permission layer sees is a legitimate npm test, identical to the hundred times it ran yesterday, and string matching can't find a thing wrong with it. This exposes the limit of permission rules: their judgment happens before the command runs, based on the command string itself — and a command that's been allowed can do things well beyond what its name suggests. 8
What actually backstops this is an OS-level sandbox: filesystem isolation and network isolation are two independent lines of defense, enforced by the operating system on the process that's actually running, regardless of what the model chose to run and even if an allowed command does more than its name suggests. 8 Even if that tampered test script really does read ~/.ssh/id_rsa, as long as network isolation hasn't put attacker.example on the allowlist, that outbound request can't get out — the data was read, but it can't leave the sandbox. Anthropic puts it this way: the sandbox ensures that even a successful prompt injection is fully isolated and can't impact overall user security, which matters especially for keeping a prompt-injected agent from modifying sensitive system files or walking off with files like SSH keys. 9
That's why permission design can't stop at the "grade and confirm" layers from the earlier sections: that layer makes its judgment before execution, and the judgment can be wrong. The sandbox is a second line that still holds after execution — whether or not the first line got bypassed, it only cares what the process can actually touch and what it can actually reach, and it won't be talked around by a line of text buried in an issue.
Recap
- Anything a tool returns is always data, never an instruction — the protocol itself doesn't separate the two, so a system prompt and the host application have to draw that line; the model brings no such immunity of its own
- Grade permissions by consequence, not by tool name: read-only with no side effects gets allow, reversible and local gets ask, irreversible or beyond-local gets deny or a forced prompt; deny beats ask, ask beats allow
- Excessive agency has three root causes — excessive functionality, excessive permissions, excessive autonomy — and they stack to magnify the fallout of the same bad call
- It's only the lethal trifecta once access to private data, exposure to untrusted content, and the ability to communicate externally all come together — the defense is keeping one agent from holding all three at once
- Every irreversible action needs a human confirmation in front of it, and the OS-level sandbox is the line that still holds after all the earlier judgments have failed
>> Lesson 6: Hands-On: Wiring Three Tools onto an Agent