Topic 10.7
Least Privilege for Tools
In one line
A model that can call tools can be steered into calling them. Give every feature only the tools it needs, run them with narrow credentials, make dangerous ones need a human yes, and decide in your code, never in the model's reply.
Think of it like this
A hotel gives the cleaner a key that opens the rooms on one floor, gives the receptionist a key to the front desk drawer, and keeps the master key and the safe in the manager's office. If a cleaner is fooled by a stranger in a uniform, the damage is one floor. Nobody expects the cleaner to be impossible to fool; the keys are cut so that being fooled is survivable.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Least privilege
- Give each part of a system only the access it needs for its job, and nothing more.
- Scope
- A named permission such as read or write that a session or a token holds.
- Excessive agency
- A model-driven system that can do more than its job needs (too many tools, too much power, too much autonomy).
- Deny by default
- Refuse anything that is not explicitly allowed.
- Human in the loop
- A person must approve an action before it runs.
- Blast radius
- How much can go wrong if one component is fooled or broken.
Step by step
01Step 1: list the tools and what they need
Three tools: read_note needs the read scope and no approval; send_email needs write and a human approval; delete_note needs admin and approval. The table is data, so a reviewer can read the whole policy in ten lines.
TOOLS = {
"read_note": {"scope": "read", "needs_approval": False},
"send_email": {"scope": "write", "needs_approval": True},
"delete_note": {"scope": "admin", "needs_approval": True},
}02Step 2: put the decision in a gate
gate runs between the model and the tool. It checks, in order: is the tool known, does the session hold the scope, does it need approval and has a human given it. The answer is one of allowed, waiting or denied. The model has no say in it.
03Step 3: run three sessions
The same four requests from a steered fake model go through three sessions. The read-only session can read and nothing else. The support session can read, and its email waits for approval; deleting is denied because it lacks the admin scope. In the third session the human has approved the email, so it runs.
04Step 4: see that the model is not in charge
The fake model asks for format_disk too. It is not in the table, so it is denied in every session. New tools are unavailable until a developer lists them: the safe default.
05Step 5: add the second wall
Behind the gate, the email function should use a credential that can only send from one address, and the note function a database user that can only read one table. If someone changes the gate later and breaks it, the credential still limits the harm.
Try it yourself
- 1
Predict the grid
Write down the twelve results before running. Which tool is denied in every session, and why?
- 2
Add a tool
Add a tool
search_webtoTOOLSwith scopereadand no approval, and add it torequests. Which session lines change? Then think about what a web page that the tool returns could do to your prompt (Topic 10.2). - 3
Approve the wrong thing
The approval set holds tool names. Change it to hold call IDs plus arguments, as a real system should. Why can a name-only approval be abused when the model repeats the call with different arguments?
Code & diagrams
Nothing is really sent or deleted. The point is the decision, made in code, whatever the model asks for.
Expected output
read-only session:
read_note -> allowed
send_email -> denied: session has no write scope
delete_note -> denied: session has no admin scope
format_disk -> denied: unknown tool
support session:
read_note -> allowed
send_email -> waiting: needs human approval
delete_note -> denied: session has no admin scope
format_disk -> denied: unknown tool
support session, email approved:
read_note -> allowed
send_email -> allowed
delete_note -> denied: session has no admin scope
format_disk -> denied: unknown toolBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Let the model skip approval
Change "send_email" in TOOLS to "needs_approval": False, as if a developer wanted fewer clicks. Run it and read the support-session lines.
Myth vs fact
Myth
A system prompt saying 'only use tools when the user asks' is enough.
Fact
It is a request to the model. A steered model can ignore it. The gate in your code must enforce the rule.
Myth
Approval prompts make every action safe.
Fact
People click through. Show exactly what will happen, ask only for risky actions, and combine with limits and logs.
Myth
Read-only tools are harmless.
Fact
A read tool can still expose private data, and its output becomes input to the model. Scope reads to what the user may see.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Split reading and acting: a first step reads untrusted content with no tools and returns a validated structure; a second step has tools but only sees that structure. Injected text cannot reach the acting step.
- ▸
Tie each approval to a call ID, the exact arguments and a short expiry. Re-check the arguments at run time: what was approved is what runs.
- ▸
Add budgets (per-session calls, daily spend, outbound emails per hour). A steered loop is stopped by the cap even if every single call looks legitimate.
- ▸
Act with the end user's own delegated permissions where you can, not a shared service account that can reach everyone's data: a steered model should be able to do no more than that user could. Third-party tools, plugins and MCP servers (Topic 7.8) are supply-chain risk (OWASP LLM03): their descriptions and outputs are untrusted text, so pin versions, review what they can reach and give them their own narrow credentials.
- ▸
Audit trail: log who asked, what the model proposed, what the gate decided, who approved, and what ran. In an incident this is how you tell a prompt problem from a gate problem (Phase 11).
Remember this
- 1
Why this exists. Topic 10.2 showed that a model can be steered by text it reads. If a steered model holds a powerful tool (send email, delete files, spend money, run code), the attacker now has that power. OWASP calls this Excessive Agency (LLM06 in the 2025 list): giving an LLM system more functionality, permissions or autonomy than it needs. You cannot guarantee the model is never fooled, so you limit what a fooled model can do.
- 2
Three kinds of excess. Too many tools (a summariser that also has a send-email tool), too much power per tool (a database tool that can run any SQL instead of one fixed read query), and too much autonomy (actions that run without a human even when they are irreversible). Review each one for every feature: list the tools, the exact operations each allows and who approves them.
- 3
Rule 1: scope per feature and per session. The tool list a model sees is chosen by your code per request. The reading feature gets read tools; a support session gets what a support agent could do; an anonymous visitor gets almost nothing. The example's gate looks up the tool, checks that the session holds the scope the tool needs (
read,write,admin) and denies the rest. An unknown tool name (format_disk) is denied by default: deny unless listed. - 4
Rule 2: narrow credentials underneath. The code behind a tool should use credentials that can do only that tool's job: a read-only database user, an API token scoped to one project, a file path restricted to one folder. Then even a bug in your gate, or a tool called with odd arguments, runs into a second wall that your gate does not control. Never give a tool the same all-powerful key your server uses.
- 5
Rule 3: validate arguments. Recall from Topic 7.7 that read tools and write tools differ in risk. The model chooses the arguments, so they are untrusted (Topics 7.4 and 10.3): check that IDs belong to the signed-in user, recipients are in an allowed list, amounts are within limits, paths stay inside a folder. In the Playbook, the e-mail tool also checks the recipient's domain.
- 6
Rule 4: a human says yes to dangerous actions. For anything irreversible or costly (sending to outsiders, deleting, paying), the tool call becomes a proposal: the app shows the user exactly what will happen ('Send this text to bo@example.com?'), and runs it only after a click. The approval must be for the exact arguments, tied to the call ID, so the model cannot swap them after the click. Add rate limits and daily caps as a final guard against loops. Log every decision with who approved it.
Explain it without notes
What is excessive agency and how do you reduce it?
Why is checking tool arguments necessary even when the tool name is allowed?
Describe a good human-approval design for an email-sending tool.
Why use narrow credentials in addition to a gate?
Practice
Write allowed_tools(feature) that returns the tools a feature may use from a dict like {"summarise": ["read_note"], "support": ["read_note", "send_email"]}; an unknown feature gets an empty list. Print the result for support and for unknown.
Write valid_recipient(address, allowed_domains) that returns True only when the address has exactly one @ and the text after it is in allowed_domains. Test it with ana@example.com, eve@evil.test, and ana@example.com@evil.test.
Trade-offs
- ↔
Approval for every action is the safest and makes the product annoying; approve only risky ones and raise limits for trusted users over time.
- ↔
Narrow credentials need more setup (many tokens, many roles) and make a whole class of damage impossible.
- ↔
Splitting a feature into read and act steps costs an extra call and code, and gives the strongest protection against injection.
Done when you can
Done when you can list the tools of a feature and the exact operations each allows.
Done when you can write a deny-by-default gate with scopes and approval.
Done when you can explain why arguments must be validated and approvals tied to exact arguments.
Done when you can name the credential under each tool and why it is the narrowest possible.