| name | add-gmail |
| description | Add Gmail integration to BastionClaw. 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. |
| allowed-tools | Bash(*), Read, Edit, Write, Glob, Grep, AskUserQuestion |
Add Gmail Integration
UX Rule: Use AskUserQuestion for ALL interactions with the user. Never just output questions as text — always use the tool so the user gets structured prompts with selectable options.
This skill adds Gmail capabilities to BastionClaw. It can be configured in two modes:
- Tool Mode - Agent can read/send emails, but only when triggered from WhatsApp
- Channel Mode - Emails can trigger the agent, schedule tasks, and receive email replies
Initial Questions
AskUserQuestion: How do you want to use Gmail with BastionClaw?
- Tool Mode — Agent can read and send emails when you ask it to. Triggered only from WhatsApp. Simpler setup, no email polling.
- Channel Mode — Everything in Tool Mode, plus emails can trigger the agent, schedule tasks, and receive email replies. 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:
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
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:
- Open https://console.cloud.google.com in your browser
- Create a new project (or select existing) - click the project dropdown at the top
Wait for user confirmation, then continue:
- 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:
- 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., "BastionClaw"), your email, and save
- For Application type, select Desktop app
- Name it anything (e.g., "BastionClaw Gmail")
- Click Create
Wait for user confirmation, then continue:
- 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:
cp "/path/user/provided/gcp-oauth.keys.json" ~/.gmail-mcp/gcp-oauth.keys.json
If user pastes the JSON content, write it directly:
cat > ~/.gmail-mcp/gcp-oauth.keys.json << 'EOF'
{paste the JSON here}
EOF
Verify the file is valid JSON:
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:
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:
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:
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):
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:
gmail: { command: 'npx', args: ['-y', '@gongrzhe/server-gmail-autoauth-mcp'] }
Find the allowedTools array and add Gmail tools:
'mcp__gmail__*'
The result should look like:
mcpServers: {
bastionclaw: ipcMcp,
gmail: { command: 'npx', args: ['-y', '@gongrzhe/server-gmail-autoauth-mcp'] }
},
allowedTools: [
'Bash',
'Read', 'Write', 'Edit', 'Glob', 'Grep',
'WebSearch', 'WebFetch',
'mcp__bastionclaw__*',
'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):
const gmailDir = path.join(homeDir, '.gmail-mcp');
if (fs.existsSync(gmailDir)) {
mounts.push({
hostPath: gmailDir,
containerPath: '/home/node/.gmail-mcp',
readonly: false
});
}
Step 3: Update Group Memory
Append to groups/CLAUDE.md (the global memory file):
## 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:
cd container && ./build.sh
Wait for container build to complete, then:
cd .. && npm run build
Wait for TypeScript compilation, then restart the service:
launchctl kickstart -k gui/$(id -u)/com.bastionclaw
Check that it started:
sleep 2 && launchctl list | grep bastionclaw
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:
tail -f logs/bastionclaw.log
Channel Mode Implementation
Channel Mode includes everything from Tool Mode, plus email polling and routing.
Additional Questions for Channel Mode
AskUserQuestion: How should the agent be triggered from email?
- Specific Label — Create a Gmail label (e.g., "BastionClaw"). Emails with this label trigger the agent. You manually label emails or set up Gmail filters.
- Email Address Pattern — Emails to a specific address pattern (e.g., andy+task@gmail.com). Uses Gmail's plus-addressing feature.
- Subject Prefix — Emails with a subject starting with a keyword (e.g., "[Andy]"). Anyone can trigger the agent by using the prefix.
AskUserQuestion: How should email conversations be grouped?
- Per Email Thread — Each email thread gets its own conversation context. Agent remembers the thread history.
- Per Sender — All emails from the same sender share context. Agent remembers all interactions with that person.
- 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:
export interface EmailChannelConfig {
enabled: boolean;
triggerMode: 'label' | 'address' | 'subject';
triggerValue: string;
contextMode: 'thread' | 'sender' | 'single';
pollIntervalMs: number;
replyPrefix?: string;
}
Read src/config.ts and add this configuration (customize values based on user's earlier answers):
export const EMAIL_CHANNEL: EmailChannelConfig = {
enabled: true,
triggerMode: 'label',
triggerValue: 'BastionClaw',
contextMode: 'thread',
pollIntervalMs: 60000,
replyPrefix: '[Andy] '
};
Step 3: Add Email State Tracking
Read src/db.ts and add these functions for tracking processed emails:
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:
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;
}
export async function checkForNewEmails(): Promise<EmailMessage[]> {
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;
}
return [];
}
export async function sendEmailReply(
threadId: string,
to: string,
subject: string,
body: string
): Promise<void> {
const replySubject = subject.startsWith('Re:')
? subject
: `Re: ${subject}`;
const prefixedBody = EMAIL_CHANNEL.replyPrefix
? `${EMAIL_CHANNEL.replyPrefix}${body}`
: body;
}
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:
import { checkForNewEmails, sendEmailReply, getContextKey } from './email-channel.js';
import { EMAIL_CHANNEL } from './config.js';
import { isEmailProcessed, markEmailProcessed, markEmailResponded } from './db.js';
Then add the startEmailLoop function:
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);
const contextKey = getContextKey(email);
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.`;
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 add startEmailLoop() in the main() function, 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):
async function runEmailAgent(
contextKey: string,
prompt: string,
email: EmailMessage
): Promise<string | null> {
const groupFolder = EMAIL_CHANNEL.contextMode === 'single'
? 'main'
: `email/${contextKey}`;
const groupDir = path.join(GROUPS_DIR, groupFolder);
fs.mkdirSync(groupDir, { recursive: true });
const emailGroup: RegisteredGroup = {
name: contextKey,
folder: groupFolder,
trigger: '',
added_at: new Date().toISOString()
};
const output = await runContainerAgent(emailGroup, {
prompt,
sessionId: sessions[groupFolder],
groupFolder,
chatJid: `email:${email.from}`,
isMain: false,
isScheduledTask: false
});
if (output.newSessionId) {
sessions[groupFolder] = output.newSessionId;
setSession(groupFolder, output.newSessionId);
}
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:
{
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/ipc.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:
mkdir -p groups/email
Write groups/email/CLAUDE.md:
# 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):
cd container && ./build.sh
Wait for build to complete, then compile TypeScript:
cd .. && npm run build
Restart the service:
launchctl kickstart -k gui/$(id -u)/com.bastionclaw
Verify it started and check for email channel startup message:
sleep 3 && tail -20 logs/bastionclaw.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:
tail -f logs/bastionclaw.log | grep -E "(email|Email)"
Troubleshooting
Gmail MCP not responding
npx -y @gongrzhe/server-gmail-autoauth-mcp
OAuth token expired
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:
-
Remove from container/agent-runner/src/index.ts:
- Delete
gmail from mcpServers
- Remove
mcp__gmail__* from allowedTools
-
Remove from src/container-runner.ts:
- Delete the
~/.gmail-mcp mount block
-
Remove from src/index.ts (Channel Mode only):
- Delete
startEmailLoop() call
- Delete email-related imports
-
Delete src/email-channel.ts (if created)
-
Remove Gmail sections from groups/*/CLAUDE.md
-
Rebuild:
cd container && ./build.sh && cd ..
npm run build
launchctl kickstart -k gui/$(id -u)/com.bastionclaw