Seal the room after you leave it.
A small CLI for sending personalized, human-feeling follow-up emails after a talk, meeting, or event. One email per recipient, with a per-recipient signed PDF link. Built for VIP-list size sends (≤50), not bulk marketing.
Vietnamese-first template (anh / em / bạn tone field built in). Cloudflare Email Service for sending, R2 + Workers for per-recipient PDF link delivery, KV for click tracking. No mailchimp, no API key for content generation — rewrites happen in Claude Code via the /email-rewrite slash command.
$ sig init
sigil init — campaign workspace setup
workspace: /Users/you/Documents/talk-2026-05
? CF API token * ••••••••…7a25
? CF account ID * 1a2b3c…
? From name * Trí Võ
? From email * tri@mtri.me
? Event name * Sài Gòn AI · May
? Event day phrase sáng thứ Năm
? Email subject * Slides PDF + lời cảm ơn
? Test email addresses (comma-separated) tri@mtri.me, qa@biplus.com.vn
? Agent shims to install claude, antigravity
Validating CF token … ok (id=8f2c4a1d…)
.env changes:
+ CF_API_TOKEN=<masked …7a25>
+ EVENT_NAME=Sài Gòn AI · May
+ EMAIL_SUBJECT=Slides PDF + lời cảm ơn
+ TEST_EMAILS=tri@mtri.me, qa@biplus.com.vn
? Apply changes? Yes
✓ wrote .env
✓ .claude/commands/email-rewrite.md
✓ .antigravity/commands/email-rewrite.md
$ gog sheets get 1A2b… 'A1:Z' --plain | sig ingest --stdin
Wrote 23 leads to data/leads.json
$ sig review --table
┌──┬──────────────────────┬───────────┬──────┬─────────┬──────────┬────────┬────┬────┐
│ #│ email │ name │ tone │ optin │ token │ sent │test│ │
├──┼──────────────────────┼───────────┼──────┼─────────┼──────────┼────────┼────┼────┤
│ 1│ ai@example.com │ Ai │ bạn │ Có │ Kqdqs… │ — │ — │ │
│ 2│ trieu@example.com │ Triều │ anh- │ Có │ aB91p… │ — │ — │ │
│ …│ … 21 more … │ │ │ │ │ │ │ │
└──┴──────────────────────┴───────────┴──────┴─────────┴──────────┴────────┴────┴────┘
23 leads · 23 unsent · 23 no-sentence · 0 no-token
$ sig prompt
Reviewing 23 lead(s). Actions: <Enter>=keep · /default · ?<prompt> · <new sentence> · /quit
────────────────────────────────────────────────────────────────────────────────
1/23 Ai <ai@example.com> tone=bạn
> ?reference her question about RAG retrieval latency in production
2/23 Triều <trieu@example.com> tone=anh-em
> /default
…
Saved. kept=4 default=9 overwritten=2 queued=8
8 rewrite(s) queued in data/rewrite-queue.json.
Run `/email-rewrite` in Claude Code chat to process.
# (in Claude Code chat:)
/email-rewrite
→ walks 8 queued rows, per-row diff + keep/regenerate/skip,
writes back to data/leads.json, clears queue
$ sig prep
Minted 0 new token(s). Total leads: 23
Wrote worker/tokens.json (23 tokens)
Updated TOKENS_JSON in worker/wrangler.toml
$ cd worker && npx wrangler deploy && cd ..
✓ Deployed pdf-sigil triggers (https://pdf.mtri.me/*)
$ sig send --test
[DRY=false] TEST → tri@mtri.me, qa@biplus.com.vn | 23 lead(s) × 2 addr = 46 send(s)
✓ ai@example.com → tri@mtri.me cf-msg-id-…
✓ ai@example.com → qa@biplus.com.vn cf-msg-id-…
✓ trieu@example.com → tri@mtri.me …
… (44 more) …
$ sig send
PROD | 23 lead(s)
About to send 23 live email(s) (ai@example.com … omar@example.com). Continue? [y/N] y
✓ ai@example.com → ai@example.com cf-msg-id-…
… (22 more) …
$ sig status
EMAIL SENT (UTC) CLICKS LAST CLICK
ai@example.com ✓ 2026-05-16 14:22Z 3 2026-05-16 14:24Z
trieu@example.com ✓ 2026-05-16 14:22Z 1 2026-05-16 15:01Z
…
Same surface, scriptable:
sig --version # 0.2.0
sig review --json | jq '.[] | select(.sentAt == null) | .email'
sig status --json | jq '[.leads[] | {email, clicks: .clickStats.count}]'
sig send --yes --only=foo@bar.com # bypass interactive confirm
NO_COLOR=1 sig review --table # zero ANSI in captured outputgit clone <this-repo> ~/Documents/sigil
cd ~/Documents/sigil
npm install
npm link # makes `sig` globally availablePer campaign, in any working directory:
# 1. Set up
sig init # English TUI; writes .env, drops agent shims
cp ~/Documents/sigil/worker/wrangler.example.toml worker/wrangler.toml
# edit wrangler.toml (worker name, route, KV/R2 bindings)
# put your Google Forms CSV at data/leads.csv (TSV from Sheets paste works)
# or pipe directly:
# gog sheets get <ID> 'A1:Z' --plain | sig ingest --stdin
# 2. Workflow
sig ingest # parse data/leads.csv → leads.json
sig review # cards (--table compact, --diff vs last send)
sig prompt # interactive per-row review
# ? prompts queued for /email-rewrite
sig prep # mint tokens, rewrite wrangler.toml block
cd worker && npx wrangler deploy && cd ..
sig preview --email=foo@bar.com # render check (warns on unresolved {{var}})
sig send --test=you@example.com # land all personalized emails in one inbox
sig send # real send (asks y/N first)
sig status # sent + clicks + deliveryMulti-lang campaigns: set BODY_TEMPLATE=templates/body.en.md in .env
to use the bundled English template. Leave unset for the default Vietnamese
body. Fork templates/body.md into your own file for a custom layout —
single file with YAML frontmatter subject: then text body, then --- html ---,
then HTML body.
In Claude Code chat (after sig prompt queued some ? prompts):
/email-rewrite
…walks the queue with per-row approval + diff, writes rewrites back to data/leads.json.
A sigil is a personal seal — pressed into wax to close a letter, marking authorship. Across cultures it's the mark of "this came from me." That's the product promise: every email is sealed, identifiable, and addressed to one person. Not a blast.
Two strong syllables. Easy on a sales call. Distinct from mailer SaaS.
- Not bulk email. There's no list of 10k. Quota gates are real but small (CF Email Service: 1000/day on Workers Paid).
- Not a CRM. State lives in one JSON file per campaign.
- Not deliverability magic. Reputation is your sending domain's job.
- Not English-only — but the default template is Vietnamese. To swap, fork
cli/lib/template.ts.
data/leads.csv ─▶ leads.json ──┬─▶ sig review / preview
│
├─▶ sig prep (mint token per row,
│ write worker/tokens.json + wrangler.toml block)
│
└─▶ sig send ──▶ Cloudflare Email Service API
│
pdf.<your-domain>/p/:token ◀────────── recipient ◀──┘
│
├─ Worker: token in TOKENS_JSON?
├─ KV.put(token, {firstClickAt, lastClickAt, count, lastUa, lastIp})
└─ stream PDF from R2 binding PDF_BUCKET
sig … |
Purpose |
|---|---|
init |
TUI workspace setup (or --non-interactive + flags); writes .env, drops agent shims |
ingest |
parse data/leads.csv → data/leads.json (also --stdin, --json) |
review |
card view (--table compact, --diff vs last send, --json) |
preview --email=X / --all |
fully rendered email; warns on unresolved {{var}} |
prompt |
interactive review — keep / default / overwrite / queue ?prompt |
prep |
mint tokens, write worker/tokens.json + inject TOKENS_JSON into wrangler.toml |
send --test |
use TEST_EMAILS from .env (or --test=a@x.com,b@y.com) |
send |
real send (asks y/N; --yes bypasses) |
send --force |
resend even if already sentAt (use with care) |
send --only=X |
single recipient |
status |
sent + per-token PDF clicks + CF feedback (--json available) |
Global flags: -h/--help, -v/--version, --no-color (also honors NO_COLOR, TERM=dumb), --no-input.
Human-paced run:
sig ingest # parse data/leads.csv
sig review --diff # inspect, see what would change vs last send
sig prompt # walk each row; type ?<hint> to queue a rewrite
# (in Claude Code chat:) /email-rewrite
sig prep # mint tokens, write worker bindings
cd worker && npx wrangler deploy && cd ..
sig send --test=you@example.com # land all N personalized copies in your inbox
sig send # asks y/N first, then sends for real
sig status # sent + clicks + delivery feedbackScripted / agent-callable run (no human at the keyboard):
sig --version # capability gate
gog sheets get <ID> 'A1:Z' --plain | sig ingest --json | jq 'length'
sig review --json | jq '.[] | select(.sentAt == null) | .email'
sig send --yes # bypass the confirm gate
sig send --only=foo@bar.com --yes
sig status --json | jq '.leads[] | {email, sent: (.sentAt != null), clicks: .clickStats.count}'
NO_COLOR=1 sig review --table # ANSI strippedExit codes: 0 ok · 1 runtime/API · 2 usage/validation · 3 env missing.
Canonical instructions live at prompts/email-rewrite.md (agent-neutral).
Each agent's slash-command shim is a one-line pointer dropped by
sig init --agents=….
| Agent | Where the shim lands | Surface |
|---|---|---|
| Claude Code | .claude/commands/email-rewrite.md |
/email-rewrite |
| Antigravity | .antigravity/commands/email-rewrite.md |
/email-rewrite |
| Codex | none (reads AGENTS.md) |
do email-rewrite |
What it does: processes data/rewrite-queue.json with per-row approval +
diff, writes to leads.json, clears queue entry.
Each campaign needs:
- Email Service onboarded for the sending domain (
tri@mtri.me-style address verified). Workers Paid plan ($5/mo) to send to non-verified destinations. - R2 bucket for the PDF.
- Worker at a route like
pdf.example.com(custom domain). - KV namespace for click stats.
See docs/howto.md for the full step-by-step. API token needs:
Email Sending: Edit(for/send)Email Sending: Read(for/feedbackinstatus)Workers KV Storage: Read(for click stats instatus)Workers Scripts: Edit+Workers R2 Storage: Edit(for wrangler deploys)
bin/sig.mjs single-entry dispatcher
cli/
init.ts ingest.ts review.ts preview.ts prompt.ts prep.ts send.ts status.ts
lib/ csv state token template email-cf cli-format cf-api argv
init-fields init-writers
templates/
body.md default VN template (mustache {{vars}})
body.en.md English example template
prompts/
email-rewrite.md canonical agent-neutral rewrite-queue prompt
.claude/commands/email-rewrite.md shim → prompts/email-rewrite.md
.antigravity/commands/email-rewrite.md shim → same
worker/
wrangler.example.toml starter — copy to wrangler.toml per campaign
src/index.ts GET /p/:token → log + KV + R2 stream
docs/howto.md
.env.example copy to .env (or run `sig init`)
AGENTS.md briefing for any agent session
Built alongside cc4.marketing — a free interactive course on using Claude Code for marketing workflows. Sigil is the post-event follow-up tool the course's curriculum gestures at, shipped as something you can actually run. The /email-rewrite slash command + per-row handcrafting is exactly the kind of agent-in-the-loop pattern the course teaches.
If you want help setting sigil up for your own event (CF onboarding, custom template, your first send), reach out at mtri.me — done-with-you setup is a thing I'll do.
MIT © 2026 Minh Tri Vo.
Use it, fork it, ship something with it. Pull requests welcome but not expected — sigil is a personal tool that just happens to be open source. If you build something on top, I'd love to hear about it.