Skip to main content

What it does

Agents tend to be granted more than they need, because it is easier to widen a grant than to work out the narrow one. Policy shrink goes the other way. It reads the authorization decisions recorded for an agent over a window, works out which grants were never used, and proposes a smaller permission set that still reproduces every call that was allowed. Nothing is changed for you. The functions read audit rows and return a report. You decide whether to apply it.
Terminal running the policy shrink example for least privilege. It lists the permissions an AI agent used and the ones it never used, then replays 496 recorded allows against the smaller policy: 493 still allowed and 3 newly denied.

Of 10 granted permissions, 5 were never used. Replaying 496 recorded allows, 3 would now be denied.

How it works

  1. analyzeUsage loads the agent’s effective permissions (its own, plus permissions delegated to it through active chains that have not expired) and splits them to one action each. It then reads the audit rows in the window and attributes every allowed call to the grant that authorized it, using the same matchResource and matchAction the engine uses, trying own permissions before delegated ones.
  2. suggestShrink turns that analysis into keep, remove and rewrites.
  3. verifyShrink replays the recorded rows against the proposal and reports what would flip. previewPolicyChange does the same by reading the rows itself, and works for any proposed change, not just a shrink.
Only allowed rows count as use. A call denied for another reason (rate limit, time window, approval, revoked agent) is not treated as proof that a grant is idle. If such a call matched a grant, that grant lands in attemptedOnly and stays in keep.

Run it

analyzeUsage returns granted, used (permission, count, firstSeen, lastSeen, and via with the delegation chain ids involved), unused, and an evidence block with row counts. Rows are read in pages of 1000 with a keyset cursor, so a long window does not load into memory at once. Set pageSize to change that.

Options for suggestShrink

A non-finite number (for example NaN from an unset environment variable) in any numeric option, or in maxExamples and pageSize, is ignored and the default is used. Output is deterministic and follows the order of the granted list.

Generalizing

With generalize on, mcp:github:issues, mcp:github:pulls and mcp:github:repos can become mcp:github:*. A merge happens only when every merged resource was used, when one of the agent’s existing grants already covers the wildcard (the result is never broader than what the agent has today), and when the wildcard matches no resource that was denied for lack of a grant. Even so, the wildcard admits siblings nobody has called yet, so each merge is listed in rewrites with its from permissions. Leave it off if you want the result to match observed use exactly. A * segment in a resource pattern matches that segment and everything after it, at any depth, and also the parent itself (tool:* matches tool, tool:a and tool:a:b). Replay uses the same matching as authorize().

Example output

This comes from examples/policy-shrink, an agent with seven grants (ten once split by action) and 498 recorded calls:
The run used minObservations: 5, so tool:db:users query, called three times, was dropped and shows up as newly denied. That is the report doing its job: it is the line you read before deciding. With the default of 1 every historical allow is kept.

Reading the report

Allowed calls that no current grant covers (an expired delegation, a removed permission) cannot be reproduced by a set built from today’s grants. They are listed in analysis.uncovered.

Delegated permissions

Delegated permissions are part of the agent’s effective set. When a call is attributed to a delegated grant, the chain id is recorded in via, so you can see which delegation to narrow instead of editing the agent. Expired and revoked chains are not part of the granted set.

Limits

  • It proves past behaviour only. It cannot know about calls the agent will make next month.
  • A rare but legitimate call (a quarterly job, an incident runbook) may fall outside the window and be proposed for removal. Review the remove list and the window before applying it, and widen since for agents with long cycles.
  • Matching covers action and resource. Constraints (rate limits, time windows, IP allowlists, approvals) depend on live state, so they are carried over on kept permissions but not re-evaluated in the replay.
  • Reading is bounded by the audit retention you configured. Rows already deleted are not evidence.
  • Overlapping grants are attributed the way authorize() picks them, first match wins. A broad grant listed before a narrow one gets the credit, and the narrow one shows as unused.
  • Audit rows only exist for decisions that were recorded. Check that auditAll was on for the window.
Last modified on October 17, 2026