How to Connect Claude Code to Gmail (with OAuth)
๐ค AI Agent? You can point your AI agent at this article and it can follow the steps to connect automatically. Everything it needs is here: the API endpoints, request/response formats, scopes, and working code samples. The TapAuth API is two calls โ
POST https://tapauth.ai/api/v1/grantsto create a grant, thenGET https://tapauth.ai/api/v1/token/{id}(with Bearer auth) to retrieve the access token. No API key needed โ your agent can follow the code examples below end-to-end.
Claude Code can read your codebase, write code, and run tests โ but the moment you want it to check your inbox or draft a reply, it's stuck. It needs a Gmail OAuth token, and OAuth was built for humans in browsers, not agents in terminals.
This is the core problem TapAuth solves. TapAuth is the token layer for AI agents โ one API call, and your agent gets a valid, scoped, user-approved Gmail token. No OAuth implementation. No redirect servers. No token storage. Your agent asks for a token and gets one.
This guide shows the full manual OAuth flow (so you see the complexity you're skipping) and then the TapAuth approach. The difference: ~200 lines of infrastructure code vs. one API call.
The real problem: OAuth wasn't built for agents
It's not just that Gmail OAuth is complex. It's that the entire OAuth model assumes your software has a browser session and a redirect endpoint. When you're building an AI agent, none of those assumptions hold:
- Your agent can't open a browser. Claude Code runs in a terminal. The OAuth consent screen requires a browser. So you need a separate redirect server, a way to pass the callback back to the agent, and coordination logic between the two. For an agent that just wants to read emails, you're now running a web server.
- Google only gives you the refresh token once. On the first authorization, Google returns a refresh token alongside the access token. If you miss storing it, or your storage fails, you have to revoke the user's grant and make them re-authorize from scratch. This is a silent, catastrophic failure mode.
- Tokens expire every hour. Access tokens are short-lived. Your agent needs background refresh logic, retry on 401, and secure storage for the refresh token. That's not agent logic โ it's infrastructure plumbing that has nothing to do with what your agent actually does.
- Scope management is a minefield. Gmail has granular scopes โ
gmail.readonly,gmail.send,gmail.modify,gmail.compose. Request sensitive scopes and Google requires app verification, which takes weeks. Get scopes wrong and your agent silently can't do its job. - Every provider is different. If you solve all of this for Gmail, congratulations โ now do it again for GitHub. And Slack. And Notion. Every provider has its own OAuth quirks, token formats, refresh behavior, and error codes. You're rebuilding the same infrastructure over and over.
This is the fundamental problem: developers building AI agents are spending more time on authentication plumbing than on the agent itself.
What the manual flow actually looks like
To understand the pain, here's a simplified version of what you'd need to implement. This is the happy path only โ production code needs error handling, token encryption, retry logic, and a persistent store on top of this.
import { google } from 'googleapis';
// Step 1: Create OAuth2 client
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
'http://localhost:3000/callback'
);
// Step 2: Generate authorization URL
const authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: [
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.send',
],
prompt: 'consent', // Force consent to ensure refresh token
});
// Step 3: Spin up a local server to catch the redirect
// (Yes, your agent needs an HTTP server just for auth)
import http from 'http';
const server = http.createServer(async (req, res) => {
const url = new URL(req.url!, 'http://localhost:3000');
const code = url.searchParams.get('code');
if (!code) return;
// Step 4: Exchange code for tokens
const { tokens } = await oauth2Client.getToken(code);
oauth2Client.setCredentials(tokens);
// CRITICAL: Store the refresh token โ Google only sends it once.
// If this write fails, the user has to re-authorize.
if (tokens.refresh_token) {
await saveRefreshTokenToEncryptedStore(tokens.refresh_token);
}
res.end('Authorized. You can close this tab.');
server.close();
});
server.listen(3000);
// Step 5: Handle token refresh (tokens expire every hour)
oauth2Client.on('tokens', (tokens) => {
if (tokens.refresh_token) {
saveRefreshTokenToEncryptedStore(tokens.refresh_token);
}
});
// Step 6: NOW you can actually use Gmail
async function listEmails() {
const gmail = google.gmail({ version: 'v1', auth: oauth2Client });
const res = await gmail.users.messages.list({
userId: 'me',
maxResults: 10,
});
return res.data.messages;
}
Count what you just built: an OAuth client, a redirect server, token exchange, secure storage, refresh handling โ and you haven't done anything related to your agent's actual job yet. This is pure infrastructure overhead. And if you need to support a second OAuth provider, you start from scratch with a different set of quirks.
What if your agent could just ask for a token?
TapAuth is an access gateway for AI agents. Instead of your agent implementing OAuth flows, it makes one API call and gets back a working token. TapAuth handles the entire OAuth dance โ consent, token exchange, encrypted storage, automatic refresh โ so your agent never touches any of it.
// Step 1: Create a grant โ tell TapAuth what you need
const grantResponse = await fetch(
'https://tapauth.ai/api/v1/grants',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.TAPAUTH_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
provider: 'google',
scopes: ['gmail.readonly', 'gmail.send'],
}),
}
);
const { grant_id, grant_secret, approve_url } = await grantResponse.json();
// Step 2: The user approves at the approve_url
// (Show this link to the user โ they authorize once)
console.log(`Approve access: ${approve_url}`);
// Step 3: Retrieve the token (poll until approved)
const tokenResponse = await fetch(
`https://tapauth.ai/api/v1/token/${grant_id}`,
{
headers: { 'Authorization': `Bearer ${grant_secret}` },
}
);
const { access_token } = await tokenResponse.json();
// Now use the token directly with Gmail
const response = await fetch(
'https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=10',
{
headers: { Authorization: `Bearer ${access_token}` },
}
);
const emails = await response.json();
Two API calls instead of two hundred lines. No redirect server. No token storage. No refresh logic. No 3 AM failures because a token expired and nobody refreshed it.
If the user hasn't approved yet, the token endpoint returns a pending status โ your agent just polls until it's ready. After the user authorizes once, TapAuth handles refresh automatically. And when you need to connect to GitHub next week, it's the same API โ just change the provider name.
This is the key insight: authentication is infrastructure, not application logic. Your agent shouldn't be an OAuth client any more than it should be a database engine. TapAuth is the token layer for AI agents โ one API call to connect any OAuth provider.
What you can build with Claude Code + Gmail + TapAuth
Once TapAuth handles authentication, your agent can focus on what it's actually good at:
- Email triage: "Read my unread emails and summarize anything that needs a response today"
- Draft replies: "Draft a reply to the email from Sarah about the Q3 budget โ be concise and confirm the meeting time"
- Code review notifications: "Check my Gmail for any GitHub notification emails about PRs assigned to me and list them"
- Inbox search: "Find all emails from our vendor about the API migration in the last 2 weeks"
Each of these is a few lines of Gmail API calls โ the hard part was always getting the token. With TapAuth, that's one API call, and you can go from zero to a working Gmail-connected agent in minutes, not days.
Scopes and permissions: Request only what you need
Google's Gmail API has fine-grained scopes. Follow the principle of least privilege:
| Scope | What it allows | When to use it |
|---|---|---|
gmail.readonly | Read emails and labels | Inbox summaries, search, triage |
gmail.send | Send emails (not read) | Sending drafts or notifications |
gmail.compose | Create and update drafts | Draft management without sending |
gmail.modify | Read, send, delete, manage labels | Full inbox management (use sparingly) |
Start with gmail.readonly and add scopes as your agent's capabilities grow. TapAuth lets you configure scopes per connection, so different agents or workflows can have different permission levels.
Stop building OAuth plumbing for your agents
Every hour you spend implementing OAuth for an agent is an hour not spent on what the agent actually does. TapAuth exists so you never write another OAuth flow for an AI agent again โ one API call to connect any OAuth provider, with tokens that are scoped, time-limited, and user-approved.
Get started โ set up Gmail in under five minutes. No client secrets in your agent code. No refresh token logic. Just tokens when your agent needs them.