AI Agents

Least Privilege for AI Agents: Design Tools That Limit Damage

How to apply least privilege to AI agents: narrow tools instead of generic ones, scoped credentials, tenant checks in code, allowlists and limits that cap the worst case.

When people talk about AI agent security, they usually talk about prompt injection: a malicious instruction hidden in a web page or an email that convinces the agent to do something it should not. That threat is real, and we cover it in our article on agent security and prompt injection.

But defending against every possible manipulation is not a winnable game. Models misread instructions. Users phrase things ambiguously. New injection techniques appear constantly. The more reliable defence is to assume the agent will sometimes do the wrong thing, and make sure the wrong thing cannot be very bad.

That is the principle of least privilege: give the agent exactly the access its task requires and nothing more.

The blast radius question

For every agent, ask: if this agent did the worst thing its tools and credentials allow, what would happen?

An agent with a read-only search tool over public documentation has a tiny blast radius. An agent with a database connection string, a shell and an email API has an enormous one. The second agent might be just as well-behaved as the first in testing. The difference only shows on the bad day.

Least privilege is about shrinking that worst case before it happens, through the tools you give the agent, the credentials those tools use, and the limits around them.

Narrow tools beat generic tools

The single biggest decision is the shape of your tools. Compare two ways of giving a support agent access to orders.

The generic way is one tool: run_sql(query). It is quick to build and the model can answer any question with it. It can also drop a table, read every customer's data, or update prices, because SQL can do all of those things.

The narrow way is several small tools: get_order(orderId), list_recent_orders(limit), get_shipping_status(orderId). Each runs one fixed, parameterised query. The agent can do everything the support task needs, and nothing else, no matter what it is told.

Narrow tools have other benefits too. They are easier for the model to use correctly, because each has a clear purpose. They are easier to test. And they are easier to audit, because the log says "get_order 1042" rather than a 40-line query.

Generic tools like a shell or raw SQL have their place, but that place is a sandbox: an isolated container with no production credentials, where any command is acceptable because nothing important can be reached.

Enforce scope in code, not in the prompt

A system prompt that says "only access orders belonging to the current customer" is a request. The model will usually follow it. Usually is not good enough for data isolation.

The rule has to live in the tool. In particular, never take the identity of the user or tenant from the model's arguments. Take it from the authenticated session that started the run, and apply it inside every query.

// The model controls orderId. It does not control who is asking.
async function getOrder(input: { orderId: string }, ctx: RunContext) {
  const order = await db.orders.findOne({
    id: input.orderId,
    customerId: ctx.customerId,   // from the session, not from the model
  });

  if (!order) {
    // Same answer for "does not exist" and "belongs to someone else",
    // so the tool cannot be used to probe for other customers' orders.
    return { error: 'No order with that id on this account.' };
  }
  return summarise(order);
}

If the model is tricked into asking for order 9999, which belongs to someone else, the tool simply finds nothing. There is no instruction to obey or disobey. The capability does not exist.

In a multi-tenant system, put this check in one place that every tool passes through, rather than repeating it in each tool. A single choke point is much harder to forget when someone adds a new tool next year.

Scope the credentials too

Tools are code, and code has bugs. The credentials behind each tool are a second line of defence.

  • Read-only where possible. If an agent only reads from a database, connect with a read-only role. A bug that tries to write simply fails.
  • One identity per agent. Give each agent its own service account or API key instead of sharing one powerful key. Logs then show which agent did what, and you can revoke one without breaking the others.
  • Minimum API scopes. When connecting to a third-party service, request only the OAuth scopes the task needs. A calendar agent that reads availability does not need permission to delete events.
  • Short-lived tokens. Prefer tokens that expire in minutes or hours over keys that last forever. A leaked token that has already expired is harmless.
  • Keep secrets out of the context. The model never needs to see an API key. Tools hold credentials. The conversation holds only results. Anything in the context window can end up in an output or a log.

Our article on MCP server authentication covers how to apply these ideas when your tools are served over the Model Context Protocol.

Allowlists over blocklists

When a tool must accept open-ended input, such as a URL to fetch, a file path to read or a recipient to email, define what is allowed rather than what is forbidden.

  • A fetch tool that only reaches a list of approved domains, rather than one that blocks a few known bad ones.
  • A file tool confined to one project directory, with paths resolved and checked so that ../ cannot escape it.
  • An email tool that can only send to addresses inside your organisation, or to the customer who opened the ticket.

Blocklists fail open: anything you did not think of is allowed. Allowlists fail closed: anything you did not think of is refused, and someone has to make a decision to add it.

The fetch tool deserves particular care. An agent that can request any URL can be used to reach internal services that are not exposed to the internet, or to send data out to an attacker's server inside the query string. Restricting outbound destinations closes both doors.

Limits cap the worst case

Even a perfectly scoped tool can do harm if it runs a thousand times. Put numeric limits on anything with side effects:

  • A maximum number of messages per run, per hour, or per recipient.
  • A ceiling on refund or payment amounts, above which the action needs human approval.
  • A cap on records deleted or modified in a single call.
  • A budget on total tool calls and tokens per run, so a looping agent stops itself. See our article on agent cost management.

Limits turn a potential disaster into an incident. An agent that sends 20 wrong emails before hitting a cap is a bad afternoon. One that sends 20,000 is a much worse week.

Separate agents for separate jobs

It is tempting to build one capable agent with every tool. Splitting work across agents with different permissions is usually safer.

A common pattern is to separate reading from acting. A research agent with broad read access, including untrusted web pages and incoming email, gathers information and writes a summary. A second agent with write access works only from that summary, never from the raw untrusted content. An injection hidden in a web page can influence the summary, but it never reaches an agent that holds the power to act on it directly. Our article on the supervisor pattern covers how to structure agents that delegate to each other.

A checklist

Before giving an agent a new tool, ask:

  • Does the task actually need this? Could a narrower tool do it?
  • Does the tool take identity from the session, never from the model?
  • Do its credentials allow more than the tool does? Can they be narrowed?
  • Is open-ended input checked against an allowlist?
  • Is there a limit on how many times, or how much, it can act?
  • If it did the worst thing it is capable of, could we live with that?

The model will keep getting better at following instructions. Least privilege means you do not have to rely on that. It holds even on the day the instructions fail.

Related articles