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.
Of 10 granted permissions, 5 were never used. Replaying 496 recorded allows, 3 would now be denied.
How it works
analyzeUsageloads 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 samematchResourceandmatchActionthe engine uses, trying own permissions before delegated ones.suggestShrinkturns that analysis intokeep,removeandrewrites.verifyShrinkreplays the recorded rows against the proposal and reports what would flip.previewPolicyChangedoes the same by reading the rows itself, and works for any proposed change, not just a shrink.
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
Withgeneralize 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 fromexamples/policy-shrink, an agent with seven grants (ten once split by action) and 498 recorded calls:
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 invia, 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
removelist and the window before applying it, and widensincefor 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
auditAllwas on for the window.
Related
- Tutorial: find and remove unused agent permissions
- Agent accountability: where permissions, approvals and audit fit together.
- Tamper evident audit trail: the decision rows that
analyzeUsagereads. - Delegation: delegated permissions count as granted.
- Taint aware tool guard: limit what a call can do once the permission is kept.