
himalaya oauth2: how to configure it, where it breaks, and when to skip it
A practical guide to setting up OAuth2 in the Himalaya CLI email client for Gmail and Outlook, plus what to do when it falls apart in headless environments.
Himalaya is a terminal-based email client that talks IMAP and SMTP, outputs JSON for scripting, and generally does what you'd want a CLI mail tool to do. It's good software. But the moment you try to wire up Himalaya OAuth2 for Gmail or Outlook, you enter a world of client IDs, redirect URIs, keyring entries, and token refresh loops that can eat an afternoon.
This guide walks through the full setup for both providers, explains the gotchas that trip people up, and covers the elephant in the room: OAuth2 browser redirects don't work when there's no browser. If you're running an agent or bot on a server, that matters.
What Himalaya CLI is and why OAuth2 is involved#
Himalaya is a command-line email client built in Rust. It handles IMAP for receiving, SMTP for sending, and can output structured JSON that's easy for scripts (and agents) to parse. It supports plain password auth, but Google deprecated "less secure app access" years ago, and Microsoft is doing the same. OAuth2 is now the expected auth method for both Gmail and Outlook.
The Himalaya OAuth2 implementation works by opening a local HTTP server, redirecting you to your provider's consent screen in a browser, catching the callback, and exchanging the authorization code for access and refresh tokens. Those tokens get stored in your system keyring.
It works well on a desktop. On a headless server, it's a different story.
How to configure OAuth2 in Himalaya (step by step)#
Here's the process from zero to working email:
- Register an OAuth2 app with your email provider (Google Cloud Console for Gmail, Azure AD for Outlook).
- Obtain your client ID and client secret from the app registration dashboard.
- Set
backend.auth.type = "oauth2"in your Himalaya TOML config file. - Add the client ID, secret, auth URL, and token URL to the config.
- Configure keyring entries for token storage (access token and refresh token).
- Set the required OAuth2 scopes for IMAP and SMTP access.
- Run
himalayaonce to trigger the interactive browser auth flow and store your tokens.
Let's break each one down with real config for both providers.
Gmail setup#
First, go to the Google Cloud Console, create a project, enable the Gmail API, and create OAuth2 credentials. Set the redirect URI to http://localhost:9090 (Himalaya's default). You'll get a client ID and client secret.
Your ~/.config/himalaya/config.toml looks like this:
[accounts.gmail]
email = "you@gmail.com"
backend.type = "imap"
backend.host = "imap.gmail.com"
backend.port = 993
backend.login = "you@gmail.com"
backend.auth.type = "oauth2"
backend.auth.client-id = "your-client-id.apps.googleusercontent.com"
backend.auth.client-secret.keyring = "gmail-oauth2-client-secret"
backend.auth.access-token.keyring = "gmail-oauth2-access-token"
backend.auth.refresh-token.keyring = "gmail-oauth2-refresh-token"
backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
backend.auth.token-url = "https://oauth2.googleapis.com/token"
backend.auth.scopes = ["https://mail.google.com/"]
message.send.backend.type = "smtp"
message.send.backend.host = "smtp.gmail.com"
message.send.backend.port = 465
message.send.backend.login = "you@gmail.com"
message.send.backend.auth.type = "oauth2"
message.send.backend.auth.client-id = "your-client-id.apps.googleusercontent.com"
message.send.backend.auth.client-secret.keyring = "gmail-oauth2-client-secret"
message.send.backend.auth.access-token.keyring = "gmail-oauth2-access-token"
message.send.backend.auth.refresh-token.keyring = "gmail-oauth2-refresh-token"
message.send.backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
message.send.backend.auth.token-url = "https://oauth2.googleapis.com/token"
message.send.backend.auth.scopes = ["https://mail.google.com/"]
The `https://mail.google.com/` scope covers both IMAP read and SMTP send. You need to duplicate the OAuth2 config for the SMTP send backend because Himalaya treats IMAP and SMTP auth independently.
Before first run, store the client secret in your keyring:
```bash
secret-tool store --label="gmail-oauth2-client-secret" service himalaya account gmail-oauth2-client-secret
Then run `himalaya list --account gmail`. It will open your browser, ask for consent, and store the access and refresh tokens in the keyring automatically.
### Outlook setup
Register an app in [Azure AD](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps). Set the redirect URI to `http://localhost:9090`. Grant `IMAP.AccessAsUser.All` and `SMTP.Send` permissions. Enable PKCE (Outlook requires it for public clients).
```toml
[accounts.outlook]
email = "you@outlook.com"
backend.type = "imap"
backend.host = "outlook.office365.com"
backend.port = 993
backend.login = "you@outlook.com"
backend.auth.type = "oauth2"
backend.auth.client-id = "your-azure-client-id"
backend.auth.client-secret.keyring = "outlook-oauth2-client-secret"
backend.auth.access-token.keyring = "outlook-oauth2-access-token"
backend.auth.refresh-token.keyring = "outlook-oauth2-refresh-token"
backend.auth.auth-url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
backend.auth.token-url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
backend.auth.pkce = true
backend.auth.scopes = ["https://outlook.office.com/IMAP.AccessAsUser.All"]
message.send.backend.type = "smtp"
message.send.backend.host = "smtp.office365.com"
message.send.backend.port = 587
message.send.backend.login = "you@outlook.com"
message.send.backend.auth.type = "oauth2"
message.send.backend.auth.client-id = "your-azure-client-id"
message.send.backend.auth.client-secret.keyring = "outlook-oauth2-client-secret"
message.send.backend.auth.access-token.keyring = "outlook-oauth2-access-token"
message.send.backend.auth.refresh-token.keyring = "outlook-oauth2-refresh-token"
message.send.backend.auth.auth-url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
message.send.backend.auth.token-url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
message.send.backend.auth.pkce = true
message.send.backend.auth.scopes = ["https://outlook.office.com/SMTP.Send"]
Note the different scopes for IMAP vs SMTP on Outlook. Gmail uses one scope for everything; Microsoft splits them.
## XOAUTH2 vs OAUTHBEARER: which method to use
Himalaya supports two SASL mechanisms for OAuth2 authentication. XOAUTH2 is the older format, used by Gmail. OAUTHBEARER is the newer RFC 7628 standard. Most people won't need to think about this because Himalaya picks the right one automatically. But if you're debugging auth failures, it helps to know the difference.
Gmail uses XOAUTH2 exclusively. Outlook supports both but defaults to XOAUTH2 as well. If you need to force one, you can set `backend.auth.method = "xoauth2"` or `backend.auth.method = "oauthbearer"` in your config. In practice, leave it unset and let Himalaya handle it.
## The redirect error everyone hits
The most common Himalaya OAuth2 failure is: `cannot wait for redirection`. This happens when Himalaya starts its local HTTP listener on port 9090 but never receives the OAuth2 callback. Causes include:
- **Port 9090 is already in use.** Check with `lsof -i :9090` and kill the conflicting process.
- **Your redirect URI doesn't match.** The URI registered in Google Cloud Console or Azure AD must be exactly `http://localhost:9090` (not `127.0.0.1`, not HTTPS).
- **A firewall is blocking localhost traffic.** Unusual but possible on locked-down corporate machines.
- **You're running headless.** Himalaya tries to open a browser. If there's no display server, it can't complete the flow.
The first three are fixable in minutes. The fourth is a fundamental design constraint.
## The headless problem (and why agents struggle with this)
Himalaya OAuth2 requires a browser. The authorization flow opens a consent page, the user clicks "Allow," and the provider redirects back to Himalaya's local server with an auth code. This is the standard OAuth2 authorization code flow, and it assumes a human is sitting in front of a screen.
If you're running an agent on a server, in a container, or in a CI/CD pipeline, there's no browser. There's no human to click "Allow." You can work around it by doing the initial auth on a desktop machine and then copying the keyring entries to the server, but the access token expires (usually in an hour), and the refresh token can be revoked at any time by Google or Microsoft. When that happens, your agent's email stops working until a human manually re-authenticates.
A [popular thread on r/openclaw](https://www.reddit.com/r/openclaw/comments/1raepk3/openclaw_himalaya_oauth2_gmail_hal_says_tool/) captures the frustration: a user had Himalaya working perfectly from the shell, but their OpenClaw agent couldn't find or use the tool because the auth session was tied to a desktop keyring that the agent process couldn't access.
This is the core tension. OAuth2 was designed to protect human users from credential theft. It does that well. But it also assumes the user is human, present, and operating a browser. Autonomous agents are none of those things.
## Token storage: keyring vs. environment variables vs. secrets managers
Himalaya stores OAuth2 tokens in the system keyring by default (GNOME Keyring, macOS Keychain, or Windows Credential Manager). The keyring entries follow the pattern you set in your config: `backend.auth.access-token.keyring` and `backend.auth.refresh-token.keyring`.
For server deployments, the system keyring often doesn't exist. Some alternatives:
- **Export tokens as environment variables** and patch the config to read from them. Himalaya doesn't natively support env var token sources, so this requires wrapper scripts.
- **Use a secrets manager** (AWS Secrets Manager, HashiCorp Vault) and inject tokens at runtime. Same wrapper-script problem.
- **Run a headless keyring daemon** like `gnome-keyring-daemon` in the container. It works, but you're adding complexity to avoid complexity.
None of these are clean. They're all duct tape over a protocol that wasn't designed for unattended use.
## When to skip Himalaya OAuth2 entirely
If you're building an interactive personal email workflow on a desktop machine, Himalaya OAuth2 works fine. Set it up once, let the keyring handle tokens, and forget about it.
If you're building something that runs unattended, on a server, as part of an agent pipeline, the OAuth2 browser redirect is a wall. You'll spend more time maintaining the auth plumbing than building the actual email features.
This is exactly the problem that agent-first email infrastructure solves. [LobsterMail](/) takes a different approach: your agent provisions its own inbox with a single SDK call, no OAuth2 flow, no browser redirect, no keyring. The agent gets an `@lobstermail.ai` address (or your custom domain) and can send and receive immediately. There's a free tier with 1,000 emails per month if you want to test it.
For the "I just need my agent to have email" use case, managed infrastructure removes the entire token lifecycle from your stack. For the "I specifically need to access my personal Gmail inbox from a terminal" use case, Himalaya with OAuth2 is still the right tool.
Pick the one that matches what you're actually building.
<FAQ>
<FAQItem question="What does backend.auth.type = 'oauth2' do in Himalaya's TOML config?">
It tells Himalaya to authenticate with the mail server using OAuth2 instead of a plain password. Himalaya will start a local HTTP server, redirect you to your provider's consent page, and exchange the authorization code for access and refresh tokens.
</FAQItem>
<FAQItem question="What is the difference between xoauth2 and oauthbearer as Himalaya auth methods?">
XOAUTH2 is the older Google-specific SASL mechanism. OAUTHBEARER is the newer RFC 7628 standard. Gmail only supports XOAUTH2. Outlook supports both. Himalaya auto-detects the right one, so you usually don't need to set it manually.
</FAQItem>
<FAQItem question="How do I register a Gmail OAuth2 app to get a client ID and client secret for Himalaya?">
Go to the [Google Cloud Console](https://console.cloud.google.com/), create a project, enable the Gmail API, then navigate to Credentials and create an OAuth 2.0 Client ID. Set the redirect URI to `http://localhost:9090`. You'll receive a client ID and secret.
</FAQItem>
<FAQItem question="How do I register an Azure AD app for Himalaya Outlook OAuth2 access?">
In the [Azure Portal](https://portal.azure.com/), go to App Registrations and create a new registration. Set the redirect URI to `http://localhost:9090`, enable PKCE, and grant `IMAP.AccessAsUser.All` and `SMTP.Send` API permissions.
</FAQItem>
<FAQItem question="Why does Himalaya fail with 'cannot wait for redirection' during the OAuth2 flow?">
This usually means port 9090 is occupied, the redirect URI in your provider's settings doesn't exactly match `http://localhost:9090`, or you're on a headless system without a browser. Check for port conflicts with `lsof -i :9090` and verify your registered redirect URI.
</FAQItem>
<FAQItem question="Do I need to compile Himalaya with a special feature flag to enable OAuth2 support?">
Pre-built binaries from the official releases include OAuth2 support. If you're building from source, make sure the `oauth2` feature is enabled in the Cargo build. Without it, the OAuth2 config fields will be silently ignored.
</FAQItem>
<FAQItem question="What OAuth2 scopes are required for IMAP access with Gmail vs. Outlook?">
Gmail uses `https://mail.google.com/` for both IMAP and SMTP. Outlook splits them: `https://outlook.office.com/IMAP.AccessAsUser.All` for reading and `https://outlook.office.com/SMTP.Send` for sending.
</FAQItem>
<FAQItem question="Does Himalaya support PKCE for OAuth2, and when should I enable it?">
Yes. Set `backend.auth.pkce = true` in your config. Outlook requires PKCE for public client flows. Gmail doesn't require it but supports it. Enabling PKCE is always a good practice for security.
</FAQItem>
<FAQItem question="How does Himalaya automatically refresh expired OAuth2 access tokens?">
When an access token expires, Himalaya uses the stored refresh token to request a new one from the provider's token endpoint. The new access token is saved back to the keyring. If the refresh token itself is revoked, you'll need to re-authenticate manually through the browser flow.
</FAQItem>
<FAQItem question="Can I use Himalaya OAuth2 in a headless or server environment without a browser?">
Not natively. The OAuth2 flow requires a browser for the consent screen. You can authenticate on a desktop, then transfer keyring entries to the server, but tokens expire and refresh tokens can be revoked. For unattended use, consider an agent-first email service like [LobsterMail](/) that doesn't require OAuth2 at all.
</FAQItem>
<FAQItem question="What is the difference between backend.auth.access-token.keyring and backend.auth.refresh-token.keyring?">
The access token keyring entry stores the short-lived token used for each IMAP/SMTP session (typically valid for one hour). The refresh token keyring entry stores the long-lived token used to obtain new access tokens without re-authenticating through the browser.
</FAQItem>
<FAQItem question="How do I configure OAuth2 for both IMAP and SMTP backends in the same Himalaya account?">
You need to specify the full OAuth2 config block under both `backend.auth` (for IMAP) and `message.send.backend.auth` (for SMTP). They can share the same client ID, secret, and keyring entries, but each needs its own auth type and scope settings.
</FAQItem>
<FAQItem question="Why would an agent using Himalaya OAuth2 fail when run outside a desktop session?">
The agent can't access the desktop keyring (which stores the OAuth2 tokens) and can't open a browser for re-authentication. This is a common issue reported by OpenClaw users running agents in background processes or containers.
</FAQItem>
<FAQItem question="What is Himalaya CLI used for?">
Himalaya is a terminal-based email client written in Rust. It handles sending and receiving email over IMAP and SMTP, outputs JSON for scripting, and supports OAuth2 authentication for Gmail and Outlook. It's popular with developers who want to manage email from the command line.
</FAQItem>
<FAQItem question="How can an email API replace managing OAuth2 tokens manually in Himalaya?">
An agent-first email API like [LobsterMail](/) provisions inboxes programmatically with no OAuth2 flow. The agent gets an email address through an SDK call, skipping the browser redirect, token storage, and refresh cycle entirely. This is useful for bots and agents that run unattended.
</FAQItem>
</FAQ>


