agentleFS
Plexito-dePlexito-de/honed-skills/skills/drive-audit/SKILL.md

drive-audit

Skill3 starsChanged 21 days agoApache-2.0View on GitHub

Use when anything in Google Drive might be shared that should not be, or when a share has to be removed, reviewed or approved. Fires on "who can see this", "stop sharing", "make it private", "anyone with the link", "public", "shared with", "unshare", "revoke access", "sharing audit", "Freigabe", "nicht mehr teilen", "nur für mich". Owns the drive-audit CLI (scan, folder, revoke, downgrade, reparent, policy), how to read each finding class, what counts as an approved share, and how to keep a policy file so the next run is a diff rather than a re-read.

  • Installs packages

1 other project uses a version of this.

Adopted by

Most recently taken up by

  • Other versions1 project uses a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 1

    1. Thisisily/momputer★ 2same name

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • In this repository9 other files it gives its agents.

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 3 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Create a project at console.cloud.google.com.

    line 123
  2. Distinctive:

    Create an OAuth client ID of type Desktop app, and download the JSON.

    line 127
  3. Distinctive:

    Not an error, and the exit code will not be 1.

    line 242
  4. Very specific:

    It tries the plain delete first: Only a grant Drive refuses with cannotDeletePermission moves its item, so an item revoke could have …

    line 257

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

drive-audit: who can see what in Drive, and which of that was approved

The finding that made this exist

A folder in My Drive reported shared: false with exactly one permission, the owner. Inside it, all but two of its several thousand files carried anyoneWithLink reader, readable by anyone holding the URL: months of screenshots, published one per upload by a screenshot tool's "share to Google Drive" output. Nobody had shared anything on purpose, and no check of the folder could have found it.

Three lessons, and each one changes how you audit:

  • A clean folder proves nothing about its contents. Exposure lives on the item.
  • Drive's own UI cannot answer this question. There is no "show me everything that is shared" view, at any scale.
  • The cause usually keeps running. Revoking is cleanup, not a fix, until the tool that published the links is changed. That folder grew while it was being audited.

First run

One Python file plus its library, nothing packaged, so you can read every line before letting it near your Drive.

# 0. the Google Cloud CLI, which step 5 of the auth recipe below needs. If you already have
#    credentials via $DRIVE_AUDIT_CREDENTIALS you can skip it.
#    https://cloud.google.com/sdk/docs/install   then:  gcloud init

# 1. dependencies, in a throwaway environment so nothing lands in a project env
python3 -m venv ~/.local/share/drive-audit/venv
~/.local/share/drive-audit/venv/bin/pip install google-auth google-api-python-client

# 2. check the classifier BEFORE granting any access. Needs no credentials at all.
python3 scripts/drive-audit --selftest
python3 scripts/example_report.py        # a full report built from invented findings

# 3. run it. The venv above is found automatically; or point DRIVE_AUDIT_PYTHON at any
#    interpreter that has those two libraries.
python3 scripts/drive-audit scan

Commands below are written as a bare drive-audit for brevity. Either call python3 scripts/drive-audit, or put it on your PATH once with ln -s "$PWD/scripts/drive-audit" ~/.local/bin/drive-audit.

Run it

drive-audit scan                  # fast: the four public and domain visibility classes
drive-audit scan --deep           # + every owned item and every shared drive (slow)
drive-audit folder <FOLDER_ID>    # one folder and its DIRECT CHILDREN, not the subtree
drive-audit policy                # which policy file is in effect
drive-audit --selftest            # offline, no credentials, no Google libraries

# Mutating. Every one of these is a DRY RUN until --apply.
drive-audit revoke --under <ID>            # the DIRECT CHILDREN of that folder, PUBLIC shares only
drive-audit revoke --file <ID>             # one named item, EVERY grant on it, not just public
drive-audit revoke --principal <EMAIL>     # everything shared with that address
drive-audit revoke --under <ID> --apply    # snapshot first, then remove
drive-audit downgrade --principal <EMAIL> --to reader --apply   # keep access, drop write
drive-audit reparent --principal <EMAIL> --into <ID>   # what revoke cannot remove: move, revoke

Exit 0 clean, 1 unapproved shares found or nothing was scanned. An empty pass is a failure on purpose: a scan that reached nothing and printed a clean bill is the one wrong answer that reads like a right one.

Three things decide whether a mutation happens, and none of them is a formality:

--applyabsent, nothing is changed. Every mutating command is a dry run by default.
--allrequired to act with no scope flag, because that scope is every public share you own. A missing --under should not silently become the whole account.
--max-changes Nrefuses above N (default 500). A number much larger than you expected means the scope is wrong, and re-reading a scope is cheaper than restoring from a snapshot. Raising it is a decision to make after reading the dry run, not a reflex when the tool objects: the cleanups this was written for ran to thousands of permissions, so hitting the ceiling is normal and being asked to look again is the point.

--under is the direct children of one folder, not its subtree, and it targets public shares only. To reach a non-public grant, name the item with --file (which covers every grant on it) or the person with --principal. There is no recursive mode: a subtree sweep is exactly the operation that should be assembled one folder at a time and looked at in between.

scan and scan --deep are not the same question. The fast scan uses Drive's visibility = operator, which only knows the public and domain classes. There is no query operator for "shared with a specific person", so shares to named individuals are invisible until --deep enumerates every owned item. Never report "nothing is shared" from a fast scan alone.

Credentials

Resolved in this order, so neither a personal account nor a delegated service account needs a code change:

  1. $DRIVE_AUDIT_CREDENTIALS_COMMAND, a command that prints a service-account JSON key, for a key kept in a secret manager instead of a file (for example doppler secrets get GOOGLE_SA_KEY --plain --project P --config C; name the project, because the tool runs from any directory). It is split with POSIX shell rules but runs without a shell, so $VAR and ~ are not expanded. Its stdin is closed, and its output is never printed, not even in an error. When it is set, it wins over a key file.
  2. $DRIVE_AUDIT_CREDENTIALS, a service-account JSON key file.
  3. $GOOGLE_APPLICATION_CREDENTIALS, the same file under Google's own variable name.
  4. Application Default Credentials.

A key from 1, 2 or 3 impersonates $DRIVE_AUDIT_IMPERSONATE when it is set, for domain-wide delegation.

A personal Google account, in full

A bare gcloud auth application-default login is not enough. It mints credentials against Google's own shared client, which carries no Drive scope, so every call returns an insufficient-scope error. You need a client of your own, and these are the whole five steps:

  1. Create a project at console.cloud.google.com.
  2. Enable the Drive API at console.cloud.google.com/apis/library/drive.googleapis.com.
  3. Configure the OAuth consent screen as External and add your own address under Test users. An unverified app is limited to its test users, which is what you want for a tool only you run. But a project left in publishing status Testing issues a refresh token that expires after seven days, so a yearly audit will find its credential dead every time. Either re-run step 5 before each run, or set the publishing status to In production, which for an app that only ever reads your own data is a status change rather than a review, and it stops the expiry.
  4. Create an OAuth client ID of type Desktop app, and download the JSON.
  5. Authorise with that client and the Drive scope:
gcloud auth application-default login \
  --client-id-file=/path/to/your-oauth-client.json \
  --scopes=https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/cloud-platform

The consent screen will warn that the app is unverified. The app is yours.

A service-account key is the wrong tool for a personal account: a service account has no access to your files, and it cannot impersonate you without a Workspace domain to delegate from.

Two things that will otherwise cost you an afternoon:

  • The scope is the full drive scope, and it has to be. drive.file restricts an app to files it created itself, so it cannot see, let alone address, anything that already existed. An auditor scoped that way reports a clean Drive and is simply blind.
  • A service account has its own empty Drive. Without an impersonation subject, every call succeeds against nothing at all. That is the worst kind of clean run, and it is why the subject is a separate variable rather than an optional extra.

With delegation, Google matches the granted scope string exactly and does not honour hierarchy, so a scope that was not granted separately fails at token exchange with unauthorized_client rather than per call.

Reading the report

Classes print worst first, and that order is the severity judgement:

ClassWhat it meansDefault action
public_indexedanyone with allowFileDiscovery: true. Listed in search, findable without the URL.revoke, unless deliberately published
public_linkanyone, link only. The URL alone is the credential.revoke unless the link was handed out on purpose
external_writersomebody outside the account can EDITverify the person, then approve or revoke
external_readersomebody outside the account can readoften legitimate: approve it in the policy
external_groupa group can reach itcheck the group's membership, which the ACL does not show
internal_writer / internal_readera NAMED person in your own domaina deliberate grant to one person: approve or revoke by name
domain_indexed / domain_linkanybody in your domain, no name attacheddecide once for the domain, not per file

allowFileDiscovery is the whole severity axis. It is the difference between "somebody has to be given the URL" and "it turns up in a search". Treating the two as one class understates the exposure, which is the direction that costs.

A named insider is not the same finding as a domain-wide grant, and folding them together is wrong on any domain with more than one account: a colleague granted access by name is one person and a decision somebody made, while "anyone in the domain" is neither. On a single-account domain the insider classes are simply empty, so the correct behaviour costs nothing there.

Two rows that are not findings to chase:

  • inherited items are descendants of a share set on an ancestor. Fix the ancestor and they follow. The report collapses them on purpose, because listing four hundred children of one shared folder buries the row that matters.
  • approved by policy items matched a policy entry. --show-approved lists them with the reason.

The policy file is the point

A second run that re-reads the whole inventory gets skipped, and a skipped audit is worth nothing. So every deliberate share goes into a policy file with a reason and a date, and the next run reports the residue rather than the inventory.

drive-audit policy prints the resolution order: $DRIVE_AUDIT_POLICY first, because the file records decisions and belongs in version control next to whatever else records decisions, then ~/.config/drive-audit/policy.toml.

[[approved_principal]]        # a person or service that may keep access
email = "advisor@example.com"
reason = "outside accountant, the folder they work in"
since = "2026-05-26"
roles = ["reader"]            # optional: an approval for reader does NOT cover writer

[[approved_tree]]             # a folder whose direct children may stay shared
folder_id = "0B_EXAMPLE_FOLDER_ID"
reason = "why this folder is shared"
since = "2026-05-26"

[[approved_public]]           # one file that is published on purpose
file_id = "0B_EXAMPLE_FILE_ID"
reason = "linked from a public page"
since = "2026-01-01"

[[approved_domain]]           # a whole domain that may keep access
domain = "example.com"
reason = "why the domain is trusted"
since = "2026-01-01"

roles is the escalation guard. Without it, an approval for reader keeps covering the same principal after somebody upgrades them to writer, which is exactly the change an audit exists to notice. Pin it on anyone who should never gain write.

A policy entry with no reason is a blanket suppression nobody can review next year. That is how a baseline stops being a baseline. A missing policy file is never an error, though: the first run has nothing to diff against and must still work.

Revoking

revoke is a dry run unless --apply. With --apply it writes ~/.local/share/drive-audit/drive-acl-snapshot-<timestamp>.json before the first delete, holding the file id, permission id, principal, role and discovery flag of everything it removes. That file is what makes the operation reversible: one row restores with a single permissions.create.

Say the consequence before running it, not after. A revoked link is dead everywhere that URL was ever pasted: a doc, a ticket, a chat message, an email. For a few thousand files that is the real price of the fix, and the snapshot is the only way back.

Three outcomes are not errors, and a tool that counts them as errors trains you to ignore its numbers:

  • 404 Permission not found is success. It means the permission is already gone, which is the state you asked for. On any resumed sweep this is the common case, because a listing is stale the moment the previous attempt removed anything. Measured at roughly a third of one re-run batch.
  • 403 cannotModifyInheritedPermission / cannotDeletePermission means the item is the wrong target. See below.
  • A finding already in the desired state.

Deletes are paced, a pause every hundred, because Drive allows roughly 12.000 queries per 60 s per user and a few thousand deletes fired flat out earns rateLimitExceeded partway through. Half done with no record of where it stopped is the worst available outcome.

When a revoke reports "decided by an ancestor folder"

Not an error, and the exit code will not be 1. The grant is not set at the level you addressed, and there are two cases. The tool prints which:

  1. The carrying folder is reachable. Re-run with --file <FOLDER_ID> against that folder and every descendant follows in one call. Acting on descendants individually is not merely wasteful, Drive refuses it. Use --file rather than --under: an item you name explicitly is treated as explicit intent and covers every grant on it, while --under and the no-scope case stay public-only, because they can sweep thousands of items. For a named person, --principal <EMAIL>.
  2. The item is ORPHANED. files.get returns no parent, because the folder the grant was set on no longer exists. The access outlived its own parent and no API call at any reachable level can remove it.

Case 2 is a real dead end, and saying so beats retrying. Measured: a former collaborator still held write on several hundred items, about a third of them orphaned, and every delete attempt returned 403 cannotDeletePermission. The only fix is to move the items into a folder the account controls, which breaks the inheritance chain, and then revoke. That reorganises somebody's Drive, so propose it and let them decide; never do it as part of a cleanup.

reparent does exactly that, and only that, once they have decided. Run it with the same scope flags as the revoke that was refused; it is a dry run until --apply.

  • It tries the plain delete first. Only a grant Drive refuses with cannotDeletePermission moves its item, so an item revoke could have cleaned is never reorganised.
  • The destination must be a private My Drive folder you own, and stay private. A moved item inherits every grant on its new parent. The tool reads the folder's full permission list and refuses a shared, trashed, shared-drive or foreign folder, before the dry run and again during the run. Sharing that folder later shares everything moved into it, so create one for this.
  • A move can drop EVERY grant the item inherited, approved ones too. Tell the owner who else loses access before --apply. The run ledger (*-ledger.json, next to the snapshot) lists each item's parents and all its grants, with which were inherited, and is rewritten before every move and after each outcome. A move whose outcome is not proven reads unknown: check that item by hand. Re-create any grant you still want from it with permissions.create.
  • It moves only orphaned files by default. An item with a parent is filed on purpose (--include-filed overrides, and then removes the item from EVERY folder it is in), an item in a shared drive is never moved, and a folder moves with everything inside it (--include-folders overrides).
  • It moves first, verifies the move, revokes, then reads the permissions back. Only a grant absent from that read-back counts as freed. An item that moved but kept its grant is listed by id and makes the exit code 1; finish it with revoke --file <ID>. An orphan has no parent to return to, so its old access can only come back as a direct grant.

That a refused grant becomes removable once its item has a new parent follows from how Drive inherits permissions. It has not been measured here on a live orphan, which is why the read-back decides every case rather than the delete's answer.

Google does not document this case. The error string is real and reproducible, but we found no page describing an orphaned item whose grant cannot be removed at any reachable level, so read the paragraph above as our own measurement rather than as vendor guidance. Related reading, covering inherited permissions generally and not this case: permissions.delete.

The same rule applies to downgrade: a role cannot be lowered below what an ancestor grants, so an over-privileged share on an orphaned item cannot be reduced either.

Two API traps worth knowing before you write your own

  • files.list silently drops permissionDetails, while permissions.list on the same file returns it. No error either way. That field carries inherited, so without it a shared folder and its several hundred children look like as many independent decisions. A per-item permissions.list is correct and costs one request per item, which is not viable across a whole Drive. Do not reach for the cheap reconstruction, and this is the trap rather than the tip. It is tempting to treat "the child carries the same permission id as an ancestor" as proof of inheritance. It is not proof of anything: a Drive permission id identifies the GRANTEE, not the grant. Verified on a live account, one person's id was identical on every item they could reach, in unrelated folders. So the test cannot tell an inherited grant from a second, independent grant to the same person, and using it to suppress a row hides a real share while printing a clean bill. This tool keeps it strictly as an ORDERING hint (act on the folder first, because Drive removes descendants' inherited access when the folder's grant goes) and lets Drive arbitrate: it rejects a genuinely inherited target with a 403, which is a correct answer from the only party that actually knows.
  • A visibility = query also returns other people's public files. It answers "what public thing can I see", not "what am I exposing", and Drive returns no usable permissions for files you do not own, so they classify as nothing while still inflating the count. In one measured run, most anyoneCanFind hits were strangers' documents. Split on ownedByMe.

Shared drives need supportsAllDrives=True and includeItemsFromAllDrives=True, plus corpora='drive' with driveId to scope to one. Miss either flag and whole drives are invisible with no error at all.

A shared drive also costs extra requests, unavoidably. files.list does not populate permissions for a shared-drive item, so a listing alone reports a clean drive while having read no ACL whatsoever. This tool asks Drive who needs a closer look: hasAugmentedPermissions marks the items carrying a grant beyond what the drive itself gives, and only those get a permissions.list. The drive's own top-level ACL is fetched once per drive, because that is where its membership lives and no file inside it carries that grant.

After a revoke, close the source

A revoke that leaves the publisher running is a chore you have scheduled forever.

  • A screenshot or capture tool with a Drive output. Several create a public link per upload, because a link is what they hand you. Turn that setting off, or point the tool at a local folder.
  • Any app with Drive write access, at myaccount.google.com/permissions. An app that published on upload once will do it again.
  • Legacy sync folders. Very old file ids often carry a domain-wide grant from a default that no longer applies.

Verify before you believe it

drive-audit --selftest      # 53 cases, no credentials, no network, no Google libraries

That it runs with no credentials is the point: the classification logic is checkable before this tool is granted any access to anything. scripts/example_report.py prints a full report from invented findings, so the output format can be read without a Drive at all.

What this does not do yet

Stated here rather than discovered by you, because an audit tool that is quiet about its blind spots is worse than one that has none:

  • A file published to the web is not detected. That permission is scoped to a separate view, and reading it needs a request parameter this tool does not pass, so a published Doc appears in no class at all. If publish-to-web is a risk you care about, check it by hand.
  • There is no restore command. The snapshot carries everything needed (type, role, allowFileDiscovery, the address, expirationTime, view), and restoring a row is one permissions.create per row with sendNotificationEmail=false, but you write that loop yourself today. Moving items back after a reparent is the same: the run ledger names the destination, each item's old parents and every grant it had, and the loop is yours. Undo is therefore a manual operation, which is a reason to read a dry run rather than to rely on the snapshot.
  • --under is one folder's direct children, never a subtree. There is deliberately no recursive mode.
  • Shared-drive coverage is newer than the rest, and costs a request per item that carries its own grant. Treat a first run against a large shared drive as something to check rather than trust.
  • The inheritance signal is an ordering hint and nothing more. See the traps section: it cannot prove inheritance, so it never suppresses a row.

Prior art

GAM prints the same ACLs (gam <user> print filelist fields permissions, and print drivefileacls per file) and is the better tool for any ad-hoc Drive question or Workspace-wide reporting. Prefer it there.

Two gaps are why this exists, both checked against GAM's own command reference rather than assumed. It has no saved baseline of approved shares for a later run to diff against, so every run re-reads the whole inventory. And while its documented workflow is to get the current ACLs before deleting, delete permissions has no option to write out what it removes, so the snapshot is a step you remember rather than a property of the revoke. Neither is a criticism: GAM is a Drive CLI, and this is a recurring audit.

Status

Built and verified against the Drive API v3 in September 2026, on a Workspace account holding tens of thousands of items. The measurements in this file were taken on real runs rather than estimated: the inheritance-id finding, the 404 rate on a resumed sweep, the orphaned-grant dead end, and the share of a visibility query that turns out to be other people's files.

What ships here is verifiable without credentials: scripts/drive-audit --selftest is 53 offline cases over the classifier, the policy engine and the ordering, and scripts/example_report.py prints a full report from invented findings. Both the severity split and the policy escalation guard were falsified by mutation rather than only confirmed, which is a stronger claim than a passing suite: the old behaviour was reintroduced and the cases went red.

Two limits worth knowing before you rely on it. Shared-drive coverage costs extra requests and is newer than the rest, so treat a first run against a large shared drive as something to check rather than trust. And a file published to the web is not yet detected: that permission is scoped to a separate view, and reading it needs a request parameter this tool does not yet pass, so a published Doc will not appear in any class. If publishing-to-web is a risk you care about, check it by hand until that is fixed.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.