add-gmail
Add Gmail integration to NanoClaw. Can be configured as a tool (agent reads/sends emails when triggered from WhatsApp) or as a full channel (emails can trigger the agent, schedule tasks, and receive replies). Guides through GCP OAuth setup and implements the integration.
Best use case
add-gmail is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Add Gmail integration to NanoClaw. Can be configured as a tool (agent reads/sends emails when triggered from WhatsApp) or as a full channel (emails can trigger the agent, schedule tasks, and receive replies). Guides through GCP OAuth setup and implements the integration.
Teams using add-gmail should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/add-gmail/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How add-gmail Compares
| Feature / Agent | add-gmail | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Add Gmail integration to NanoClaw. Can be configured as a tool (agent reads/sends emails when triggered from WhatsApp) or as a full channel (emails can trigger the agent, schedule tasks, and receive replies). Guides through GCP OAuth setup and implements the integration.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Add Gmail Integration
This skill adds Gmail capabilities to NanoClaw. It can be configured in two modes:
1. **Tool Mode** - Agent can read/send emails, but only when triggered from WhatsApp
2. **Channel Mode** - Emails can trigger the agent, schedule tasks, and receive email replies
## Initial Questions
Ask the user:
> How do you want to use Gmail with NanoClaw?
>
> **Option 1: Tool Mode**
> - Agent can read and send emails when you ask it to
> - Triggered only from WhatsApp (e.g., "@Andy check my email" or "@Andy send an email to...")
> - Simpler setup, no email polling
>
> **Option 2: Channel Mode**
> - Everything in Tool Mode, plus:
> - Emails to a specific address/label trigger the agent
> - Agent replies via email (not WhatsApp)
> - Can schedule tasks via email
> - Requires email polling infrastructure
Store their choice and proceed to the appropriate section.
---
## Prerequisites (Both Modes)
### 1. Check Existing Gmail Setup
First, check if Gmail is already configured:
```bash
ls -la ~/.gmail-mcp/ 2>/dev/null || echo "No Gmail config found"
```
If `credentials.json` exists, skip to "Verify Gmail Access" below.
### 2. Create Gmail Config Directory
```bash
mkdir -p ~/.gmail-mcp
```
### 3. GCP Project Setup
**USER ACTION REQUIRED**
Tell the user:
> I need you to set up Google Cloud OAuth credentials. I'll walk you through it:
>
> 1. Open https://console.cloud.google.com in your browser
> 2. Create a new project (or select existing) - click the project dropdown at the top
Wait for user confirmation, then continue:
> 3. Now enable the Gmail API:
> - In the left sidebar, go to **APIs & Services → Library**
> - Search for "Gmail API"
> - Click on it, then click **Enable**
Wait for user confirmation, then continue:
> 4. Now create OAuth credentials:
> - Go to **APIs & Services → Credentials** (in the left sidebar)
> - Click **+ CREATE CREDENTIALS** at the top
> - Select **OAuth client ID**
> - If prompted for consent screen, choose "External", fill in app name (e.g., "NanoClaw"), your email, and save
> - For Application type, select **Desktop app**
> - Name it anything (e.g., "NanoClaw Gmail")
> - Click **Create**
Wait for user confirmation, then continue:
> 5. Download the credentials:
> - Click **DOWNLOAD JSON** on the popup (or find it in the credentials list and click the download icon)
> - Save it as `gcp-oauth.keys.json`
>
> Where did you save the file? (Give me the full path, or just paste the file contents here)
If user provides a path, copy it:
```bash
cp "/path/user/provided/gcp-oauth.keys.json" ~/.gmail-mcp/gcp-oauth.keys.json
```
If user pastes the JSON content, write it directly:
```bash
cat > ~/.gmail-mcp/gcp-oauth.keys.json << 'EOF'
{paste the JSON here}
EOF
```
Verify the file is valid JSON:
```bash
cat ~/.gmail-mcp/gcp-oauth.keys.json | head -5
```
### 4. OAuth Authorization
**USER ACTION REQUIRED**
Tell the user:
> I'm going to run the Gmail authorization. A browser window will open asking you to sign in to Google and grant access.
>
> **Important:** If you see a warning that the app isn't verified, click "Advanced" then "Go to [app name] (unsafe)" - this is normal for personal OAuth apps.
Run the authorization:
```bash
npx -y @gongrzhe/server-gmail-autoauth-mcp auth
```
If that doesn't work (some versions don't have an auth subcommand), run it and let it prompt:
```bash
timeout 60 npx -y @gongrzhe/server-gmail-autoauth-mcp || true
```
Tell user:
> Complete the authorization in your browser. The window should close automatically when done. Let me know when you've authorized.
### 5. Verify Gmail Access
Check that credentials were saved:
```bash
if [ -f ~/.gmail-mcp/credentials.json ]; then
echo "Gmail authorization successful!"
ls -la ~/.gmail-mcp/
else
echo "ERROR: credentials.json not found - authorization may have failed"
fi
```
Test the connection by listing labels (quick sanity check):
```bash
echo '{"method": "tools/list"}' | timeout 10 npx -y @gongrzhe/server-gmail-autoauth-mcp 2>/dev/null | head -20 || echo "MCP responded (check output above)"
```
If everything works, proceed to implementation.
---
## Tool Mode Implementation
For Tool Mode, integrate Gmail MCP into the agent runner. Execute these changes directly.
### Step 1: Add Gmail MCP to Agent Runner
Read `container/agent-runner/src/index.ts` and find the `mcpServers` config in the `query()` call.
Add `gmail` to the `mcpServers` object:
```typescript
gmail: { command: 'npx', args: ['-y', '@gongrzhe/server-gmail-autoauth-mcp'] }
```
Find the `allowedTools` array and add Gmail tools:
```typescript
'mcp__gmail__*'
```
The result should look like:
```typescript
mcpServers: {
nanoclaw: ipcMcp,
gmail: { command: 'npx', args: ['-y', '@gongrzhe/server-gmail-autoauth-mcp'] }
},
allowedTools: [
'Bash',
'Read', 'Write', 'Edit', 'Glob', 'Grep',
'WebSearch', 'WebFetch',
'mcp__nanoclaw__*',
'mcp__gmail__*'
],
```
### Step 2: Mount Gmail Credentials in Container
Read `src/container-runner.ts` and find the `buildVolumeMounts` function.
Add this mount block (after the `.claude` mount is a good location):
```typescript
// Gmail credentials directory
const gmailDir = path.join(homeDir, '.gmail-mcp');
if (fs.existsSync(gmailDir)) {
mounts.push({
hostPath: gmailDir,
containerPath: '/home/node/.gmail-mcp',
readonly: false // MCP may need to refresh tokens
});
}
```
### Step 3: Update Group Memory
Append to `groups/CLAUDE.md` (the global memory file):
```markdown
## Email (Gmail)
You have access to Gmail via MCP tools:
- `mcp__gmail__search_emails` - Search emails with query
- `mcp__gmail__get_email` - Get full email content by ID
- `mcp__gmail__send_email` - Send an email
- `mcp__gmail__draft_email` - Create a draft
- `mcp__gmail__list_labels` - List available labels
Example: "Check my unread emails from today" or "Send an email to john@example.com about the meeting"
```
Also append the same section to `groups/main/CLAUDE.md`.
### Step 4: Rebuild and Restart
Run these commands:
```bash
cd container && ./build.sh
```
Wait for container build to complete, then:
```bash
cd .. && npm run build
```
Wait for TypeScript compilation, then restart the service:
```bash
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
```
Check that it started:
```bash
sleep 2 && launchctl list | grep nanoclaw
```
### Step 5: Test Gmail Integration
Tell the user:
> Gmail integration is set up! Test it by sending this message in your WhatsApp main channel:
>
> `@Andy check my recent emails`
>
> Or:
>
> `@Andy list my Gmail labels`
Watch the logs for any errors:
```bash
tail -f logs/nanoclaw.log
```
---
## Channel Mode Implementation
Channel Mode includes everything from Tool Mode, plus email polling and routing.
### Additional Questions for Channel Mode
Ask the user:
> How should the agent be triggered from email?
>
> **Option A: Specific Label**
> - Create a Gmail label (e.g., "NanoClaw")
> - Emails with this label trigger the agent
> - You manually label emails or set up Gmail filters
>
> **Option B: Email Address Pattern**
> - Emails to a specific address pattern (e.g., andy+task@gmail.com)
> - Uses Gmail's plus-addressing feature
>
> **Option C: Subject Prefix**
> - Emails with a subject starting with a keyword (e.g., "[Andy]")
> - Anyone can trigger the agent by using the prefix
Also ask:
> How should email conversations be grouped?
>
> **Option A: Per Email Thread**
> - Each email thread gets its own conversation context
> - Agent remembers the thread history
>
> **Option B: Per Sender**
> - All emails from the same sender share context
> - Agent remembers all interactions with that person
>
> **Option C: Single Context**
> - All emails share the main group context
> - Like an additional input to the main channel
Store their choices for implementation.
### Step 1: Complete Tool Mode First
Complete all Tool Mode steps above before continuing. Verify Gmail tools work by having the user test `@Andy check my recent emails`.
### Step 2: Add Email Polling Configuration
Read `src/types.ts` and add this interface:
```typescript
export interface EmailChannelConfig {
enabled: boolean;
triggerMode: 'label' | 'address' | 'subject';
triggerValue: string; // Label name, address pattern, or subject prefix
contextMode: 'thread' | 'sender' | 'single';
pollIntervalMs: number;
replyPrefix?: string; // Optional prefix for replies
}
```
Read `src/config.ts` and add this configuration (customize values based on user's earlier answers):
```typescript
export const EMAIL_CHANNEL: EmailChannelConfig = {
enabled: true,
triggerMode: 'label', // or 'address' or 'subject'
triggerValue: 'NanoClaw', // the label name, address pattern, or prefix
contextMode: 'thread',
pollIntervalMs: 60000, // Check every minute
replyPrefix: '[Andy] '
};
```
### Step 3: Add Email State Tracking
Read `src/db.ts` and add these functions for tracking processed emails:
```typescript
// Track processed emails to avoid duplicates
export function initEmailTable(): void {
db.exec(`
CREATE TABLE IF NOT EXISTS processed_emails (
message_id TEXT PRIMARY KEY,
thread_id TEXT NOT NULL,
sender TEXT NOT NULL,
subject TEXT,
processed_at TEXT NOT NULL,
response_sent INTEGER DEFAULT 0
)
`);
}
export function isEmailProcessed(messageId: string): boolean {
const row = db.prepare('SELECT 1 FROM processed_emails WHERE message_id = ?').get(messageId);
return !!row;
}
export function markEmailProcessed(messageId: string, threadId: string, sender: string, subject: string): void {
db.prepare(`
INSERT OR REPLACE INTO processed_emails (message_id, thread_id, sender, subject, processed_at)
VALUES (?, ?, ?, ?, ?)
`).run(messageId, threadId, sender, subject, new Date().toISOString());
}
export function markEmailResponded(messageId: string): void {
db.prepare('UPDATE processed_emails SET response_sent = 1 WHERE message_id = ?').run(messageId);
}
```
Also find the `initDatabase()` function in `src/db.ts` and add a call to `initEmailTable()`.
### Step 4: Create Email Channel Module
Create a new file `src/email-channel.ts` with this content:
```typescript
import { EMAIL_CHANNEL } from './config.js';
import { isEmailProcessed, markEmailProcessed, markEmailResponded } from './db.js';
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: { target: 'pino-pretty', options: { colorize: true } }
});
interface EmailMessage {
id: string;
threadId: string;
from: string;
subject: string;
body: string;
date: string;
}
// Gmail MCP client functions (call via subprocess or import the MCP directly)
// These would invoke the Gmail MCP tools
export async function checkForNewEmails(): Promise<EmailMessage[]> {
// Build query based on trigger mode
let query: string;
switch (EMAIL_CHANNEL.triggerMode) {
case 'label':
query = `label:${EMAIL_CHANNEL.triggerValue} is:unread`;
break;
case 'address':
query = `to:${EMAIL_CHANNEL.triggerValue} is:unread`;
break;
case 'subject':
query = `subject:${EMAIL_CHANNEL.triggerValue} is:unread`;
break;
}
// This requires calling Gmail MCP's search_emails tool
// Implementation depends on how you want to invoke MCP from Node
// Option 1: Use @anthropic-ai/claude-agent-sdk with just gmail MCP
// Option 2: Run npx gmail MCP as subprocess and parse output
// Option 3: Import gmail-autoauth-mcp directly
// Placeholder - implement based on preference
return [];
}
export async function sendEmailReply(
threadId: string,
to: string,
subject: string,
body: string
): Promise<void> {
// Call Gmail MCP's send_email tool with in_reply_to for threading
// Prefix subject with replyPrefix if configured
const replySubject = subject.startsWith('Re:')
? subject
: `Re: ${subject}`;
const prefixedBody = EMAIL_CHANNEL.replyPrefix
? `${EMAIL_CHANNEL.replyPrefix}${body}`
: body;
// Implementation: invoke Gmail MCP send_email
}
export function getContextKey(email: EmailMessage): string {
switch (EMAIL_CHANNEL.contextMode) {
case 'thread':
return `email-thread-${email.threadId}`;
case 'sender':
return `email-sender-${email.from.toLowerCase()}`;
case 'single':
return 'email-main';
}
}
```
### Step 5: Add Email Polling to Main Loop
Read `src/index.ts` and add the email polling infrastructure. First, add these imports at the top:
```typescript
import { checkForNewEmails, sendEmailReply, getContextKey } from './email-channel.js';
import { EMAIL_CHANNEL } from './config.js';
import { isEmailProcessed, markEmailProcessed, markEmailResponded } from './db.js';
async function startEmailLoop(): Promise<void> {
if (!EMAIL_CHANNEL.enabled) {
logger.info('Email channel disabled');
return;
}
logger.info(`Email channel running (trigger: ${EMAIL_CHANNEL.triggerMode}:${EMAIL_CHANNEL.triggerValue})`);
while (true) {
try {
const emails = await checkForNewEmails();
for (const email of emails) {
if (isEmailProcessed(email.id)) continue;
logger.info({ from: email.from, subject: email.subject }, 'Processing email');
markEmailProcessed(email.id, email.threadId, email.from, email.subject);
// Determine which group/context to use
const contextKey = getContextKey(email);
// Build prompt with email content
const prompt = `<email>
<from>${email.from}</from>
<subject>${email.subject}</subject>
<body>${email.body}</body>
</email>
Respond to this email. Your response will be sent as an email reply.`;
// Run agent with email context
// You'll need to create a registered group for email or use a special handler
const response = await runEmailAgent(contextKey, prompt, email);
if (response) {
await sendEmailReply(email.threadId, email.from, email.subject, response);
markEmailResponded(email.id);
logger.info({ to: email.from }, 'Email reply sent');
}
}
} catch (err) {
logger.error({ err }, 'Error in email loop');
}
await new Promise(resolve => setTimeout(resolve, EMAIL_CHANNEL.pollIntervalMs));
}
}
Then find the `connectWhatsApp` function and add `startEmailLoop()` call after `startMessageLoop()`:
```typescript
// In the connection === 'open' block, after startMessageLoop():
startEmailLoop();
```
### Step 6: Implement Email Agent Runner
Add this function to `src/index.ts` (or create a separate `src/email-agent.ts` if preferred):
```typescript
async function runEmailAgent(
contextKey: string,
prompt: string,
email: EmailMessage
): Promise<string | null> {
// Email uses either:
// 1. A dedicated "email" group folder
// 2. Or dynamic folders per thread/sender
const groupFolder = EMAIL_CHANNEL.contextMode === 'single'
? 'main' // Use main group context
: `email/${contextKey}`; // Isolated email context
// Ensure folder exists
const groupDir = path.join(GROUPS_DIR, groupFolder);
fs.mkdirSync(groupDir, { recursive: true });
// Create minimal registered group for email
const emailGroup: RegisteredGroup = {
name: contextKey,
folder: groupFolder,
trigger: '', // No trigger for email
added_at: new Date().toISOString()
};
// Use existing runContainerAgent
const output = await runContainerAgent(emailGroup, {
prompt,
sessionId: sessions[groupFolder],
groupFolder,
chatJid: `email:${email.from}`, // Use email: prefix for JID
isMain: false,
isScheduledTask: false
});
if (output.newSessionId) {
sessions[groupFolder] = output.newSessionId;
saveJson(path.join(DATA_DIR, 'sessions.json'), sessions);
}
return output.status === 'success' ? output.result : null;
}
```
### Step 7: Update IPC for Email Responses (Optional)
If you want the agent to be able to send emails proactively from within a session, read `container/agent-runner/src/ipc-mcp.ts` and add this tool:
```typescript
// Add to the MCP tools
{
name: 'send_email_reply',
description: 'Send an email reply in the current thread',
inputSchema: {
type: 'object',
properties: {
body: { type: 'string', description: 'Email body content' }
},
required: ['body']
}
}
```
Then add handling in `src/index.ts` in the `processTaskIpc` function or create a new IPC handler for email actions.
### Step 8: Create Email Group Memory
Create the email group directory and memory file:
```bash
mkdir -p groups/email
```
Write `groups/email/CLAUDE.md`:
```markdown
# Email Channel
You are responding to emails. Your responses will be sent as email replies.
## Guidelines
- Be professional and clear
- Keep responses concise but complete
- Use proper email formatting (greetings, sign-off)
- If the email requires action you can't take, explain what the user should do
## Context
Each email thread or sender (depending on configuration) has its own conversation history.
```
### Step 9: Rebuild and Test
Rebuild the container (required since agent-runner changed):
```bash
cd container && ./build.sh
```
Wait for build to complete, then compile TypeScript:
```bash
cd .. && npm run build
```
Restart the service:
```bash
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
```
Verify it started and check for email channel startup message:
```bash
sleep 3 && tail -20 logs/nanoclaw.log | grep -i email
```
Tell the user:
> Email channel is now active! Test it by sending an email that matches your trigger:
> - **Label mode:** Apply the "${triggerValue}" label to any email
> - **Address mode:** Send an email to ${triggerValue}
> - **Subject mode:** Send an email with subject starting with "${triggerValue}"
>
> The agent should process it within a minute and send a reply.
Monitor for the test:
```bash
tail -f logs/nanoclaw.log | grep -E "(email|Email)"
```
---
## Troubleshooting
### Gmail MCP not responding
```bash
# Test Gmail MCP directly
npx -y @gongrzhe/server-gmail-autoauth-mcp
```
### OAuth token expired
```bash
# Re-authorize
rm ~/.gmail-mcp/credentials.json
npx -y @gongrzhe/server-gmail-autoauth-mcp
```
### Emails not being detected
- Check the trigger configuration matches your test email
- Verify the label exists (for label mode)
- Check `processed_emails` table for already-processed emails
### Container can't access Gmail
- Verify `~/.gmail-mcp` is mounted in container
- Check container logs: `cat groups/main/logs/container-*.log | tail -50`
---
## Removing Gmail Integration
To remove Gmail entirely:
1. Remove from `container/agent-runner/src/index.ts`:
- Delete `gmail` from `mcpServers`
- Remove `mcp__gmail__*` from `allowedTools`
2. Remove from `src/container-runner.ts`:
- Delete the `~/.gmail-mcp` mount block
3. Remove from `src/index.ts` (Channel Mode only):
- Delete `startEmailLoop()` call
- Delete email-related imports
4. Delete `src/email-channel.ts` (if created)
5. Remove Gmail sections from `groups/*/CLAUDE.md`
6. Rebuild:
```bash
cd container && ./build.sh && cd ..
npm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
```Related Skills
setup
Run initial NanoClaw setup. Use when user wants to install dependencies, authenticate WhatsApp, register their main channel, or start the background services. Triggers on "setup", "install", "configure nanoclaw", or first-time setup requests.
setup-linux
Set up NanoClaw on Linux (Ubuntu/Debian recommended). Installs deps, configures .env, optional WhatsApp auth, and optional systemd service.
debug
Debug container agent issues. Use when things aren't working, container fails, authentication problems, or to understand how the container system works. Covers logs, environment variables, mounts, and common issues.
customize
Add new capabilities or modify NanoClaw behavior. Use when user wants to add channels (Telegram, Slack, email input), change triggers, add integrations, modify the router, or make any other customizations. This is an interactive skill that asks questions to understand what the user wants.
add-discord
Enable Discord as a NanoClaw channel (works alongside WhatsApp or Discord-only).
/add-codex
Add Codex CLI as an alternative NanoClaw agent runtime.
gmail-automation
Lightweight Gmail integration with standalone OAuth authentication. No MCP server required.
gmail-checker
Check Gmail for unread inbox emails, filtered by priority. Use when asked to check emails, check inbox, email digest, email summary, or "any new mail". Outputs a brief list sorted by priority (HIGH/MEDIUM/LOW). Skips marketing, promotions, social, and update categories. Configurable via gmail-config.json.
Skill: Gmail Auto-Reply for Client
## Purpose
gmail-ai
AI-enhanced Gmail — smart email triage, priority scoring, AI-generated replies, thread summarization, and automated categorization. IMAP/SMTP with OpenRouter-powered intelligence. Use for inbox zero, email management, smart replies, and email automation.
gmail-summarize
Fetch recent unread Gmail (yesterday + today) and send a digest to the user.
gmail-label-routing
Configurar en Gmail el enrutamiento por remitente hacia etiquetas usando el workflow local `scripts/gws_gmail_label_workflow.py`, incluyendo crear/usar etiqueta, crear filtro, aplicar retroactivo y opcionalmente sacar de INBOX. Usar cuando el usuario pida cosas como “manda este remitente a esta etiqueta”, “hazlo para varios remitentes”, “aplícalo a correos existentes”, “sácalos de INBOX”, o “limpia/reemplaza filtros duplicados para ese remitente”.