Skip to content

SOPs

Notify-James Pattern — Send a Resend Email From Any Script

When a script needs to tell James something happened — a build patched, a daily digest is ready, a CRM contact came in hot — it should not invent its own email path. There is one canonical pattern for “fire an email from a script”, and it lives in a single helper.

This page is that pattern, named so it is easy to point Claude at: “use the notify-James pattern for this alert.”

Notify-James pattern — outbound email stack

Three callers (any script under ~/apps/) shell out to the helper. The helper hits Resend over HTTPS. Resend signs the message with the jameshurst.com DKIM key and delivers as Claude Code <claude@jameshurst.com>. James reads it in his normal Gmail inbox like any other email.

AnchorValue
Repository~/apps/gmail-notify/ (NOT gmail-helper — that one reads the inbox, opposite job)
Helper script~/apps/gmail-notify/send-email.py
From display nameClaude Code
From emailclaude@jameshurst.com
To (default)ojhurst@gmail.com
ServiceResend (HTTPS POST to api.resend.com/emails)
AuthRESEND_API_KEY env var (fallback baked into the script)
DKIM domainjameshurst.com (signed by Resend on James’s behalf)

There are two repos with similar names that do opposite jobs:

  • ~/apps/gmail-helper/ — INBOUND. Reads the inbox, searches, archives, triages. OAuth-based. Used by /gmail skill, inbox triage flows.
  • ~/apps/gmail-notify/ — OUTBOUND. Sends email out via Resend. Used by every “alert me when X happens” automation.

The mnemonic: Helper reads. Notify sends.

Shell out from any script. Plain text:

Terminal window
python3 ~/apps/gmail-notify/send-email.py \
--to "ojhurst@gmail.com" \
--subject "Subject line" \
--body "Plain text body"

Stdin works too:

Terminal window
echo "Body from stdin" | python3 ~/apps/gmail-notify/send-email.py \
--to "ojhurst@gmail.com" \
--subject "Subject line"

HTML body (with optional plain-text fallback):

Terminal window
python3 ~/apps/gmail-notify/send-email.py \
--to "ojhurst@gmail.com" \
--subject "Subject" \
--html "<h1>Rich body</h1><p>...</p>" \
--body "Plain-text fallback"

Override the from address with --sender if a specific automation needs its own display name. Default is Claude Code <claude@jameshurst.com>.

The right pattern: only email when something actually changed or actually needs attention. Cron jobs that fire every minute should not email every minute. Wrap the send in a “did anything happen” check:

Terminal window
OUTPUT=$(run_the_thing)
if echo "$OUTPUT" | grep -q "PATCHED:"; then
python3 ~/apps/gmail-notify/send-email.py --to ojhurst@gmail.com --subject "..." --body "$OUTPUT"
fi

Add a cooldown lock in /tmp/<job>.email.lock for jobs whose triggers can fire in bursts (launchd WatchPaths is the typical case — multiple file events per real change).

Existing callers (proof this is the well-trodden path)

Section titled “Existing callers (proof this is the well-trodden path)”

Every one of these scripts uses ~/apps/gmail-notify/send-email.py:

  • ~/apps/utils/claude-code-vscode/watch-and-patch.sh — VS Code extension auto-update notifications
  • ~/apps/claude-code-crm/notify.py — Go-Kart Park CRM alerts
  • ~/apps/claude-code-crm/morning_digest.py — daily owner brief
  • ~/apps/claude-code-crm/daily_review.py
  • ~/apps/claude-code-crm/messaging.py
  • ~/apps/cloudflare-audit/audit.py
  • ~/apps/pokemasters-blog/scripts/daily-stats-email.py
  • ~/apps/meal-tracker/bin/digest.py
  • ~/apps/gkp-facebook-ad-scheduler/toggle.py

When adding a new “tell me when X happens” automation, copy from one of these — do not write a new email path.

send-email.py honors DIGEST_CAPTURE_DIR — when set, it writes the email payload to a JSON file in that directory instead of sending. daily-digest.py uses this to consolidate many small sub-emails into one daily digest. Useful when you want to aggregate rather than spam.

The “why we use Resend at all, and how DKIM is set up” pattern is documented separately in Custom Domain Email — Cloudflare Routing + Resend + Gmail Alias. That page covers the broader stack (inbound routing via Cloudflare + outbound via Resend + Gmail alias for compose). This page is purely about the outbound helper.

  • RESEND_API_KEY invalid or missing → script exits 1 with the Resend error body printed to stderr. Cron jobs should pipe stderr to a log so the failure is visible.
  • Recipient bounces / Resend reputation issue → check the Resend dashboard for delivery status. Free tier is 3,000 emails/month — only relevant if a runaway loop sends thousands.
  • DKIM domain ever changes → update the Resend domain config; the from-address constant in send-email.py does not need to change as long as the domain is jameshurst.com.

The string “notify-James pattern” is a specific phrase Claude Code knows. Saying “use the notify-James pattern for this” in a task is unambiguous — it resolves to this page.