Paste emails from Claude Code into 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.
Copy this into Claude Code.
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 asusual, then pipe the body (not the subject line) into ~/.claude/bin/gmail-clipso 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 itsown compose style, same as typed text. A plain-text flavor is set too."""import htmlimport osimport reimport subprocessimport sysimport 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.