agentleFS
spongetimblinspongetimblin/claude-skill-library/skills/gworkspace-api-personal/SKILL.md

gworkspace-api-personal

Skill1 starsChanged 6 days agoMITView on GitHub

Load context for WRITING to Google Sheets and EDITING Google Docs on your PERSONAL Google account (you@example.com) through your token-gated Apps Script web app, and for attaching Drive files to events on your personal calendar. Use whenever a task needs to add, update or clear cells or rows in a personal sheet, or find-and-replace, replace, insert or delete a paragraph in a personal doc, or when tempted to edit one through Claude in Chrome. Not needed for read-only access; the Google Drive connector reads sheets and docs.

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

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

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    If a sheet or doc lives on a second Google account, deploy a second copy of the script from that account with its own env var names.

    line 23
  2. Very specific:

    If the env vars are empty, the script isn't deployed yet.

    line 31
  3. Very specific:

    Every sheet op takes spreadsheet_id (the long id from the sheet's URL) and, except list_tabs, a sheet tab name.

    line 71
  4. Very specific:

    Prefer set_cell/add_row for header-keyed checklist sheets (they validate headers and refuse ambiguity).

    line 114
  5. Distinctive:

    Every mutation (sheet or doc) is auto-logged (with old values) to the "Claude Sheets Audit (gsheets-personal)" spreadsheet, which sits with …

    line 117

≈ “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

gworkspace-api-personal: Google Sheets and Google Docs write path (personal account)

When to use this skill

The description in this file's frontmatter is a short version of the text below. It was shortened on 2026-10-03 so that every skill's description fits in Claude's skill list. Nothing was dropped: this is the original description, word for word.

Load context for editing Google Sheets AND Google Docs on your PERSONAL Google account (you@example.com) via a token-gated Apps Script web app (any spreadsheet or document that account can edit), plus attaching Drive files to Calendar events on your personal calendar. Use whenever a task needs to WRITE to a personal-account Google Sheet (add/update/clear cells or rows) or to EDIT the body of a personal-account Google Doc (find-and-replace, replace/insert/delete a paragraph), or when tempted to edit a personal sheet or doc via Claude in Chrome (this path replaces that). Also for attaching files to personal calendar events. Not needed for read-only access (the Google Drive connector reads sheets and docs fine, including comments).

A standalone Apps Script web app deployed from the user's personal account, you@example.com ("Execute as: Me"), exposes a fixed op whitelist over any spreadsheet or Google Doc that account can edit. This is the sanctioned way to edit personal-account Google Sheets and Google Docs; do NOT use Claude in Chrome for sheet or doc edits. The Drive connector can read a Doc but cannot edit its body.

If a sheet or doc lives on a second Google account, deploy a second copy of the script from that account with its own env var names.

Credentials

Naming note (renamed 2026-09-04 from gsheets-personal): the env vars, the Apps Script project name (gsheets-personal in the Apps Script project list), the web app URL, the audit spreadsheet name, and the comments inside Code.gs keep the historical gsheets-personal name on purpose, for the same reasons as the work-account twin: renaming would mean a redeploy and shell-config edits for no functional gain, and Code.gs must stay byte-identical to the deployed copy so checksum verification keeps working.

  • Env vars GSHEETS_PERSONAL_API_URL and GSHEETS_PERSONAL_API_TOKEN are set in ~/.zshenv and available in every Claude Code shell.
  • Never print, echo, log, or store the token's value anywhere (chat, files, command output). Reference it only via the env var. Never ask the user to paste it.
  • If the env vars are empty, the script isn't deployed yet. Point the user to ~/.claude/skills/gworkspace-api-personal/apps-script/SETUP.md instead of improvising another write path.

Call pattern

Call the web app with scripts/gws_call.py, never a bare curl. Google intermittently fails to deliver this web app's replies. The script runs and finishes, but the reply, which Google serves from script.googleusercontent.com after a redirect, stalls for 10 to 30 seconds and then arrives as a "Page Not Found" page or a redirect back to the web app. Sometimes the first request stalls as well. A bare curl has no timeout and no retry, so it hangs or returns non-JSON. The client puts a timeout on both requests and resends when that is safe. Measured 2026-10-06: 12 of 40 pings failed this way in 23 minutes, while the Apps Script execution log showed every run completing in about half a second, no caller other than Claude, and the work-account twin answering 40 of 40 at the same moments. The cause is on Google's side and specific to this deployment; nothing in Code.gs is slow.

The client is this skill's own copy and calls only the personal web app, through GSHEETS_PERSONAL_API_URL and GSHEETS_PERSONAL_API_TOKEN. The work skill has a separate copy in its own folder. Never call one account's web app with the other skill's script.

python3 ~/.claude/skills/gworkspace-api-personal/scripts/gws_call.py ping
python3 ~/.claude/skills/gworkspace-api-personal/scripts/gws_call.py read_range '{"spreadsheet_id": "ID", "sheet": "Tab", "range": "A1:C5"}'

Several calls in one command, run in order and stopped at the first failure. Pass the JSON on stdin so quotes and emoji need no shell escaping:

python3 ~/.claude/skills/gworkspace-api-personal/scripts/gws_call.py --batch <<'EOF'
{"defaults": {"spreadsheet_id": "ID", "sheet": "Tab"},
 "steps": [{"op": "write_range", "range": "B5:C5", "values": [[1, "two"]]},
           {"op": "read_range", "range": "A5:C5"}]}
EOF

From Python, add the scripts folder to sys.path, then from gws_call import call; call("read_range", spreadsheet_id=..., sheet=..., range=...) returns the result or raises.

Each response is one JSON line: {"ok":true,"result":{...}} or {"ok":false,"error":"..."}; always check ok. A healthy call takes 1 to 3 seconds. One that had to be resent takes 10 to 40 and carries "attempts": N; the retry notes go to stderr. Exit codes are in the script's docstring.

What the client does when a reply is lost:

  • It resends every read and every write that can be sent twice, up to five attempts: write_range, set_cell, clear_range, set_link, set_note, attach_to_event, strip_meet, doc_set_link, doc_set_text_style, doc_replace_paragraph. After a resent write, old_value in the result may already be the new value; the audit spreadsheet keeps the original.
  • It never resends append_rows, add_row, doc_replace_text, doc_insert_paragraph_after or doc_delete_paragraph, because a second send could apply the change twice. It exits with code 3 and "outcome_unknown": true. Read the sheet or doc back to see whether the change landed before sending it again. For a new sheet row, avoid the problem: read the last row with get_headers and write_range the next row instead of using append_rows.
  • An op added to Code.gs later is not resent until its name is added to RETRY_SAFE in the client. Add it there when sending it twice leaves the same result as sending it once.

If the client is missing and curl is the only option: use -sL --max-time 60, and never pass -X POST. Apps Script answers with a 302 redirect, and -X POST forces POST onto the redirect, which returns "Page Not Found". A curl command line also shows the token in the process list while it runs, which the client avoids.

Ops

Every sheet op takes spreadsheet_id (the long id from the sheet's URL) and, except list_tabs, a sheet tab name. Exact payloads are documented in apps-script/Code.gs.

opwhat it does
pingliveness + auth check
list_tabstab names, dimensions, hidden flags
get_headersrow-1 headers (trimmed) + last data row
read_rangevalues for an A1 range (or whole data region); render: display (default) / raw / formula
write_rangeoverwrite an exact A1 range with a matching 2-D array
append_rowsappend raw rows after the last data row
add_rowheader-keyed append: values maps header text → cell; unknown headers error; duplicate guard on cols A/B (allow_duplicate: true to force)
set_cellone cell, located by row_match (col_a_contains + optional col_b_equals) × column_header; refuses ambiguous matches
clear_rangeclear values in an A1 range (formatting untouched)
read_linkscell text + rich-text hyperlink URLs for an A1 range (UI "Insert link" links, invisible to plain reads)
set_linkwrite one cell as text hyperlinked to a URL (rich text, like Insert link in the UI); plain writes to a linked cell strip the link. Takes cell (one A1 cell, like F305) with text and url, not range
read_notescell text + each cell's NOTE for an A1 range (the "Insert note" annotation, invisible to plain reads)
set_noteset one cell's NOTE, leaving its value and formatting alone; empty/null note clears it. Takes cell (one A1 cell, like M313) and note, not range. NOT threaded comments, which need the Drive API and aren't available here
attach_to_eventattach a Drive file to a Calendar event (allowlisted calendars only, currently you@example.com); pass share_with, since API attachments are not auto-shared
strip_meetremove the Google Meet conference from a Calendar event (allowlisted calendars only)

Google Docs ops (ported from the work twin 2026-09-04)

Every doc op takes doc_id (the long id from the Doc's URL). Elements are addressed by their index among the body's direct children, as returned by doc_get_text. Every mutating op except doc_replace_text and doc_set_link requires a contains guard: the target element's current text must contain it, or the op refuses. Re-run doc_get_text after any insert or delete, because indices shift.

opwhat it does
doc_get_textbody children as {index, type, text} (text capped at 3000 chars each); optional contains filter (case-sensitive) returns only matching elements
doc_replace_textliteral find-and-replace across the body; keeps the matched run's formatting; expect_count refuses the whole op if the occurrence count differs. Best tool for most edits.
doc_replace_paragraphreplace the full text of the element at index (guarded by contains); paragraph attributes kept, inline formatting reset
doc_insert_paragraph_afterinsert a new paragraph after the PARAGRAPH at index (guarded by contains), copying its attributes
doc_delete_paragraphremove the element at index (guarded by contains); old text goes to the audit log
doc_set_text_stylecopy_style_from: <index> copies a reference element's text attributes (font, size, colour, bold...) and paragraph attributes (spacing, indents, alignment) onto the element at index (guarded by contains); explicit bold, italic, underline, foreground_color, font_family, font_size apply afterwards. Always run this after doc_insert_paragraph_after, pointing at a plain body paragraph.
doc_set_linkset the hyperlink (url, or null to remove) on every literal occurrence of find; expect_count refuses on a count mismatch; foreground_color (for example #1155cc) colors only the matched text

doc_get_text also accepts with_links: true (adds a links array, {text, url}, per element: the only way to see where a URL's text actually points) and with_style: true (adds style: {paragraph, text} per element, for comparing an edited paragraph against its neighbours).

Docs caveats, learned on the work twin 2026-09-04 and confirmed here on a throwaway test Doc the same day: doc_replace_text keeps the hyperlink attribute of the matched run, so replacing a URL's text does not change where it links (check with with_links, fix with doc_set_link). doc_insert_paragraph_after copies attributes imperfectly (on the work account's DPA template the new paragraph came out bold, black, and with default spacing next to navy 10.5pt Arial neighbours; on the personal test Doc it lost the neighbour's spacing and indent attributes); always follow it with doc_set_text_style using copy_style_from a plain body paragraph, then confirm with with_style that the two elements' style objects are identical. copy_style_from can only copy attributes the reference paragraph has set explicitly; if the neighbour uses document defaults, the text attributes it copies are empty. with_style reports only the attributes set explicitly on the element's first character, so an empty text style means "no explicit attributes", not "default appearance": on the personal test Doc, a paragraph whose text had been replaced with doc_replace_paragraph next to a styled neighbour rendered bold navy Arial 11 in the Docs UI while with_style returned {} for it. When appearance matters, open the Doc in the browser and check. Paragraphs that look separate in the Drive connector's rendering can be one element with line breaks, so read doc_get_text before choosing an insert point. A lost reply does not mean the write failed (see "Call pattern"): gws_call.py resends the ops that are safe to send twice, and for the others, re-read the doc before sending again.

Rules

  1. Read before you write. get_headers (or read_range) first for sheets, doc_get_text first for docs; never write to a range or element you haven't looked at this session.
  2. Show the user the proposed write and get their OK before clear_range or any write that overwrites non-empty cells. A write into empty cells (write_range, set_cell, add_row, append_rows, set_link, set_note) can proceed when the task already authorizes it, as /cat-food-log does for the day's row; read the target first so you know the cells are empty.
  3. Verify after writing: re-read the range (or check the op's returned old_value/new_value) and confirm to the user what changed.
  4. Prefer set_cell/add_row for header-keyed checklist sheets (they validate headers and refuse ambiguity); use write_range/append_rows for everything else.
  5. Formula guard: values starting with = are rejected unless you pass allow_formulas: true. Only pass it when a formula is genuinely intended (e.g. HYPERLINK).
  6. Dates: write them as display strings in the sheet's existing format (e.g. 1/25/2027); Sheets parses them into real dates.
  7. Every mutation (sheet or doc) is auto-logged (with old values) to the "Claude Sheets Audit (gsheets-personal)" spreadsheet, which sits with the script in your Drive; mention this if the user worries about an overwrite.
  8. Caps: reads ≤ 20,000 cells, writes ≤ 5,000 cells per call; doc_get_text output ≤ 200,000 chars (use contains to narrow). Split larger jobs.
  9. This path can't do everything (no tab creation, formatting, comments, filters for sheets; no tables, images, or partial-run formatting for docs). If a task needs an op that doesn't exist, tell the user and propose adding it to Code.gs (they redeploy; URL stays the same). Don't fall back to Claude in Chrome without asking.

Reading (no write needed)

Use the Google Drive connector (read_file_content, which can include comments) for read-only work on sheets and docs when it's connected to the personal account (verified 2026-08-27 and again 2026-09-04 in the user's you@example.com Claude Code environment; check list_recent_files or search_files owners if unsure, since which account a connector is signed into varies by environment). read_range here works for any personal-account sheet regardless, and is handy when you need exact cell coordinates, formulas, or a fresh read immediately after a write; doc_get_text when you need element indices or a fresh read after a doc write.

Related

  • First consumer of the Sheets ops: ~/.claude/skills/cat-food-log/ (Cat Food & Weight Log; uses write_range, set_link, set_note).
  • ~/.claude/scheduled-tasks/reach-podcast-guests-sync/reach_sync.py imports scripts/gws_call.py. If the client moves or its call() signature changes, update that script too.
  • Deploy/redeploy/revoke instructions: apps-script/SETUP.md.

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.