Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Read this skill when you need to use user's Gmail. Don't have to open a browser tab.
.claude/skills/kunanonj-aside-google-gmail/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 144% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 40% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 54% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 12% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 63% | 0% |
Use the gmail global in the REPL tool. It uses direct HTTP against Gmail's internal sync API — no tab navigation needed.
IMPORTANT: User may use multiple Google accounts (e.g. work email / personal email). ALWAYS CALL await googleAccounts.print() to print all logged-in google accounts and identify the correct uid before using Gmail.
gmail method takes uid (the /u/{uid}/ index) as its first argument.uid. Use Promise.all for searching multiple accounts.jsconst uid = 1; // uid from Google Accounts list // Inbox and Search const inbox = await gmail.getInbox(uid); // shorthand for search(uid, 'in:inbox') const searchResults = await gmail.search(uid, 'from:alice@example.com subject:"meeting"'); // → { results: GmailSearchResult[], hasMore, total, nextOffset } // Read thread (body is markdown by default) const thread1 = await gmail.getThread(uid, 'thread-f:123456'); // → { threadId, subject, messages: GmailThreadMessage[] } // Compose (opens tab with pre-filled fields, returns Playwright Page) const cp1 = await gmail.openComposer(uid, { to: 'bob@example.com', subject: 'Hi', bodyHtml: '<b>Hello</b>' }); // Reply (opens inline reply in thread detail page, returns Playwright Page) const rp1 = await gmail.openReplyComposer(uid, { threadId: 'thread-f:123456', bodyHtml: '<p>Thanks</p>' }); // Download attachment await gmail.downloadAttachment(uid, attachment.url, 'report.pdf');
gmail.search(uid: number, query: string, opts?: { offset?: number }): Promise<GmailSearchResponse>Search threads via Gmail search operators (from:, to:, subject:, label:, before:, after:, is:, has:attachment, etc.).
uid — Google account index (/u/{uid}/).query — Gmail search query string.opts.offset — Pagination offset (default 0). Use nextOffset from previous response.Results are paginated, concise and already token-efficient; It's safe to call console.log to see all results. Call gmail.getThread to fetch the full thread.
gmail.getInbox(uid: number, offset = 0): Promise<GmailSearchResponse>Shorthand for gmail.search(uid, 'in:inbox', { offset }).
gmail.getThread(uid: number, threadId: string, opts?: { bodyFormat?: 'raw' | 'markdown' }): Promise<GmailThread>Fetch full thread with all messages, including body and attachments.
opts.bodyFormat — 'markdown' (default): body converted to compact markdown. safe to read. ('raw': original HTML)[Some Link][3] then [3]: https://example.com at the bottom. use RegEx to capture the URL according to the index.gmail.openComposer(uid: number, opts: { to?: string, cc?: string, bcc?: string, subject?: string, bodyHtml?: string }): Promise<Page>Open a Gmail compose tab with pre-filled fields. Returns the Playwright Page object for further interaction.
uid — Google account index.opts.to, opts.cc, opts.bcc — Recipients.opts.subject — Subject line.opts.bodyHtml — HTML body to inject into the compose draft.After clicking the send button in Composer, the tab will be automatically closed by Gmail.
gmail.openReplyComposer(uid: number, opts: { threadId: string, mode?: 'reply' | 'replyAll', bodyHtml?: string }): Promise<Page>Open an inline reply composer within a thread detail page.
uid — Google account index.opts.threadId — Thread to reply to.opts.mode — 'reply' (default) or 'replyAll' (recommended for multi-party threads).gmail.openThreadDetailsPage(uid: number, threadId: string): Promise<Page>Open the Gmail thread detail page in a new tab.
gmail.downloadAttachment(uid: number, url: string, destPath: string): Promise<string>Download an attachment to the dest path (relative path to the session directory). Returns the resolved file path.
tstype Participant = { name: string; email: string; }; interface GmailSearchResult { threadId: string; // e.g. 'thread-f:123456' subject: string; snippet: string; // small preview text of the thread timestamp: Date; participants: Participant[]; isUnread: boolean; } interface GmailSearchResponse { results: GmailSearchResult[]; hasMore: boolean; total: number; nextOffset: number; } interface GmailThread { threadId: string; subject: string; messages: GmailThreadMessage[]; raw?: string; // raw JSON when heuristic parsing has low confidence } interface GmailThreadMessage { messageId: string; // e.g. 'msg-f:123456' from: Participant; to: Participant[]; cc: Participant[]; subject: string; snippet: string; timestamp: Date; labels: string[]; body: string; // HTML (raw) or markdown, depending on bodyFormat attachments: Array<{ filename: string; mimeType: string; size: number; url: string; // needs auth — don't download directly, use gmail.downloadAttachment() inline: boolean; // true for inline images }>; }
After openComposer or openReplyComposer, interact with the returned Page:
js// Send only after verifying the compose body is non-empty. // If the user intentionally wants an empty email, mention that explicitly. const bodyText = await page.locator('[role=textbox][aria-label]').innerText(); if (!bodyText.trim()) throw new Error('Gmail compose body is empty; refusing to send.'); await page.locator('[data-tooltip*="Send"]').click(); // Attach file await page.locator('input[type=file][name=Filedata]').setInputFiles('/path/to/file'); // Discard draft await page.locator('[data-tooltip*="Discard"]').click();
gmail-draft JSON code block:\\\gmail-draft { "to": ["hello@example.com"], "cc": [], "bcc": [], "subject": "Hello world!", "body": "This is the draft body." } \\\
Then continue sending unless user required the confirmation. If user required confirmation, use Notification tool instead of ask_user_question tool.
Before clicking Send, verify the opened Gmail composer body is not empty again.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 4,464 | 6,033 | +35% | 1 | 1 | 0% | 675 | 2,089 | +209% | 0 | 0 | — |
case-02 | fail→fail | 3,485 | 3,902 | +12% | 1 | 1 | 0% | 579 | 1,975 | +241% | 0 | 0 | — |
case-03 | fail→fail | 4,118 | 7,117 | +73% | 1 | 1 | 0% | 716 | 2,587 | +261% | 0 | 0 | — |
case-04 | fail→pass | 6,133 | 5,270 | -14% | 1 | 1 | 0% | 1,121 | 2,740 | +144% | 0 | 0 | — |
case-05 | fail→fail | 2,590 | 6,463 | +150% | 1 | 1 | 0% | 440 | 2,303 | +423% | 0 | 0 | — |
case-06 | fail→pass | 10,695 | 4,652 | -57% | 1 | 1 | 0% | 1,888 | 2,650 | +40% | 0 | 0 | — |
case-07 | fail→pass | 7,633 | 2,578 | -66% | 1 | 1 | 0% | 1,480 | 2,281 | +54% | 0 | 0 | — |
case-08 | fail→pass | 10,194 | 2,633 | -74% | 1 | 1 | 0% | 2,042 | 2,286 | +12% | 0 | 0 | — |
case-09 | fail→fail | 8,021 | 5,603 | -30% | 1 | 1 | 0% | 1,144 | 2,169 | +90% | 0 | 0 | — |
case-10 | fail→pass | 6,981 | 2,144 | -69% | 1 | 1 | 0% | 1,340 | 2,185 | +63% | 0 | 0 | — |
case-11 | fail→fail | 4,727 | 1,629 | -66% | 1 | 1 | 0% | 767 | 2,085 | +172% | 0 | 0 | — |
case-16 | fail→pass | 9,090 | 3,166 | -65% | 1 | 1 | 0% | 1,594 | 2,313 | +45% | 0 | 0 | — |
case-12 | fail→pass | 9,510 | 5,031 | -47% | 1 | 1 | 0% | 1,751 | 2,534 | +45% | 0 | 0 | — |
case-13 | pass→pass | 7,703 | 4,610 | -40% | 1 | 1 | 0% | 1,268 | 2,568 | +103% | 0 | 0 | — |
case-14 | fail→pass | 8,136 | 2,780 | -66% | 1 | 1 | 0% | 1,503 | 2,323 | +55% | 0 | 0 | — |
case-15 | fail→pass | 12,755 | 2,449 | -81% | 1 | 1 | 0% | 2,193 | 2,218 | +1% | 0 | 0 | — |
case-17 | fail→pass | 7,507 | 1,890 | -75% | 1 | 1 | 0% | 1,187 | 2,144 | +81% | 0 | 0 | — |
case-18 | pass→pass | 8,700 | 2,759 | -68% | 1 | 1 | 0% | 1,720 | 2,356 | +37% | 0 | 0 | — |
case-19 | fail→pass | 8,363 | 2,155 | -74% | 1 | 1 | 0% | 1,833 | 2,235 | +22% | 0 | 0 | — |
case-20 | fail→fail | 12,010 | 5,423 | -55% | 1 | 1 | 0% | 2,263 | 2,179 | -4% | 0 | 0 | — |
case-21 | pass→fail | 3,932 | 5,355 | +36% | 1 | 1 | 0% | 614 | 2,154 | +251% | 0 | 0 | — |
case-22 | fail→pass | 12,346 | 6,531 | -47% | 1 | 1 | 0% | 2,460 | 2,400 | -2% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 16 counted toward the lift figure. The other 6 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +50 percentage points is the difference between those two pass rates over the 16 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.