# Least privilege for AI agents, in practice

> Everyone agrees on least privilege for AI agents. Very little is written about what the rules look like, which one wins when two disagree, and how long a change takes to bite.

**TL;DR** Least privilege for agents comes down to four mechanisms: a role that is a complete persona rather than a pile of toggles, restrictions that name either a whole connector or one individual tool, an override that replaces a role's rules instead of layering on them, and frozen arguments that the model never sees and cannot pass. The trap that catches people is an allow rule naming nothing, which denies everything.

Least privilege for AI agents is the least controversial idea in the category and one of the least specified. Everybody agrees an agent should have only the access it needs. Very little gets written about the actual mechanics: what a rule looks like, what happens when two of them disagree, and how long a change takes to bite.

This is that, for agents specifically, using the four mechanisms we ended up with. The examples are ours, but the reasoning applies to whatever you are configuring.

## Start by deciding what the agent is

The first design decision is not a rule at all. It is whether the agent has an identity of its own.

Give an agent its own account and you have created a new employee who never leaves, whose access grows every time somebody needs it to do one more thing, and who appears in no offboarding process because nobody offboards software. The access it accumulates is nobody's in particular, which means nobody reviews it.

The alternative is that the agent has no access of its own and always acts for a named person. Every call resolves against that person's current role. There is no standing grant to review because there is no standing grant.

That choice determines everything else. The rest of this post assumes the second, because the first does not really have a least-privilege story, it has a service-account story.

## Mechanism one: roles as complete personas

A role is a named set of permissions, and in our model **a member holds exactly one**.

That constraint is doing more work than it looks like. If roles stacked, then what somebody can do is the union of several documents, and the question "what does this role mean" becomes unanswerable without also knowing who holds it alongside what. Debugging effective permissions turns into set arithmetic across an unbounded number of grants, which is how most permission systems become unauditable.

One role per member forces each role to be a complete description of a job. The built-in ones form a strict chain, each tier being the tier below plus more:

    Guest  <  Member  <  Team Admin  <  People Admin  <  Org Admin  <  Org Owner

Each tier holds everything the tier to its left holds, plus more. The chain is built by construction rather than written out by hand, so that containment cannot drift as permissions are added. Two roles sit deliberately outside it, because they are not more or less of anything: **Billing Admin**, who manages the subscription and has no product access at all, and **Auditor**, who can see everything and change nothing.

Both of those, along with Guest, are free seats. That is a small decision with a real effect. Charging for the compliance reviewer's account is a good way to end up with fewer compliance reviewers, and a read-only role that costs nothing is one that actually gets handed out.

Two more details worth stealing.

**Org Admin is defined by subtraction.** It is every permission except deleting the organization and managing billing. Defined that way rather than enumerated, a newly added permission lands in the admin role automatically and nobody has to remember to add it.

**Deleting the organization is the owner's alone**, and the reason is written down: an attacker who lands an administrator account can do a great deal of damage, but should not be able to delete the evidence along with the workspace.

## Mechanism two: restrictions, per connector and per tool

Roles govern what somebody can do to the system. Restrictions govern which of the connected systems they can reach, and which individual operations inside them.

A rule targets **a role or a specific person**. It does not target the organization as a whole, because there is nothing there to target: the default is the absence of any rule, which permits everything. A rule you want everybody to be under is written against every role.

Each rule is either an **allow** or a **block**, and it names connectors, individual tools, or both.

Blocking a connector is the blunt instrument and is often the correct one. If the marketing team has no business in the payments system, blocking the connector says exactly that, in one rule, in a way that survives somebody adding new tools to that connector later.

Blocking a tool is how you express the more common shape: this team needs this system, but not all of it. Support can read tickets and cannot delete them. Finance can look at invoices and cannot issue refunds.

### The trap

Here is the thing that catches people, stated as plainly as we can:

**The allowlist engages on the presence of an allow rule, not on its contents.**

The moment an allow rule exists for a target, anything it does not name is denied. That is the point of an allowlist, and it is what people expect once they think about it. What they do not expect is the degenerate case. An allow rule that names **no connectors and no tools** denies everything. It is the strictest rule the system can express.

It is also what you get if you create an allow rule in the interface and save it before adding anything to it. We have watched that happen. The rule is doing exactly what it says; it just does not say what the person thought.

Two other combinations are worth keeping in mind. Blocks always beat allows, so naming something in both is a block. And allow rules union with each other, as do blocks, so adding a second allow rule widens rather than narrows.

### Why rules bind operations rather than names

One subtlety that matters more for custom connectors than public ones.

A rule is written against a connector and a tool name, which is how a person thinks about it. Internally, the canonical operation behind that name is pinned at the moment the rule is written. A **block** matches on the name or on the pinned operation. An **allow** matches only on the pinned operation.

The asymmetry is deliberate. A tool's advertised name can be changed by whoever edits that connector's documentation, so the name is a token controlled by the governed party. Someone who can edit a connector could swap two names and have a different operation answer to an allowed one. Blocks are a fail-closed net and match on either signal, since both only ever add refusals. Allows match the operation only, because that is the thing that cannot be renamed underneath them.

The short version: **governance binds the operation, never the label.**

## Mechanism three: an override that replaces rather than layers

One person needs an exception. They are in a role that cannot reach the billing system, and for two weeks they can.

The obvious design is to layer a personal rule on top of the role's rules. This turns out badly. The effective policy for that person is now the merge of two rule sets under precedence you have to look up, and the person writing the exception cannot see the result from either screen.

So a rule targeting a person **replaces** the role's rules for them, completely. Precedence is a simple ladder with no merging across levels:

    person-level rule   >   role-level rule   >   no rule at all (allow)

The cost is that an exception is more work to write, because it has to restate what still applies rather than just what changed. That is the right cost. It means the person-level rule is a complete statement of what this person can reach, readable on its own, and the question "why can Priya reach this" has one answer rather than two that have to be combined.

There is also a guard on writing these at all: you cannot create or edit a rule that constrains you, unless every rule currently applying to you is one you wrote yourself. Otherwise the first move of anyone with the permission would be to exempt themselves.

## Mechanism four: freeze the arguments

Roles and restrictions decide which tools. Frozen parameters decide what those tools are allowed to be called with, and it is the mechanism people reach for last and then use constantly.

A frozen parameter pins an argument to a fixed value on a specific tool in a specific toolbox. A file-search tool frozen to one folder. A ticket-creation tool frozen to one queue. A reporting tool frozen to one account id.

Two things happen, and both are necessary.

**The argument is stripped from the advertised schema.** The model never sees that the parameter exists. It cannot be asked to set it, cannot hallucinate a value for it, and cannot be talked into changing it, because as far as the model is concerned there is no such field.

**The value is merged over whatever came in.** If a client passes the key anyway, the frozen value wins. Hiding a field is a usability measure; overriding it is the security measure. Doing only the first would mean the protection lasts exactly as long as nobody looks at the wire format.

Full precedence on the arguments of any call:

    entry defaults   <   what the model passed   <   frozen parameters

This is also the cheapest way to build a safe version of a dangerous tool. Rather than blocking the file search outright, freeze it to the folder that team should see and share that. They get a working tool. They cannot point it anywhere else.

## How long a change takes

Every product in this category will tell you access changes take effect right away. Here is ours, honestly, because the two halves are different and knowing which is which changes what you do in an incident.

**Effective on the next call, with no cache:**

- Revoking a client's authorization
- Removing somebody from the organization
- Suspending somebody

Removal and suspension revoke every live authorization in the same transaction as the membership change, so there is no window where the membership is gone and a token still works.

**About two minutes:**

- Changing somebody's role
- Adding, editing or deleting a restriction

These resolve through a short cache, and the propagation is what it is. Two minutes is fine for governance work, which is what those changes are for. It is not fine for "stop this agent now", which is what the first group is for.

We would rather write that down than round it off. If a vendor tells you every change everywhere is instantaneous, ask them what is in front of their permission lookup.

## Reviewing it later, which is when it matters

Every access model decays. The question is whether yours decays visibly.

Three habits keep this one readable, and each of them is a consequence of a
design decision above rather than a process you have to bolt on.

**Read the roles, not the members.** Because a person holds exactly one role,
reviewing access means reading a handful of role definitions rather than
reconciling grants per person. A company with two hundred people has perhaps
eight roles. Reviewing eight documents is a meeting. Reviewing two hundred grant
sets is a project that gets deferred.

**Treat person-level rules as the exception list.** They replace a role's rules
rather than layering on them, which makes them self-contained and therefore
readable, but it also makes them easy to forget. Anything written as a
person-level rule should have a reason and a date attached in whatever tracker
you use, because it is the one part of the model that does not get reviewed by
reading the roles.

**Watch the refusals, not just the successes.** A refused call is a signal, and
it points at one of two things. Either a rule is wrong and somebody legitimately
needs something they cannot reach, in which case the refusal is your requirements
document. Or somebody is repeatedly trying something they should not, in which
case you want to know that too. Both are invisible in a trail that records only
what succeeded.

There is a fourth habit that is less a habit than a periodic reality check: have
someone in each team open their client and read the tool list they are handed.
The configuration says what you intended. The tool list says what they got. Those
two diverge slowly and quietly, usually because a connector gained tools on a
catalog update and a block rule written at the tool level did not cover the new
ones. A connector-level rule would have. That is the argument for blocking at the
system level whenever the honest answer is that this team has no business in that
system at all.

## Least privilege for AI agents, assembled

A support team that should read tickets, look at customer records, and change neither:

1. **Role.** Member. They need to run tools and use shared connections, not manage people or write governance.
2. **Connector restriction.** Block the connectors they have no business in at all. The payments system and the HR system are not part of this job.
3. **Tool restriction.** On the helpdesk connector, block the delete and merge operations. Reading and replying stay.
4. **Frozen parameters.** On the shared search tool, freeze the workspace to the support workspace.
5. **Check it from the other side.** Have someone on that team open their client and look at the tool list. A restricted tool is never advertised, so the list is the answer.

Step five is the one people skip and the one that finds the mistake. The tool list a person actually receives is the ground truth about what they can reach, and it takes thirty seconds to look at.

## Where to go next

The rules above decide what can happen. [What an AI audit log has to capture](/blog/what-an-ai-audit-log-must-capture/) covers what gets written down when it does. [Offboarding someone who left an agent behind](/blog/ai-agent-offboarding/) covers the case these mechanisms do not reach on their own, which is a person leaving while a shared connection still points at their credentials. If you are still choosing a product, [MCP gateways compared](/blog/mcp-gateway-comparison/) sorts the category by whether it can express any of this. The [connector catalog](/connectors/) lists the tools each connection exposes, which is the vocabulary you write restrictions in, and [what teams do with Elaichi](/use-cases/) shows the shapes these rules usually take per team.

## FAQ

### Can somebody hold two roles at once?

No, exactly one. That is a deliberate constraint rather than a limitation: if roles stacked, the effective permissions of any given person would be the union of several documents and nobody could answer what a role means without also knowing who holds it. One role per member means every role has to be a complete persona, and it means the answer to what somebody can do is one lookup.

### What is the difference between blocking a connector and blocking a tool?

Scope, and how you should use them. Blocking a connector removes an entire system from somebody's reach, which is the right move when the answer is that this team has no business in that system at all. Blocking a tool leaves the system reachable but removes one operation, which is how you express that support may read tickets but not delete them.

### If I write an allow rule, is everything else still allowed?

No, and this is the single most important thing to understand. The allowlist engages on the existence of an allow rule, not on its contents. Once you write one, anything it does not name is denied. An allow rule that names nothing at all therefore denies everything, which is the strictest rule you can express and almost never what somebody meant to write.

### How long does it take for a change to take effect?

About two minutes for a change to a role or a restriction, which resolve through a short cache. Revoking somebody's grant, removing them from the organization or suspending them is effective on the next call, with no cache in front of it. When you need an agent to stop reaching something now, use one of the second group.

### Does a compliance reviewer need a paid seat to check any of this?

No. There is a read-only Auditor role that costs nothing, with visibility across the organization and no ability to change anything or run any tool. Guest and Billing Admin are free seats too. Making oversight cost money is a good way to have less of it.
