Minimal Gmail CLI with restricted scopes for Claude Code and other agents
npm install @smcllns/gmailA minimal Gmail CLI with restricted permissions so Claude Code and other agents can autonomously read and organize your inbox.
``bash`
bunx @smcllns/gmail search "in:inbox is:unread" --max 10
This is a fork of the excellent @mariozechner/gmcli. The original requests full Gmail permissions (mail.google.com), and I wanted to restrict capabilities to prevent agents from accidentally sending or deleting email. The intent is to let agents run autonomously to understand and manage the inbox, while requiring a human to make any one-way door decisions.
1. Restricted OAuth scopes - The original uses mail.google.com (full access). This fork requests only:gmail.modify
- (restricted) - required to add/remove labels and archivegmail.labels
- - to create and edit labelsgmail.readonly
- Optional dry-run mode uses for read-only access
2. Dangerous operations blocked in CLI - Even where OAuth scopes allow, the CLI blocks:
- send and delete commands are disabled
- Disabled commands return guidance directing users to the Gmail web interface
3. Simplified for agent usage:
- Renamed binary from gmcli to gmailbunx @smcllns/gmail
- Default account config so commands don't require email prefix
- Usage: or npx @smcllns/gmail
| Feature | @mariozechner/gmcli (original) | @smcllns/gmail (this fork) |
| --- | --- | --- |
| Gmail permissions | Full access | Read and organize mail (no send/delete) |
| OAuth scopes | mail.google.com | gmail.modify, gmail.labels (live) / gmail.readonly (dry-run) |gmcli
| Read email | ✅ Yes | ✅ Yes |
| Send email | ✅ Yes | ❌ No |
| Delete email | ✅ Yes | ❌ No |
| Manage labels | ❌ No | ✅ Yes |
| Shell command | | gmail |
| Set default account | ❌ No | ✅ Yes |
`bash`
npm install -g @smcllns/gmail
Or run directly without global install:
`bash`
npx @smcllns/gmail
After setup, search your inbox:
`bash`
gmail search "in:inbox is:unread" --max 10
1. Go to Google Cloud Console
2. Create a new project (or select existing)
3. Enable the Gmail API
4. Go to "APIs & Services" → "Credentials"
5. Create "OAuth client ID" → "Desktop app"
6. Download the credentials JSON file
`bashSet up OAuth Client credentials (once per machine)
gmail accounts credentials ~/path/to/credentials.json
Read-only accounts can search and read threads; label changes require upgrade.
`
Uses Gmail search syntax:
`bash`
gmail search "in:inbox"
gmail search "from:boss@company.com is:unread"
gmail search "has:attachment filename:pdf after:2024/01/01"
gmail search "label:Work subject:urgent" --max 50
gmail search --label Label_123 # filter by label ID (from 'labels list')
gmail search "is:unread" --label INBOX # combine query with label filter
`bash`
gmail thread
gmail thread
Downloaded attachment filenames are sanitized and may differ from originals.
Adding TRASH or SPAM is blocked unless you pass --allow-dangerous-labels.
`bash`
gmail labels list
gmail labels create "My Label"
gmail labels create "Urgent" --text "#ffffff" --bg "#fb4c2f" # with colors
gmail labels edit "My Label" --name "Renamed" --bg "#16a765"
gmail labels
gmail labels
`bash`
gmail url
By default, credentials, accounts, and attachments are stored in ~/.gmail-cli/. Use --config-dir to store them in a project-local directory instead:
`bash`All commands use the custom directory for that invocation
gmail --config-dir ./.gmail accounts credentials ~/creds.json
gmail --config-dir ./.gmail accounts add you@gmail.com
gmail --config-dir ./.gmail search "in:inbox"
Programmatic usage:
`typescript`
const gmail = new GmailService({ configDir: './.gmail' });
Relative paths are resolved to absolute. The directory is created automatically on first use.
`
USAGE
gmail accounts
gmail config
gmail
gmail --account
gmail --config-dir
ACCOUNT COMMANDS
gmail accounts credentials
gmail accounts list List configured accounts
gmail accounts add
gmail accounts add
gmail accounts upgrade
gmail accounts remove
CONFIG COMMANDS
gmail config default
gmail config show Show current configuration
GMAIL COMMANDS
gmail search [query] [--max N] [--page TOKEN] [--label L]
Search threads. Query uses Gmail syntax, --label filters by name or ID.
Returns: thread ID, date, sender, subject, labels.
gmail thread
Get full thread. --download saves attachments to
gmail labels list
List all labels with ID, name, type, and colors.
gmail labels create
Create a new label with optional colors.
gmail labels edit
gmail labels
Modify labels on threads.
System labels: INBOX, UNREAD, STARRED, IMPORTANT, TRASH, SPAM
Adding TRASH or SPAM is blocked unless --allow-dangerous-labels is set.
gmail url
Generate Gmail web URLs for threads.
RESTRICTED (returns guidance to use Gmail web UI)
gmail send
gmail delete
DATA STORAGE (default: ~/.gmail-cli/, override with --config-dir)
If accounts.json is corrupted or malformed, the CLI will error instead of silently continuing.`
`typescript
import { GmailService } from '@smcllns/gmail';
const gmail = new GmailService();
const thread = await gmail.getThread('you@gmail.com', 'threadId123');
`
#### Programmatic OAuth tokens
Provide tokens directly without filesystem access — useful for web apps, serverless functions, and multi-tenant servers:
`typescript
import { GmailService, type EmailAccount } from '@smcllns/gmail';
// Pass accounts at construction
const gmail = new GmailService({
accounts: [{
email: 'user@gmail.com',
oauth2: { clientId, clientSecret, refreshToken, accessToken },
}],
});
// Or add/update tokens after construction
gmail.setAccountTokens({
email: 'user@gmail.com',
oauth2: { clientId, clientSecret, refreshToken, accessToken },
});
// Then use normally
const threads = await gmail.searchThreads('user@gmail.com', 'in:inbox', 50);
`
When only using in-memory accounts, GmailService never touches the filesystem (~/.gmail-cli/).
If you set account.scopes, label operations enforce those scopes; include gmail.labels for create/update.
Note: getThread() normalizes Google API responses, converting null values to undefined.
#### Available methods
| Method | Description |
| --- | --- |
| searchThreads(email, query, maxResults?, pageToken?, labelIds?) | Search threads using Gmail query syntax |getThread(email, threadId, downloadAttachments?)
| | Get full thread with parsed message content |modifyLabels(email, threadIds, addLabels?, removeLabels?)
| | Add/remove labels on threads |listLabels(email)
| | List all labels with IDs, names, types, and colors |createLabel(email, name, options?)
| | Create a label with optional colors |updateLabel(email, labelId, options?)
| | Update a label's name or colors |getLabelMap(email)
| | Get bidirectional label name/ID lookup maps |downloadMessageAttachments(email, messageId)
| | Download all attachments from a message |setAccountTokens(account)
| | Add or update account tokens in memory |addGmailAccount(email, clientId, clientSecret, manual?, options?)
| | Add account via OAuth (disk-backed) |updateGmailAccount(email, clientId, clientSecret, manual?, options?)
| | Re-auth and replace stored tokens (disk-backed) |listAccounts()
| | List all configured accounts |deleteAccount(email)
| | Remove an account |
A mock implementation for testing code that depends on GmailService:
`typescript
import { MockGmailService } from '@smcllns/gmail/testing';
const mock = new MockGmailService();
// Configure test data
mock.setThread('thread123', { id: 'thread123', historyId: '1', messages: [] });
mock.setSearchResults('in:inbox', { threads: [] });
mock.setLabels([{ id: 'Label_1', name: 'Work', type: 'user' }]);
// Simulate errors
mock.setError('getThread', new Error('API Error'));
mock.setError('searchThreads', new Error('Rate limited'), true); // once only
// Inspect calls after test
expect(mock.calls.searchThreads).toHaveLength(1);
expect(mock.calls.searchThreads[0].args[1]).toBe('in:inbox');
// Reset between tests
mock.reset();
``
MIT