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 10This 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
| 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 |
npm install -g @smcllns/gmailOr run directly without global install:
npx @smcllns/gmail <command>After setup, search your inbox:
gmail search "in:inbox is:unread" --max 10Skip file-based setup entirely by passing OAuth credentials as env vars:
GMAIL_CLIENT_ID=xxx GMAIL_CLIENT_SECRET=xxx GMAIL_REFRESH_TOKEN=xxx GMAIL_ACCOUNT=you@gmail.com \
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.
- 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
# 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 you@gmail.com
# Or use --manual for headless/server environments
gmail accounts add you@gmail.com --manual
# Dry-run (read-only) mode
gmail accounts add you@gmail.com --readonly
# Upgrade to live mode (label changes)
gmail accounts upgrade you@gmail.com
Read-only accounts can search and read threads; label changes require upgrade.Uses Gmail search syntax:
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 filterEach result represents a matching thread. DATE, FROM, and SUBJECT describe the thread's newest message, which may differ from the specific message that matched the query. DATE is rendered in UTC, while result order remains Gmail's search order.
gmail thread <threadId>
gmail thread <threadId> --download # saves attachments to ~/.gmail-cli/attachments/Downloaded attachment filenames are sanitized and may differ from originals.
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 for human review before sending. Useful for agents that should propose replies without sending directly.
gmail draft <threadId> --to "recipient@example.com" --subject "Re: Hello" --body "Thanks for your email"
gmail draft new --to "someone@example.com" --subject "Hello" --body "Just reaching out"In-thread replies automatically set In-Reply-To and References headers for proper Gmail threading.
gmail url <threadId>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 you@gmail.com
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.
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, newest-message 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.
import { GmailService } from '@smcllns/gmail';
const gmail = new GmailService();
const thread = await gmail.getThread('you@gmail.com', 'threadId123');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: '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.
| 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 |
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();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.
MIT