Skip to content

SOPs

Gmail Search and Triage

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 with gmail.modify scope. Reads, searches, archives, trashes, marks-read, surfaces unsubscribe links.
  • Claude.ai Gmail MCPmcp__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.

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_threads
  • mcp__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_thread
  • create_draft, list_drafts

If one of those gets added to the CLI, the block list here expands.

Default flow when I say “check my inbox,” “search my Gmail for X,” or “pull up the email about Y”:

Terminal window
# 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 keyword
python3 ~/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.

When I say “triage my inbox,” “clean up Gmail,” or “let’s go through emails”:

  1. Pull the inboxpython3 ~/apps/gmail-helper/gmail.py inbox --limit 30
  2. One at a time — present sender, subject, snippet, date. No batching until a pattern is obvious.
  3. Recommend — trash, archive, skip, unsubscribe. Say why.
  4. Wait for my call — no action without an explicit yes.
  5. Execute and move onarchive, trash, mark-read, or unsubscribe subcommand.
  6. Watch for batch patterns — if three garbage-reminder emails show up in a row, propose a bulk operation.
  7. Before any bulk op — show the exact match criteria (sender AND subject, never just one), the count, and three sample emails. Wait for approval.
  8. Open Gmail in the browserhttps://mail.google.com/mail/u/0/#search/{query} so I can eyeball the same set Claude is about to act on.
  • 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.

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:

PatternSenderSubject matchAction
Garbage reminderscalendar-notification@google.com”Take Garbage Out”Trash
School attendance (absent)Nebo-InfiniteCampus@mg.nebo.edu”Student Attendance”Trash
BYU Athletics promosBYU Athletics (any sender)AnyTrash
Affiliate cookie clicksclaude@jameshurst.com”Affiliate Cookie:“Trash
GKP test leads*@gokartpark.comContains “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-calendar and youtube-upload under the same Google Cloud project. Client id 162690810654-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.
  • Send a Gmail, not search: Send a Gmail
  • Hook source: ~/apps/cc/hooks/gmail-mcp-guard.sh
  • Settings registration: ~/.claude/settings.jsonhooks.PreToolUse → matcher mcp__claude_ai_Gmail__(search_threads|get_thread)
  • Triage ledger: ~/apps/gmail-helper/triage-rules.md
  • Meta-SOP: Adding a Page to tms-internal