# Paste emails from Claude Code into Gmail

> A small script that puts the emails Claude Code writes on your Mac clipboard in Gmail's own formatting, so they paste in looking typed.

Source: https://completeseo.com/docs/claude-code/emails-to-gmail/

When Claude Code writes an email and you copy it out of the chat, Gmail picks up the chat's font and loses the space between paragraphs. Remove formatting fixes the font, but it strips numbered lists too.

This script puts the email on your clipboard the way Gmail formats typed text, with a blank line between paragraphs and Gmail's own numbered and bulleted lists. It sets no font or color, so the email takes your Gmail default. It runs on a Mac.

## Setup

Copy this into Claude Code.

```text
Save the script below as ~/.claude/bin/gmail-clip and make it executable.
From now on, whenever you write an email for me, show it in the chat as
usual, then pipe the body (not the subject line) into ~/.claude/bin/gmail-clip
so it's on my clipboard. Remember this as a standing instruction.

#!/usr/bin/env python3
"""Put an email draft on the macOS clipboard as Gmail-native HTML.

Usage: gmail-clip [file.md]   (reads stdin when no file is given)

Input is the email body as plain markdown-ish text:
  blank line          -> paragraph break (Gmail's <div><br></div>)
  each other line     -> its own line (Gmail's <div>)
  "1. " / "- " lines  -> numbered / bulleted list (Gmail's <ol>/<ul>)
  **bold**, [text](url)
A leading "> " on lines is stripped, so a chat blockquote pastes as-is.

The HTML carries no fonts, colors, or margins, so Gmail renders it in its
own compose style, same as typed text. A plain-text flavor is set too.
"""
import html
import os
import re
import subprocess
import sys
import tempfile

OL = re.compile(r"^\s*\d+[.)]\s+(.*)$")
UL = re.compile(r"^\s*[-*•]\s+(.*)$")


def inline(s):
    s = html.escape(s, quote=False)
    s = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", s)
    s = re.sub(
        r"\[([^\]]+)\]\((https?://[^)\s]+|mailto:[^)\s]+)\)",
        lambda m: f'<a href="{html.escape(m.group(2))}">{m.group(1)}</a>',
        s,
    )
    return s


def to_html(text):
    lines = [re.sub(r"^>\s?", "", l.rstrip()) for l in text.strip("\n").split("\n")]
    out, i = [], 0
    while i < len(lines):
        line = lines[i]
        m_ol, m_ul = OL.match(line), UL.match(line)
        if m_ol or m_ul:
            tag, pat = ("ol", OL) if m_ol else ("ul", UL)
            items = []
            while i < len(lines) and pat.match(lines[i]):
                items.append(pat.match(lines[i]).group(1))
                i += 1
                # indented continuation lines belong to the item
                while i < len(lines) and lines[i].startswith((" ", "\t")) and lines[i].strip() and not (OL.match(lines[i]) or UL.match(lines[i])):
                    items[-1] += " " + lines[i].strip()
                    i += 1
            lis = "".join(f'<li style="margin-left:15px">{inline(t)}</li>' for t in items)
            out.append(f"<{tag}>{lis}</{tag}>")
            # Gmail's list margins already give the gap; skip one blank line after
            if i < len(lines) and not lines[i].strip():
                i += 1
            continue
        if not line.strip():
            # skip the blank line right before a list (list margin covers it)
            nxt = lines[i + 1] if i + 1 < len(lines) else ""
            if not (OL.match(nxt) or UL.match(nxt)):
                out.append("<div><br></div>")
        else:
            out.append(f"<div>{inline(line)}</div>")
        i += 1
    return "".join(out)


def to_plain(text):
    lines = [re.sub(r"^>\s?", "", l.rstrip()) for l in text.strip("\n").split("\n")]
    s = "\n".join(lines)
    s = re.sub(r"\*\*(.+?)\*\*", r"\1", s)
    s = re.sub(r"\[([^\]]+)\]\(([^)\s]+)\)", r"\1 (\2)", s)
    return s


JXA = r"""
ObjC.import('AppKit');
function run(argv) {
  var read = function (p) { return $.NSString.stringWithContentsOfFileEncodingError(p, $.NSUTF8StringEncoding, null); };
  var pb = $.NSPasteboard.generalPasteboard;
  pb.clearContents;
  pb.setStringForType(read(argv[0]), $.NSPasteboardTypeHTML);
  pb.setStringForType(read(argv[1]), $.NSPasteboardTypeString);
  return 'ok';
}
"""


def main():
    text = open(sys.argv[1], encoding="utf-8").read() if len(sys.argv) > 1 else sys.stdin.read()
    body = '<meta charset="utf-8">' + to_html(text)
    with tempfile.TemporaryDirectory() as d:
        hp, tp = os.path.join(d, "h.html"), os.path.join(d, "t.txt")
        open(hp, "w", encoding="utf-8").write(body)
        open(tp, "w", encoding="utf-8").write(to_plain(text))
        r = subprocess.run(["osascript", "-l", "JavaScript", "-e", JXA, hp, tp], capture_output=True, text=True)
    if r.returncode != 0 or "ok" not in r.stdout:
        sys.exit(f"clipboard write failed: {r.stderr.strip()}")
    if os.environ.get("GMAIL_CLIP_DEBUG"):
        print(body)
    print("Copied for Gmail.")


if __name__ == "__main__":
    main()
```

After that, every email Claude Code writes you is already on your clipboard. Press Cmd+V in Gmail. If you've copied something else since, ask Claude Code to copy it again.
