Gmail (read-only)
The Gmail plugin gives the assistant three tools — gmail_search, gmail_read, gmail_labels — and
nothing else. It is read-only, on purpose (see Why read-only).
Installing it takes a tap. Connecting it takes about ten minutes the first time, because you supply your own OAuth client, and the rest of this page is why that is unavoidable and what to click.
Install it
Section titled “Install it”Open the library in the app or the browser UI, filter to Plugins, pick Gmail (read-only),
install. What you are agreeing to is shown before the button, read out of the manifest: the tools it exposes, the base tools its guest may call (http_read and
nothing more), and the host a credential would be attached to (gmail.googleapis.com).
Its code runs in the WASM sandbox — no ambient authority, brokered imports only, memory-capped and
deadline-bounded — and every request it makes still meets the gate like
any other. The plugin also brings a SKILL.md, which is why the assistant knows Gmail’s query syntax
instead of guessing at it.
Nothing works yet. The plugin has no account.
Why you register your own OAuth client
Section titled “Why you register your own OAuth client”gmail.readonly is what Google calls a restricted scope. An application that ships one OAuth
client for all its users must pass Google’s verification and an annual third-party security
assessment (CASA) — for Gmail scopes, a full penetration test by an approved lab, repeated every
twelve months. No self-hosted assistant is going to ship that, and we would not want to: every
household’s mail would then run through a single Google project, ours.
So the client is yours. It costs nothing, and Google asks nothing of you for using it with your own account.
Create it
Section titled “Create it”- Open the Google Cloud Console and create a project — any name; it is yours alone.
- APIs & Services → Library → enable the Gmail API.
- APIs & Services → OAuth consent screen: choose External, fill in an app name and your own address as support and developer contact.
- Set the publishing status to “In production”. (Verification and the CASA assessment are what an app needs to serve OTHER people; your own client, used by your own account, needs neither — what unverified costs you is the warning screen and a cap of 100 users.) This is the step everybody skips and everybody regrets: while the consent screen sits in Testing, Google revokes the refresh token after 7 days and the assistant loses access every week. In production it keeps working. Your own unverified app shows a “Google hasn’t verified this app” screen the first time — click through it under Advanced. Verification only matters for handing the app to other people.
- Add the scope
https://www.googleapis.com/auth/gmail.readonly. - Credentials → Create credentials → OAuth client ID → Desktop app. Copy the client id and the client secret.
Connect it
Section titled “Connect it”On the machine running the daemon, once:
printf %s 'GOCSPX-xxxxxxxx' | nocturn auth gmail -client-id 123456.apps.googleusercontent.com -client-secret-stdinThe secret goes in on stdin, not as a flag: a flag lands in your shell history and in every ps
on the machine. Google calls a desktop client’s secret non-confidential, which is a claim about
Google’s threat model rather than about yours.
It prints a URL. Open it, pick the account, click through the unverified-app screen. The token and
the client land encrypted in that extension’s own folder
(workspaces/<ws>/extensions/gmail/secrets.enc) — not in the workspace vault, not in the manifest, and
never in the assistant’s context. Re-running nocturn auth gmail later needs no flags: the client is
remembered.
The vault must be unlocked for this, so NOCTURN_MASTER_PASSPHRASE has to be set — see
Vault.
What happens on the first call
Section titled “What happens on the first call”The assistant calls gmail_search, and the gate asks you — once — about gmail.googleapis.com, on
your phone or in the app. The token is stamped in host-side at the boundary; the plugin’s code never
sees it, and neither does the model.
If you skipped the connect step, the tool says so rather than failing obscurely:
secret "plugin:gmail/account": secret not found — the gmail account is notconnected yet; run: nocturn auth accountWhy read-only
Section titled “Why read-only”The gate’s target on the network axis is a host. One approval for gmail.googleapis.com would
cover reading and sending alike, and “may send mail” is not something to hand over as a side effect of
“may read mail” — least of all for an assistant that reads text other people wrote, which is the
prompt-injection surface par excellence.
Sending belongs to a tool that can ask about a recipient. That is what the mail package is being built for, and it will cover IMAP accounts generally.
The alternative: IMAP with an app password
Section titled “The alternative: IMAP with an app password”If none of the above appeals, wait for that mail tool. IMAP with an app password sidesteps the whole apparatus — no Cloud project, no consent screen, no publishing status, no weekly expiry — and the same credential shape covers iCloud, Fastmail, mailbox.org, a server of your own, and Proton through its Bridge. Gmail supports app passwords too, once two-factor authentication is on.
What installing it grants
Section titled “What installing it grants”| Tools it exposes | 3 |
| Base tools its guest may call | http_read |
| A credential rides to | gmail.googleapis.com |
| Sign-in asks for | https://www.googleapis.com/auth/gmail.readonly |
| Bundled skill | gmail — its description joins the prompt catalog |
| Its code runs in the WASM sandbox: no ambient authority, brokered imports only, memory-capped | |
| and deadline-bounded. What the sandbox does not decide is the table above — that is the review | |
| surface, and every effect past it still meets the gate. |
| Tool | What it does |
|---|---|
gmail_search |
Search the user’s Gmail and return matching messages as a compact list (id, from, subject, date, snippet). Use Gmail’s own query syntax, e.g. “is:unread”, “from:anna newer_than:7d”, “has:attachment invoice”. |
gmail_read |
Read one Gmail message in full: headers and the plain-text body. Takes an id from gmail_search. |
gmail_labels |
List the names of the user’s Gmail labels and mailboxes, so a search can be scoped to one with label: |