Least privilege for AI agents
Agents should receive the smallest set of explicit capabilities necessary to complete their jobs.
Least privilege is the oldest idea in security. It is also the one agent development has most comprehensively skipped, because the fastest way to make an agent work is to hand it your credentials.
The thesis
There is a specific mistake that almost every agent deployment makes, and it is not carelessness. It is a category error:
We give agents accounts when we should give them capabilities.
An account is a bundle of authority assembled for a human, who has judgement, accountability, a career, and a manager. A capability is a specific permitted action. Humans get accounts because enumerating everything a person might legitimately need is impossible.
None of that reasoning applies to an agent. An agent's job is narrow and known in advance. It has no judgement to exercise on the edge cases, and no career to risk. Enumerating what it needs is not only possible. It is easy, and skipping it is what turns a bug into an incident.
An agent given an account inherits every permission that account has ever accumulated. An agent given capabilities has exactly what somebody decided it should have.
Why accounts are the wrong unit
Concretely, here is what "give the agent my token" means.
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${env:GITHUB_TOKEN}" }
}
}
}
That looks careful. The token is referenced, not hardcoded. It is exactly what the documentation says to do.
If GITHUB_TOKEN is a classic personal access token with repo scope, that
agent can read every private repository you have access to, including your
employer's, including ones you forgot you were added to. It can force-push. It
can delete branches. It can read Actions secrets. It can create a deploy key,
which is a persistence mechanism that survives the token being revoked.
Nothing in the configuration says any of that. The blast radius is entirely a property of a credential that lives somewhere else.
Three specific problems with account-level credentials:
They accumulate. Your account has permissions from three jobs ago on that repository nobody archived. You do not know what they are. The agent gets all of them.
They are ambient. Once the token is in the environment, every tool on that server can use it. Adding a tool adds reach with no new grant.
They are indistinguishable in audit logs. When something goes wrong, the log says you did it. Legally and organisationally, that is true.
The capability classes
Least privilege needs a vocabulary, and "permissions" is too fine-grained to reason about. Ten classes, which is what Gate normalises every tool onto:
- Name
read- Type
- observe data
- Description
Not safe. See below.
- Name
search- Type
- discover data
- Description
Strictly more than read: it finds data nobody pointed at.
- Name
write- Type
- modify state
- Description
Recoverable, usually, if you notice.
- Name
communicate- Type
- send outside the system
- Description
The channel through which everything else becomes someone else's.
- Name
delete- Type
- destroy state
- Description
The class where "we noticed and fixed it" stops being available.
- Name
identity- Type
- change who can act
- Description
Create accounts, reset passwords, issue credentials.
- Name
admin- Type
- change the rules
- Description
IAM, policy, ownership, branch protection, webhooks.
- Name
secrets- Type
- read credential material
- Description
Puts credentials into a context window.
- Name
execute- Type
- run arbitrary code
- Description
Not a capability. A capability generator.
- Name
financial- Type
- move money
- Description
Converts a software mistake directly into money leaving.
The value of the vocabulary is that it survives translation. write_file,
fs.put, edit and apply_patch are the same grant. Once you think in
capabilities you can review an agent that uses tools you have never seen.
Read is not safe
The most common mistake after "give it my token" is "read-only is fine".
Read-only is fine right up until the agent can also communicate. Then read-only means "everything I can see, an attacker can have", because a prompt injection does not need write access to exfiltrate. It needs a read and a channel.
There is a second, subtler problem. search is worse than read, and they get
treated the same. A read tool gets what you point it at. A search tool finds
things nobody knew were there: the credential in the old commit, the customer
list in the wiki page from 2023, the incident doc with the password in it. It
turns "the agent has access to the repository" into "the agent has access to
everything anyone ever put in the repository".
Practical rule: scope reads to what the job needs, and treat any agent that can both read and communicate as capable of disclosing everything it can read.
Write, delete, and reversibility
Write and delete are usually granted together, and they should not be. The question that separates them is can you undo this?
- A commit is reversible. A force-push over an unreplicated branch is not.
- An updated row is reversible if you have point-in-time recovery. A dropped table is not, and you will find out which you have during the incident.
- A Slack message is technically deletable and practically permanent, because people read it.
Reversibility is what turns an incident into an outage, and it is almost never in anyone's threat model. Ask it explicitly for every write-class tool.
Where a platform offers soft delete, use it. Where it does not, a credential without delete permission is worth the configuration effort.
Execute is not a capability
Arbitrary execution (a shell tool, a code interpreter, execute_query,
container exec) is not one capability among ten. It is the ability to
manufacture the other nine.
An agent that can run sh -c can read anything the process can read, write
anything it can write, delete anything, and open a socket to anywhere. Whether
you also gave it a delete tool is a detail.
This is why "we only gave it a shell for convenience" is the single most expensive sentence in agent configuration. If you grant execution, you have granted everything the process can reach, and your least-privilege work now has to happen at the process boundary (a container, a sandbox, a service account) rather than at the tool boundary.
If you can replace it, replace it. Three parameterised queries instead of
execute_query. Two named commands instead of a shell. This is the expensive
change and the one with the largest effect.
Financial, identity, admin
Three classes deserve a different default: human approval, always.
Financial. A refund tool reachable by a prompt-injected agent is a way for an attacker to be paid. There is no threshold at which unattended agent payment authority is a good default. Use restricted keys, set provider-side amount limits, and keep financial tools on a server with no other capabilities, particularly not execution.
Identity. An agent that can create accounts or issue credentials can create persistence that outlives the incident. Revoking the agent's token does not revoke the account it made.
Admin. This is the class that lets an agent change its own limits.
Everything else in a review describes what the agent can do today; admin
describes what it can decide to be able to do tomorrow. It is also the class
most likely to disable your other controls. Branch protection, required reviews,
audit webhooks and secret scanning are all administrative settings.
Short-lived and scoped
Two properties that do more than any tool-level control:
Scoped. A credential that can only touch what the job needs bounds every tool at once, including the ones added next month. This is the highest-leverage change available: one credential edit, whole-server effect.
Short-lived. A token that expires in an hour is a token whose leak has a deadline. Prefer OAuth flows with refresh over long-lived personal access tokens; prefer workload identity over static keys; prefer per-session credentials over per-installation ones.
In descending order of preference:
- Short-lived, scoped, per-session credentials (OAuth with narrow scopes)
- Long-lived but tightly scoped (fine-grained tokens, read-only DB roles)
- Long-lived and broad: the thing you are trying to stop doing
Approval boundaries
Least privilege is about what the agent can do. Approval is about what it can do unattended, and it is the control everything else quietly assumes.
Draw the line at consequence and reversibility, not at frequency:
| Approve automatically | Require a human |
|---|---|
| Reads within scope | Anything destructive |
| Writes to a branch | Anything financial |
| Searches | Anything that changes permissions |
| Draft outputs | Anything sending externally to a new recipient |
| Anything against production |
The failure mode to design against is approval fatigue. Twenty prompts an hour trains people to click yes, which is worse than not asking. Keep tools coarse where they are safe and fine where they are not, so the prompts that appear are the ones worth reading.
And keep auto-approval out of committed configuration. It is switched on to get through a tedious session, and it lands in a shared settings file forever. GATE019.
Doing it
A practical sequence for an agent you already have.
- Enumerate.
npx @usegate/cli scan. What does it currently have? - Ask what it needs. Not what it uses; what its job requires. The gap is usually large.
- Fix the credential first. One change, whole-server effect. Fine-grained token, read-only role, scoped key.
- Fix the filesystem scope. Usually one argument.
~to./projectremoves several findings at once. - Remove destructive tools you cannot justify. Ask what breaks. Usually nothing.
- Break the dangerous combinations. Splitting
secretsandcommunicateacross two agents is often cheaper than hardening either. - Record a baseline so nothing widens without somebody noticing.
gate scan --write-baseline
Least privilege is not something you finish. The baseline is what keeps it from slipping back.