Field guide
Gmail's Missing From: Header
Claude can read and reply to your inbox — but it can't draft as accounts@yourbusiness.com if that's not the address it's authenticated as. Here's the actual fix, OAuth setup and all, written from a live rollout rather than the docs.
6 steps · one Google Cloud project · about 20 minutes

Why this happens
Most small businesses run several addresses off one Gmail account — dan@ and accounts@ off the same mailbox, say. Gmail itself handles this fine: add a verified "Send mail as" alias, and a human picks which address to send from right there in the compose window.
Claude's built-in Gmail integration doesn't expose that choice. It creates drafts as whichever address it's already authenticated as — there's no field for "compose this one as someone else." For most work that's irrelevant. It only bites when a business genuinely needs replies to go out from more than one address, and Claude is doing the drafting.
The fix isn't inside Claude's Gmail integration at all — it's a small, separate script that talks to the Gmail API directly, with its own narrowly-scoped credentials, just for the one thing the built-in tool can't do.
What you'll need
- A Google account with Gmail (Workspace or personal) that already has the second address set up as a verified "Send mail as" alias.
- A Claude Code environment you're the only user of — this matters, see the security note near the end.
- About 20 minutes, most of it spent clicking through a few Google screens once.
The walkthrough
Verify the alias
in Gmail settings
Before anything else, confirm the address you want to draft from is already added and verified: Settings → See all settings → Accounts and Import → Send mail as. If it's not there, add it now. Everything past this point assumes it's already verified — the eventual error if it isn't ("alias needs verifying") is easy to mistake for something more serious than a five-minute Gmail settings fix.
Create an OAuth client
in Google Cloud Console
Go to console.cloud.google.com. Any existing project works fine — you don't need a dedicated one (more on that in the field notes below). Enable the Gmail API: APIs & Services → Library → search "Gmail API" → Enable. Then APIs & Services → Credentials → Create Credentials → OAuth client ID. Choose Desktop app as the type, name it something you'll recognize, and download the resulting client_secret_....json.
Generate a token, once
in your own terminal
This step has to happen on a machine with a browser — the consent flow redirects to localhost, so it can't run inside Claude's cloud sandbox. In a fresh folder alongside the downloaded JSON file:
python3 -m venv venv
source venv/bin/activate
pip install google-auth-oauthlib google-api-python-clientThen a short script, get_token.py:
from google_auth_oauthlib.flow import InstalledAppFlow
SCOPES = ['https://www.googleapis.com/auth/gmail.compose']
flow = InstalledAppFlow.from_client_secrets_file(
'client_secret_....json', # match the exact filename you downloaded
SCOPES)
creds = flow.run_local_server(port=0)
with open('token.json', 'w') as f:
f.write(creds.to_json())
print("Done — token.json created.")Run it:
python3 get_token.pyA browser tab opens. Sign in as the account, click through the "Google hasn't verified this app" warning (expected — it's your own app, not a red flag), approve access. token.json lands in the same folder.
Store the token
in Claude Code's environment settings
This is the point to stop and read the security note below before pasting anything. Assuming it checks out: open this environment's settings in the Claude Code web UI, find Environment variables, and add a new line in .env format:
GMAIL_OAUTH_TOKEN='<paste the full contents of token.json>'Single-quote the whole value — the JSON is full of double quotes and commas that need to be treated literally, not parsed.
Add the hook
.claude/hooks/session-start.sh in the repo
This is what makes the credential available automatically every session, instead of a one-off manual fix:
#!/bin/bash
set -euo pipefail
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
exit 0
fi
CRED_DIR="$HOME/.config/gmail-oauth"
CRED_FILE="$CRED_DIR/token.json"
if [ -z "${CLAUDE_ENV_FILE:-}" ]; then
echo "session-start.sh: CLAUDE_ENV_FILE not set - skipping env export." >&2
fi
if [ -n "${GMAIL_OAUTH_TOKEN:-}" ]; then
mkdir -p "$CRED_DIR"
chmod 700 "$CRED_DIR"
printf '%s' "$GMAIL_OAUTH_TOKEN" > "$CRED_FILE"
chmod 600 "$CRED_FILE"
python3 - "$CRED_FILE" <<'PYEOF'
import json, sys
path = sys.argv[1]
with open(path) as f:
data = json.load(f)
if data.get("type") != "authorized_user":
data["type"] = "authorized_user"
with open(path, "w") as f:
json.dump(data, f)
PYEOF
chmod 600 "$CRED_FILE"
if [ -n "${CLAUDE_ENV_FILE:-}" ]; then
echo "export GOOGLE_APPLICATION_CREDENTIALS=\"$CRED_FILE\"" >> "$CLAUDE_ENV_FILE"
fi
fi
VENV_DIR="$HOME/.venvs/gmail-oauth"
if [ ! -x "$VENV_DIR/bin/python3" ]; then
python3 -m venv "$VENV_DIR"
fi
"$VENV_DIR/bin/pip" install --quiet --upgrade pip
"$VENV_DIR/bin/pip" install --quiet google-auth google-api-python-client
if [ -n "${CLAUDE_ENV_FILE:-}" ]; then
echo "export GMAIL_OAUTH_PYTHON=\"$VENV_DIR/bin/python3\"" >> "$CLAUDE_ENV_FILE"
fiRegister it in .claude/settings.json:
{
"hooks": {
"SessionStart": [
{ "hooks": [ { "type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" } ] }
]
}
}And the script that actually creates a draft with a custom From: header, using those credentials — save this wherever fits your repo:
#!/usr/bin/env python3
import base64, json, sys, argparse
from email.mime.text import MIMEText
from googleapiclient.discovery import build
import google.auth
def create_raw_draft(to_addr, subject, body, from_addr, thread_id, in_reply_to=None, references=None):
credentials, _ = google.auth.default()
service = build('gmail', 'v1', credentials=credentials)
msg = MIMEText(body)
msg['From'] = from_addr
msg['To'] = to_addr
msg['Subject'] = subject
if in_reply_to:
msg['In-Reply-To'] = in_reply_to
if references:
msg['References'] = references
raw = base64.urlsafe_b64encode(msg.as_string().encode()).decode().rstrip('=')
draft = service.users().drafts().create(
userId='me', body={'message': {'raw': raw, 'threadId': thread_id}}
).execute()
return draft['id']
if __name__ == '__main__':
p = argparse.ArgumentParser()
p.add_argument('--to', required=True)
p.add_argument('--subject', required=True)
p.add_argument('--body', required=True)
p.add_argument('--from', dest='from_addr', required=True)
p.add_argument('--thread-id', required=True)
p.add_argument('--in-reply-to')
p.add_argument('--references')
a = p.parse_args()
draft_id = create_raw_draft(a.to, a.subject, a.body, a.from_addr, a.thread_id, a.in_reply_to, a.references)
print(json.dumps({'success': True, 'draft_id': draft_id}))Publish the app
OAuth consent screen
Easy to skip, and the one step most likely to bite you a week later if you do. A freshly created OAuth app defaults to Testing status — refresh tokens issued while in Testing expire after about 7 days, regardless of how often they're used. Go to the Audience tab (Google's moved this out of the old single-page consent screen layout) and click Publish App.
Gmail's gmail.compose scope is "sensitive," not "restricted" — publishing an unverified app is fine for your own single-user use, well under Google's threshold for requiring full verification. You'll still see the "unverified app" warning on login; that's expected and safe to click through.
Regenerate the token after publishing, even if you already made one — it's not confirmed whether a token issued during Testing gets grandfathered into Production behavior once you publish, so don't rely on that. Repeat step 3, get a fresh token, replace the environment variable from step 4.
Field notes — what actually went wrong
Every one of these cost real debugging time on a live rollout. Save yourself the trip.
Mac's pip refuses a global install. Homebrew-managed Python blocks pip install outside a virtual environment with an "externally managed environment" error. Fix: always use a venv, both for the local token-generation step and inside the cloud hook.
Don't paste Python into a terminal. It runs shell commands, not Python — pasting a script inline gets a cryptic parse error. Write it to a file first.
A cloud-sandbox library conflict. Installing google-auth/google-api-python-client outside a dedicated venv can crash with a _cffi_backend/PyO3 panic from a conflicting system-level cryptography package. The hook script above already isolates this — don't skip that step even if a quick manual test seems to work without it.
A missing “type” field. The raw output of google-auth-oauthlib's creds.to_json() doesn't include "type": "authorized_user", which google.auth.default() needs to recognise the file. The hook patches this in automatically.
CLAUDE_ENV_FILE only exists during a real session start. Manually re-running the hook script to debug it will hit "unbound variable" unless guarded — the script above already guards it, but worth knowing if you're troubleshooting by hand.
No dedicated secrets store in Claude Code cloud environments. Confirmed directly against the platform docs: environment variables and setup scripts are visible to anyone who uses that environment. See the security note below.
Reusing an old Google Cloud project shows its old app name. If you create the OAuth client inside an existing project rather than a fresh one, the consent screen displays that project's configured "App name" during login — which might be left over from something unrelated. Cosmetic, doesn't affect which credential is actually used, but worth knowing so it doesn't look like you signed into the wrong thing.
Security note
Treat the token as a real secret. It's an OAuth refresh token scoped to gmail.compose — real send capability for the mailbox it's tied to, not just draft-and-wait-for-a-human. Never commit it to a repo. And because Claude Code's cloud environments currently have no dedicated secrets store, only put it there if you're genuinely the sole user of that environment — anyone else with access to it could read it too.
The prompt to use
Rather than working through all six steps by hand, paste the following into a fresh Claude Code session and let it drive the parts that don't need you at a keyboard elsewhere:
Copy this prompt
I want Claude Code to be able to draft Gmail replies from a specific address — [your-second-address@yourbusiness.com] — that isn't the one it's normally authenticated as. The standard Gmail integration can't set a custom From: header, so I need a small direct-API script plus a way to keep its credentials available every session. Please:
- First remind me to confirm [your-second-address@yourbusiness.com] is already a verified "Send mail as" alias in Gmail settings — don't proceed until I've checked.
- Walk me through creating a Google Cloud OAuth 2.0 "Desktop app" client with the Gmail API enabled, and getting me to run a short local script on my own machine (it needs a browser, so it can't run in this session) to generate a refresh token.
- Before I paste that token anywhere, tell me clearly that this environment's variables are visible to anyone who uses it — there's no dedicated secrets store — and ask me to confirm I'm the only person using this environment before proceeding.
- Write a SessionStart hook that materializes the token to a credentials file at the start of every session, patches in a "type": "authorized_user" field if it's missing (required by google.auth.default() but not included by default), and installs google-auth plus google-api-python-client into an isolated Python venv — some cloud sandboxes have a system-level cryptography conflict that breaks these packages outside one.
- Register the hook in .claude/settings.json.
- Write a script that builds a raw MIME message with a custom From: header and creates a Gmail draft via the API using those credentials.
- Test it end-to-end with an actual draft, not just a credentials-load check.
- Remind me to check my OAuth app's publishing status (Google Cloud Console → OAuth consent screen → Audience tab) and publish it — a freshly created app defaults to "Testing," where refresh tokens expire after about a week regardless of use.
- If I already generated a token before publishing, have me regenerate it afterward, since it's unclear whether Google carries an already-issued token over into Production behavior.
Work through this step by step and check in with me at each point that needs me to act somewhere else — Google Cloud Console, my own terminal — rather than doing it all silently.
This is exactly the kind of small-but-fiddly integration work Dan's Systems helps businesses get right the first time. If you'd rather have it set up for you than do it yourself, get in touch.