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:

  1. Short-lived, scoped, per-session credentials (OAuth with narrow scopes)
  2. Long-lived but tightly scoped (fine-grained tokens, read-only DB roles)
  3. 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 automaticallyRequire a human
Reads within scopeAnything destructive
Writes to a branchAnything financial
SearchesAnything that changes permissions
Draft outputsAnything 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.

  1. Enumerate. npx @usegate/cli scan. What does it currently have?
  2. Ask what it needs. Not what it uses; what its job requires. The gap is usually large.
  3. Fix the credential first. One change, whole-server effect. Fine-grained token, read-only role, scoped key.
  4. Fix the filesystem scope. Usually one argument. ~ to ./project removes several findings at once.
  5. Remove destructive tools you cannot justify. Ask what breaks. Usually nothing.
  6. Break the dangerous combinations. Splitting secrets and communicate across two agents is often cheaper than hardening either.
  7. 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.

Was this page helpful?