@smcllns/gmail
v0.9.1
Published
Minimal Gmail CLI with restricted scopes for Claude Code and other agents
Downloads
149
Maintainers
Readme
@smcllns/gmail
A minimal Gmail CLI with restricted permissions so Claude Code and other agents can autonomously read and organize your inbox.
bunx @smcllns/gmail search "in:inbox is:unread" --max 10Why use this?
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.
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 labels- Optional dry-run mode uses
gmail.readonlyfor read-only access
Dangerous operations blocked in CLI - Even where OAuth scopes allow, the CLI blocks:
sendanddeletecommands are disabled- Disabled commands return guidance directing users to the Gmail web interface
Simplified for agent usage:
- Renamed binary from
gmclitogmail - Default account config so commands don't require email prefix
- Usage:
bunx @smcllns/gmail <command>ornpx @smcllns/gmail <command>
- Renamed binary from
Comparison
| 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) |
| Read email | ✅ Yes | ✅ Yes |
| Send email | ✅ Yes | ❌ No (create drafts for human review) |
| Delete email | ✅ Yes | ❌ No |
| Compose drafts | ❌ No | ✅ Yes (plaintext, human sends) |
| Manage labels | ❌ No | ✅ Yes |
| Security proxy mode | ❌ No | ✅ Yes (GMAIL_PROXY env var) |
| Shell command | gmcli | gmail |
| Set default account | ❌ No | ✅ Yes |
Install
npm install -g @smcllns/gmailOr run directly without global install:
npx @smcllns/gmail <command>Quickstart
After setup, search your inbox:
gmail search "in:inbox is:unread" --max 10Environment variable credentials
Skip file-based setup entirely by passing OAuth credentials as env vars:
GMAIL_CLIENT_ID=xxx GMAIL_CLIENT_SECRET=xxx GMAIL_REFRESH_TOKEN=xxx [email protected] \
gmail search "in:inbox" --max 10| Env var | Required | Description |
|---------|----------|-------------|
| GMAIL_CLIENT_ID | Yes | OAuth client ID |
| GMAIL_CLIENT_SECRET | Yes | OAuth client secret |
| GMAIL_REFRESH_TOKEN | Yes | OAuth refresh token |
| GMAIL_ACCOUNT | Yes | Email address |
| GMAIL_PROXY | No | Route through security proxy (unix socket path or host:port) |
When set, credentials stay in memory — no files are read or written. This is the recommended approach for automated environments and sprites.
Setup (one-time)
1. Create OAuth credentials
- Go to Google Cloud Console
- Create a new project (or select existing)
- Enable the Gmail API
- Go to "APIs & Services" → "Credentials"
- Create "OAuth client ID" → "Desktop app"
- Download the credentials JSON file
2. Configure the CLI
# Set up OAuth Client credentials (once per machine)
gmail accounts credentials ~/path/to/credentials.json
# Add your Gmail account (opens google sign-in in browser to auth)
gmail accounts add [email protected]
# Or use --manual for headless/server environments
gmail accounts add [email protected] --manual
# Dry-run (read-only) mode
gmail accounts add [email protected] --readonly
# Upgrade to live mode (label changes)
gmail accounts upgrade [email protected]
Read-only accounts can search and read threads; label changes require upgrade.Usage
Search
Uses Gmail search syntax:
gmail search "in:inbox"
gmail search "from:[email protected] 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 filterRead threads
gmail thread <threadId>
gmail thread <threadId> --download # saves attachments to ~/.gmail-cli/attachments/Downloaded attachment filenames are sanitized and may differ from originals.
Manage labels
Adding TRASH or SPAM is blocked unless you pass --allow-dangerous-labels.
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 <threadId> --add Receipts --remove INBOX # add label "Receipts" and archive thread
gmail labels <threadId> --add TRASH --allow-dangerous-labels # requires explicit overrideCreate drafts
Create drafts for human review before sending. Useful for agents that should propose replies without sending directly.
gmail draft <threadId> --to "[email protected]" --subject "Re: Hello" --body "Thanks for your email"
gmail draft new --to "[email protected]" --subject "Hello" --body "Just reaching out"In-thread replies automatically set In-Reply-To and References headers for proper Gmail threading.
Get Gmail URLs to view messages in browser
gmail url <threadId>Security proxy mode
When the GMAIL_PROXY env var is set, all Gmail API calls route through a proxy instead of directly to googleapis.com. The proxy handles authentication and can enforce policies on which operations are permitted.
# Via unix socket
GMAIL_PROXY=/run/gmail-proxy/gmail.sock gmail search "in:inbox"
# Via TCP
GMAIL_PROXY=localhost:9877 gmail search "in:inbox"This is useful for sandboxed environments where the CLI shouldn't hold OAuth tokens directly — the proxy manages credentials and the CLI communicates through a local socket or port.
Custom config directory
By default, credentials, accounts, and attachments are stored in ~/.gmail-cli/. Use --config-dir to store them in a project-local directory instead:
# All commands use the custom directory for that invocation
gmail --config-dir ./.gmail accounts credentials ~/creds.json
gmail --config-dir ./.gmail accounts add [email protected]
gmail --config-dir ./.gmail search "in:inbox"Programmatic usage:
const gmail = new GmailService({ configDir: './.gmail' });Relative paths are resolved to absolute. The directory is created automatically on first use.
Full command reference
USAGE
gmail accounts <action> Account management
gmail config <action> Configuration management
gmail <command> [options] Gmail operations (uses default account)
gmail --account <email> <command> Gmail operations with specific account
gmail --config-dir <path> <command> Use custom config directory (default: ~/.gmail-cli/)
ACCOUNT COMMANDS
gmail accounts credentials <file> Set OAuth credentials (once)
gmail accounts list List configured accounts
gmail accounts add <email> Add account (--manual for browserless OAuth)
gmail accounts add <email> --readonly Add account in read-only mode (dry-run)
gmail accounts upgrade <email> Upgrade to live access (modify labels)
gmail accounts remove <email> Remove account
CONFIG COMMANDS
gmail config default <email> Set default account
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 <threadId> [--download]
Get full thread. --download saves attachments to <config-dir>/attachments/.
gmail labels list
List all labels with ID, name, type, and colors.
gmail labels create <name> [--text HEX] [--bg HEX]
Create a new label with optional colors.
gmail labels edit <label> [--name <newName>] [--text HEX] [--bg HEX]
Edit a label's name and/or colors.
gmail labels <threadIds...> [--add L] [--remove L] [--allow-dangerous-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 draft <threadId> --to <email> --subject <text> --body <text>
Create a draft reply in a thread. Use "new" as threadId for standalone drafts.
In-thread replies auto-set In-Reply-To/References headers.
gmail url <threadIds...>
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)
<config-dir>/credentials.json OAuth client credentials
<config-dir>/accounts.json Account tokens
<config-dir>/config.json CLI configuration
<config-dir>/attachments/ Downloaded attachments
If `accounts.json` is corrupted or malformed, the CLI will error instead of silently continuing.Programmatic Usage
GmailService
import { GmailService } from '@smcllns/gmail';
const gmail = new GmailService();
const thread = await gmail.getThread('[email protected]', 'threadId123');Programmatic OAuth tokens
Provide tokens directly without filesystem access — useful for web apps, serverless functions, and multi-tenant servers:
import { GmailService, type EmailAccount } from '@smcllns/gmail';
// Pass accounts at construction
const gmail = new GmailService({
accounts: [{
email: '[email protected]',
oauth2: { clientId, clientSecret, refreshToken, accessToken },
}],
});
// Or add/update tokens after construction
gmail.setAccountTokens({
email: '[email protected]',
oauth2: { clientId, clientSecret, refreshToken, accessToken },
});
// Then use normally
const threads = await gmail.searchThreads('[email protected]', '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 |
| createDraft(email, options) | Create a draft (to, subject, body, threadId?, inReplyTo?, references?) |
| 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 |
MockGmailService
A mock implementation for testing code that depends on GmailService:
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();License
MIT
