SOPs
Gmail Search and Triage
The Rule
Section titled “The Rule”Any Gmail search or read defaults to the API-key CLI. The Claude.ai Gmail MCP server is kept only for what it is better at (labels and drafts) and must never be the default path for search.
- API-key CLI —
~/apps/gmail-helper/gmail.py, local OAuth token withgmail.modifyscope. Reads, searches, archives, trashes, marks-read, surfaces unsubscribe links. - Claude.ai Gmail MCP —
mcp__claude_ai_Gmail__*tool calls routed through Anthropic’s cloud. Good at labels and drafts. Cannot archive or trash.
The CLI owns the same OAuth token as every other local Gmail script (send-email, ATH daily digest, inbox-zero dashboard): one token, one refresh path, one scope, one auth story to debug. Pipe-friendly stdout means grep, id-chaining, and shell loops work. Full gmail.modify scope means the same session that finds an email can also act on it. The MCP connector offers none of that and adds an Anthropic-cloud round trip for what should be a local call.
Deterministic enforcement
Section titled “Deterministic enforcement”I do not rely on Claude remembering this rule. A PreToolUse hook at ~/apps/cc/hooks/gmail-mcp-guard.sh blocks the two MCP tools that overlap with the CLI:
mcp__claude_ai_Gmail__search_threadsmcp__claude_ai_Gmail__get_thread
When one of those fires, the hook exits non-zero and prints the CLI commands back. Claude re-routes to the CLI automatically. Registered in ~/.claude/settings.json under PreToolUse with matcher mcp__claude_ai_Gmail__(search_threads|get_thread).
The remaining MCP tools are still allowed because the CLI does not cover them yet:
list_labels,label_message,label_thread,unlabel_message,unlabel_threadcreate_draft,list_drafts
If one of those gets added to the CLI, the block list here expands.
How Claude searches my Gmail
Section titled “How Claude searches my Gmail”Default flow when I say “check my inbox,” “search my Gmail for X,” or “pull up the email about Y”:
# List the last 30 inbox messages (default table + JSON)python3 ~/apps/gmail-helper/gmail.py inbox --limit 30
# Filter inline by sender, subject, or snippet keywordpython3 ~/apps/gmail-helper/gmail.py inbox --limit 50 2>/dev/null | grep -i 'business profile'
# Read a specific message in full (use the 8-char id from the list output — Claude should widen to the full id internally)python3 ~/apps/gmail-helper/gmail.py read <message_id>The inbox command emits a human-readable table to stdout, followed by a ---JSON--- marker and the full JSON payload. That is deliberate — I can read the table at a glance, and Claude can parse the JSON for ids, senders, and unread flags.
Triage workflow
Section titled “Triage workflow”When I say “triage my inbox,” “clean up Gmail,” or “let’s go through emails”:
- Pull the inbox —
python3 ~/apps/gmail-helper/gmail.py inbox --limit 30 - One at a time — present sender, subject, snippet, date. No batching until a pattern is obvious.
- Recommend — trash, archive, skip, unsubscribe. Say why.
- Wait for my call — no action without an explicit yes.
- Execute and move on —
archive,trash,mark-read, orunsubscribesubcommand. - Watch for batch patterns — if three garbage-reminder emails show up in a row, propose a bulk operation.
- Before any bulk op — show the exact match criteria (sender AND subject, never just one), the count, and three sample emails. Wait for approval.
- Open Gmail in the browser —
https://mail.google.com/mail/u/0/#search/{query}so I can eyeball the same set Claude is about to act on.
Archive versus trash
Section titled “Archive versus trash”- Archive — might need someday. Receipts, confirmations, anything searchable later.
- Trash — zero future value. Expired notifications, cold pitches, duplicates of things I already have.
If I am not sure, archive. Trash is the aggressive move and should only happen on patterns I have already approved.
Living triage rules
Section titled “Living triage rules”The approved batch patterns and sender categories live at ~/apps/gmail-helper/triage-rules.md — that is the rolling log every session updates as we discover new rules. This wiki page is the doctrine, that file is the ledger.
Representative rules as of 2026-04-16:
| Pattern | Sender | Subject match | Action |
|---|---|---|---|
| Garbage reminders | calendar-notification@google.com | ”Take Garbage Out” | Trash |
| School attendance (absent) | Nebo-InfiniteCampus@mg.nebo.edu | ”Student Attendance” | Trash |
| BYU Athletics promos | BYU Athletics (any sender) | Any | Trash |
| Affiliate cookie clicks | claude@jameshurst.com | ”Affiliate Cookie:“ | Trash |
| GKP test leads | *@gokartpark.com | Contains “test” or “dummy” | Trash |
Auth, tokens, and what to do when it breaks
Section titled “Auth, tokens, and what to do when it breaks”- Token file:
~/apps/gmail-helper/token.json— local only, git-ignored. - Scope:
gmail.modify— read, search, archive, trash, label. - OAuth client: shared with
google-calendarandyoutube-uploadunder the same Google Cloud project. Client id162690810654-v6js0c70aglc8bfoe4ocghomcm080uqp. - Auto-refresh: the CLI refreshes the access token on every call. No manual refresh needed day to day.
- Re-auth path: if a call returns 401 or “token expired,” run
python3 ~/apps/gmail-helper/authorize.py. Browser opens, Google login, authorize Gmail, token is re-saved. Takes under a minute.
Related
Section titled “Related”- Send a Gmail, not search: Send a Gmail
- Hook source:
~/apps/cc/hooks/gmail-mcp-guard.sh - Settings registration:
~/.claude/settings.json→hooks.PreToolUse→ matchermcp__claude_ai_Gmail__(search_threads|get_thread) - Triage ledger:
~/apps/gmail-helper/triage-rules.md - Meta-SOP: Adding a Page to tms-internal