Google Drive API skill for Codex
Upload, export and sync Drive files by ID. Codex treats Drive files by ID, includes shared drives in every query, uploads with resumable sessions and updates existing files instead of creating duplicates. It exports Docs and Sheets in the right MIME type, asks before sharing anything, audits external and anyone-with-link access, and keeps mirrors current with the changes feed rather than re-listing whole folders.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- google-drive-api Installs to ~/.agents/skills/google-drive-api/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install google-drive-api The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project google-drive-api Installs to .agents/skills/google-drive-api/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/google-drive-api.zipunzip -o google-drive-api.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) google-drive-api PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $google-drive-api, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Works with Google Drive API v3 from Python or Node - searching with q queries, listing folders and shared drives, resumable uploads, downloading and exporting Google Docs/Sheets, managing permissions and sharing, syncing with the changes feed, and avoiding duplicate names and quota errors. Use when asked to upload files to Drive, mirror a Drive folder, find files by query, share or audit permissions, export Docs to PDF or DOCX, or keep a local store in sync with Drive.
Use it when
- Files must be uploaded to, downloaded from or exported out of Google Drive
- A folder or shared drive needs to be mirrored or kept in sync
- You need to audit or change Drive sharing permissions
- Search must find files by name, type, folder or content
Use something else when
- You are still choosing OAuth vs service account or fixing invalid_grant: use google-workspace-apis
- The files live in OneDrive or SharePoint: use microsoft-graph
How to check the result
Compare local MD5 hashes with md5Checksum and the folder file count after uploads, and re-list permissions after any sharing change to show the final access table.
The mistake it prevents
Looking files up by name, which breaks because Drive allows duplicate names in the same folder.
Done well means: files are addressed by ID (never by name), every list call covers shared drives when the user has them, uploads resume instead of restarting, sharing changes are confirmed before they are made, and syncs use the changes feed rather than re-listing.
Authentication and scope choice are in $google-workspace-apis. Default to the drive.file
scope (files the app created or the user picked); request full drive only when the task
truly needs every file, because it is a restricted scope.
Mental model
- A file has an ID, a name (not unique, even in one folder), a
mimeTypeand one parent. - Folders are files with
mimeType = 'application/vnd.google-apps.folder'. - Google Docs, Sheets and Slides have no bytes to download; you export them.
- Shared drives are owned by the organisation; calls need
supportsAllDrives=Trueand lists also needincludeItemsFromAllDrives=Trueplus acorporachoice. - Trash is reversible;
files.deleteis permanent. Treat delete as destructive.
Search
from googleapiclient.discovery import build
drive = build("drive", "v3", credentials=creds, cache_discovery=False)
def list_files(q, fields="id, name, mimeType, modifiedTime, size, md5Checksum, parents"):
token = None
while True:
resp = drive.files().list(
q=q,
fields=f"nextPageToken, files({fields})",
pageSize=1000,
pageToken=token,
supportsAllDrives=True,
includeItemsFromAllDrives=True,
corpora="allDrives",
orderBy="modifiedTime desc",
).execute()
yield from resp.get("files", [])
token = resp.get("nextPageToken")
if not token:
return
folder_id = "1AbC..."
for f in list_files(f"'{folder_id}' in parents and trashed = false"):
print(f["id"], f["name"])
| Want | q |
|---|---|
| Children of a folder | '<folderId>' in parents and trashed = false |
| Exact name | name = 'Q3 report.pdf' (escape ' as \') |
| Only folders | mimeType = 'application/vnd.google-apps.folder' |
| Only Google Sheets | mimeType = 'application/vnd.google-apps.spreadsheet' |
| Changed recently | modifiedTime > '2026-10-01T00:00:00' |
| Text inside files | fullText contains 'invoice 4471' |
| Shared with me | sharedWithMe = true |
| Custom app property | appProperties has { key='externalId' and value='42' } |
corpora="allDrives" is slower; when you know the shared drive use corpora="drive" with
driveId. Listing is not recursive: walk folders breadth-first and keep a set of visited IDs
(shortcuts can create cycles).
Upload
| Size / need | Method |
|---|---|
| Up to ~5 MB, no metadata | Simple (uploadType=media) |
| Up to ~5 MB with metadata | Multipart |
| Larger, flaky network, or anything in a batch job | Resumable (default choice) |
from googleapiclient.http import MediaFileUpload
media = MediaFileUpload("build/report.pdf", mimetype="application/pdf",
resumable=True, chunksize=8 * 1024 * 1024) # multiple of 256 KiB
req = drive.files().create(
body={"name": "report.pdf", "parents": [folder_id],
"appProperties": {"externalId": "run-2026-10-28"}},
media_body=media, fields="id, md5Checksum", supportsAllDrives=True,
)
resp = None
while resp is None:
status, resp = req.next_chunk(num_retries=5)
if status:
print(f"{int(status.progress() * 100)}%")
- To convert on upload (CSV to Sheet, DOCX to Doc), set the target Google
mimeTypein body. - Update content of an existing file:
files().update(fileId=..., media_body=...); this keeps the ID, sharing and revision history. Creating a new file each run creates duplicates. - Idempotent uploads: tag files with
appPropertiesand search for the tag before creating. - Per-user upload cap is 750 GB per day; bulk migrations must spread over days or users.
Download and export
import io
from googleapiclient.http import MediaIoBaseDownload
def download(file_id, path, export_mime=None):
req = (drive.files().export_media(fileId=file_id, mimeType=export_mime) if export_mime
else drive.files().get_media(fileId=file_id, supportsAllDrives=True))
with io.FileIO(path, "wb") as fh:
dl = MediaIoBaseDownload(fh, req, chunksize=16 * 1024 * 1024)
done = False
while not done:
_, done = dl.next_chunk(num_retries=5)
| Source | Export MIME |
|---|---|
| Doc | application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document, text/markdown, text/plain |
| Sheet | text/csv (first sheet only), application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
| Slides | application/pdf, application/vnd.openxmlformats-officedocument.presentationml.presentation |
files.export is capped at 10 MB of output. For bigger documents use the exportLinks URLs
from file metadata with an authorised request, or the Sheets API for per-sheet values.
Permissions and sharing
Sharing is outward-facing: confirm the exact files, people and role with the user before any
permissions.create, and never set type: anyone without explicit approval.
drive.permissions().create(
fileId=file_id,
body={"type": "user", "role": "reader", "emailAddress": "[email protected]"},
sendNotificationEmail=False, # default True sends an email from the user
supportsAllDrives=True,
fields="id",
).execute()
| Role | Can |
|---|---|
reader / commenter | View / comment |
writer | Edit, and by default reshare |
fileOrganizer | Shared drives: organise content |
organizer | Shared drives: manage members |
owner | My Drive only; requires transferOwnership=True and acceptance rules apply |
Audit: permissions().list(fileId=..., fields="permissions(id,type,role,emailAddress,domain)")
for each file; flag type in ("anyone", "domain") and external domains. Permissions on a folder
are inherited by its children; inherited ones cannot be removed on the child.
Sync with the changes feed
token = drive.changes().getStartPageToken(supportsAllDrives=True).execute()["startPageToken"]
# persist token; later:
while token:
resp = drive.changes().list(pageToken=token, spaces="drive", includeRemoved=True,
supportsAllDrives=True, includeItemsFromAllDrives=True,
fields="nextPageToken, newStartPageToken, changes(fileId, removed, file(name, parents, md5Checksum, trashed))").execute()
for ch in resp["changes"]:
apply_change(ch) # idempotent by fileId
if "newStartPageToken" in resp:
save_token(resp["newStartPageToken"]); break
token = resp["nextPageToken"]
Push notifications (changes.watch / files.watch) need an HTTPS endpoint and channels that
expire; they tell you to call changes.list, they do not carry the data.
Gotchas
- Two files with the same name in one folder is normal; matching by name corrupts syncs.
md5Checksumis absent for Google-native files; comparemodifiedTimeorversioninstead.- Missing
supportsAllDrives=Truereturns 404 for files that exist in a shared drive. - A service account without delegation uploads into its own Drive with its own quota and the user cannot see the file; upload into a shared drive or share the folder to the account.
fieldsomitted returns a minimal set; asking for*is slow and quota-heavy.- 403
storageQuotaExceededon service accounts: they have no storage of their own for new files in My Drive; use a shared drive.
Verification
- After uploads: compare local MD5 with
md5Checksumand the expected file count in the folder. - After sharing: re-list permissions and show the user the final access table.
- After sync: run a full list once and diff against local state; the diff must be empty.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Google Drive API"
short_description: "Upload, export and sync Drive files by ID"
default_prompt: "$google-drive-api Upload the nightly build artifacts to our shared drive folder and skip unchanged files" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.