Are You Actually Using Every Permission You Granted? Tightening Antigravity's Unified Permissions from Real Usage Logs
Once you flip a unified permission policy to 'allow everything,' unused grants quietly pile up. This is the grant-to-usage reconciliation loop: match granted permissions against your action logs, revoke what was never exercised, and narrow what's too broad — with working TypeScript and real numbers from solo operation.
One weekend I was reviewing my own agent settings and felt a small chill. Six months earlier, back when I couldn't yet predict how the agent would behave, I had waved through a set of permissions with "just allow all of it." They were still there. When I traced the run logs, several of them had never once been used. An open door, left open, for nothing.
We talk a lot about approval fatigue — too many dialogs to read. What unsettled me was the opposite. Because I had pre-approved everything, nobody (not even me) revisited the scope anymore. The absence of prompts had become the danger itself.
In this piece I want to build a mechanism for safely shrinking permissions you granted broadly in Antigravity 2.2.1's unified permission system, using your action logs as evidence. The key is to treat permission management not as a one-time setup but as a loop that keeps turning: grant, observe, reclaim.
The Failure on the Far Side of Approval Fatigue
Unified permissions consolidate the approval dialogs and per-MCP allowlists that used to be scattered around into a single policy. I covered the consolidation angle in how to fold your Antigravity permission settings into a single policy. But a new trap appears on the other side of that consolidation.
Once consolidated, granting happens once. And because it happens once, it stops getting reviewed. For an indie developer this bites harder — there's no colleague reviewing your config, so the hole a past version of you opened "just in case" never gets closed. You're widening your blast radius with no need to.
I think of this as the mirror image of approval fatigue. If approval fatigue is "too many prompts, so you stop reading them," this is "no prompts, so you forget they exist." The remedy is a mirror image too: consolidation for the former, and reclamation based on real usage for the latter.
Making Tightening a Grant → Observe → Reclaim Loop
Cloud permission tooling has a concept of surfacing grants that haven't been used within a window as "unused." I'm borrowing the same idea for local agent operation.
Phase
What you do
Signal
Grant
Early on, when behavior is unpredictable, allow slightly broadly
Priority: don't block the work
Observe
Keep logging the operations and paths actually used
Tool name, target path, timestamp
Reclaim
Join grants against real usage, surface unused ones as candidates
Observation window, run-count thresholds
The important thing is not to auto-apply the reclamation. What the reclaim phase produces is a proposal; a human confirms it and closes the gap. Since this rests entirely on the trustworthiness of your action log, it only works when paired with a design like keeping an audit log that can detect tampering.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You can mechanically detect 'silent over-provisioning' — the mirror image of approval fatigue — straight from your real usage logs
✦You'll have working TypeScript that joins your grant policy against action audit logs to surface unused grants and overly broad scopes
✦You'll be able to fold a weekly least-privilege maintenance routine into your own operation, with guards that stop you from tightening too early
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Data Model: Policy (Grants) and Action Log (Real Usage)
Start by typing the two inputs. The grant policy is a list of "which operations, up to which scope, are allowed." The action log is a time series of "what was actually done."
// permission-types.ts// Operation kinds. A minimal model for handling Antigravity's unified permissions locally.export type Verb = "read" | "write" | "exec" | "net";// One granted permission. scope is a glob (e.g. "src/**", "https://api.github.com/**").export interface Grant { id: string; // e.g. "write:src" verb: Verb; scope: string; // glob pattern grantedAt: string; // ISO8601}// One action the agent actually took (a single audit-log line).export interface Action { ts: string; // ISO8601 verb: Verb; target: string; // the path or URL actually touched runId: string; // identifies the run}export interface Policy { grants: Grant[];}
Keeping scope as a glob is what later lets you propose "narrow this over-broad grant." If you granted write:src/** but only ever touched things under src/config/, there's room to narrow.
Implementation: Joining Grants Against Real Usage
Read in the action logs for the observation window and, for each grant, tally whether it was actually used and — if so — which paths the usage clustered around. For glob matching I use picomatch, widely used in the Node ecosystem.
// reconcile.tsimport picomatch from "picomatch";import type { Grant, Action, Policy } from "./permission-types";export interface GrantUsage { grant: Grant; hitCount: number; // number of real actions matching this grant usedTargets: string[]; // distinct targets actually touched}// For each grant, tally real usage within the observation window.export function reconcile(policy: Policy, actions: Action[]): GrantUsage[] { return policy.grants.map((grant) => { const match = picomatch(grant.scope); const hits = actions.filter( (a) => a.verb === grant.verb && match(a.target) ); // Keep the distinct targets touched — the raw material for narrowing scope. const usedTargets = [...new Set(hits.map((a) => a.target))]; return { grant, hitCount: hits.length, usedTargets }; });}
The intent here is to delegate matching to the policy-side glob. The action log records raw paths honestly, and the decision logic lives entirely in the grant's scope. Concentrating the decision in one place means you can change the matching rules later without regenerating your action logs.
Reclaiming the Unused and Narrowing the Too-Broad
From the tally, build two kinds of proposal. One is "a grant never used = a revoke candidate." The other is "a grant that is used, but whose real usage fits within a much narrower path = a narrow candidate."
// suggest.tsimport type { GrantUsage } from "./reconcile";export interface Suggestion { grantId: string; kind: "revoke" | "narrow"; reason: string; proposedScope?: string; // for narrow: the scope after tightening}// Find the common prefix directory of the used paths (seed for narrowing).function commonDir(paths: string[]): string | null { if (paths.length === 0) return null; const split = paths.map((p) => p.split("/")); const head = split[0]; const out: string[] = []; for (let i = 0; i < head.length; i++) { if (split.every((s) => s[i] === head[i])) out.push(head[i]); else break; } // Drop the trailing filename segment; keep only the directory. return out.length > 1 ? out.slice(0, -1).join("/") : null;}export function suggest(usages: GrantUsage[]): Suggestion[] { const out: Suggestion[] = []; for (const u of usages) { if (u.hitCount === 0) { out.push({ grantId: u.grant.id, kind: "revoke", reason: "Never exercised within the observation window", }); continue; } const dir = commonDir(u.usedTargets); // Only propose narrowing when real usage is clearly tighter than the granted scope. if (dir && !u.grant.scope.startsWith(dir)) { out.push({ grantId: u.grant.id, kind: "narrow", reason: `Real usage stays under ${dir}/`, proposedScope: `${dir}/**`, }); } } return out;}
commonDir takes the common prefix directory of the paths actually touched and proposes narrowing scope to there. Stopping at the directory rather than the filename means that touching a different file in the same directory next week won't break anything. Over-narrowing just triggers so many re-requests that reclamation loses its point.
Guards Against Tightening Too Early
The biggest danger in this loop is tightening before you've observed enough. Revoke a permission that "just happened not to be used this month" and the agent stalls next month. So reclamation always sits behind two thresholds.
// guard.tsimport type { Suggestion } from "./suggest";export interface GuardConfig { minRuns: number; // observe at least this many runs before deciding minWindowDays: number; // observe at least this many days before deciding}export function applyGuard( suggestions: Suggestion[], observedRuns: number, windowDays: number, cfg: GuardConfig): { ready: boolean; suggestions: Suggestion[]; note: string } { if (observedRuns < cfg.minRuns || windowDays < cfg.minWindowDays) { return { ready: false, suggestions: [], note: `Under-observed (${observedRuns} runs / ${windowDays} days). Holding until thresholds (${cfg.minRuns} runs / ${cfg.minWindowDays} days).`, }; } return { ready: true, suggestions, note: "Sufficiently observed. Presenting proposals." };}
In my own solo operation I started with minRuns at 100 and minWindowDays at 21. My work is mostly scheduled runs, so the run count fills up first. If yours is a setup you only reach for manually and irregularly, weight the days side more heavily instead. This is a value you tune to your operating rhythm; there isn't one right answer.
While observation is still thin, don't tighten — keep running as is. At this stage, not blocking the work matters more.
Running It Weekly: An Actual Reduction
In my own operation I fold this reclaim loop into a weekly check. Running several sites solo under Dolice, I tend to accumulate more holes opened for the agent, and without periodic re-tightening they spread fast. Here's a real month:
Metric
Before
After
Total grants
23
14
Unused grants (revoke candidates)
9
0
Over-broad scopes (narrow candidates)
5
1
Observed: runs / window
140 runs / 28 days
Nine of the 23 had not been exercised once across 28 days and 140 runs. Most were net and exec grants I had opened early on "in case I need them." Reclaiming them didn't stall the agent, and over the next two weeks of re-observation there was exactly one re-request. Seeing it as numbers made plain just how broadly I'd been opening things — a little embarrassing, honestly.
I keep the step where a human eyes the proposals. Once, a permission used only by an end-of-month batch was sitting among the revoke candidates, and it only surfaced once observation spanned the month boundary. Had I auto-applied, it would have quietly broken at month-end. For the design of the permission boundary itself, safely handing write access to an Antigravity agent is a useful reference.
Wrapping Up
Start by writing out, on one page, the permissions your agent currently holds, and join them against your recent action logs. If even one "never used" grant turns up, this reclaim loop is worth running. Reducing grants is not reducing capability — it's quietly handing back the keys you aren't using, one at a time.
Permission management is an unglamorous corner, but the longer you run automation safely as a solo developer, the more this dull re-tightening pays off. I'm still feeling out my own observation thresholds by hand. If you're juggling several automated jobs too, I'd be glad to keep searching for the right balance alongside you.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.