| name | gitmail |
| description | Drive the gitmail outreach flow that sends personalized cold emails to GitHub stargazers of similar repos. Batches the user's four upfront decisions (language / subject style / targeting strategy / recipient count) into a single AskUserQuestion call before any heavy work runs, finds similar repos and collects recipient emails, renders a fast single-LLM-call dry-run preview of the email body, and gates live SMTP send strictly behind an explicit user confirmation. |
| level | 3 |
gitmail Skill
Goal: when the user types /gitmail <project>, Claude analyzes the project, asks four upfront decisions in one batch, renders a fast single-LLM-call body preview, and only sends for real after the user has explicitly said "send" / "๋ฐ์กํด์ค".
Trigger phrases
Auto-trigger on:
/gitmail
- "gitmail", "gitmail ํด์ค", "gitmail ๋ณด๋ด์ค", "gitmail outreach"
- "gitmail this project", "gitmail์ผ๋ก ๋ฉ์ผ ๋ณด๋ด์ค", "gitmail๋ก ํ๋ณดํด์ค"
- Korean transliteration: "๊น๋ฉ์ผ", "๊น๋ฉ์ผ ์จ์", "๊น๋ฉ์ผ ์จ์ค", "๊น๋ฉ์ผ๋ก ๋ณด๋ด์ค", "๊น๋ฉ์ผ ๋ฐ์ก", "๊น๋ฉ์ผ ๋๋ ค์ค"
- GitHub-mail intent: "๊นํ๋ธ ๋ฉ์ผ", "๊นํ๋ธ ๋ฉ์ผ ๊ธฐ๋ฅ", "๊นํ๋ธ ๋ฉ์ผ ๊ธฐ๋ฅ์ผ๋ก", "๊นํ๋ธ ๋ฉ์ผ ๊ธฐ๋ฅ ์จ์", "GitHub ๋ฉ์ผ ๊ธฐ๋ฅ์ผ๋ก", "github ๋ฉ์ผ๋ก ์๋ ค์ค"
Korean: "์ด ํ๋ก์ ํธ ํ๋ณด๋ฉ์ผ ๋ณด๋ด์ค", "GitHub ์คํ๊ฑฐ์๊ฒ ๋ฉ์ผ", "๋น์ทํ ๋ ํฌ ์ฌ์ฉ์ํํ
๋ฉ์ผ", "์ด๊ฑฐ ๋ฉ์ผ๋ก ์๋ ค์ค", "asyncprofiler ๋ณํํ ์ฌ๋ํํ
๋ณด๋ด์ค"
English: "email people who starred similar repos", "send a launch outreach to stargazers", "promote my project via cold email"
Chinese: "็ป็ฑปไผผไปๅบ็ stargazer ๅ้ฎไปถ", "ๆจๅนฟๆ็้กน็ฎ ้ฎไปถ"
Japanese: "ไผผใใชใใธใใชใฎในใฟใผใฌใถใผใซใกใผใซ", "ใใญใธใงใฏใใ็ดนไปใใใกใผใซ"
Disambiguation rule: when the user's intent is generic ("๋ฐ์ด๋ด ํด์ค" / "promote this" / "make it viral") without mentioning email, mail, gitmail, or GitHub stargazers, do NOT auto-trigger gitmail โ defer to the viral skill's router (X / Reddit / Gitmail 3-way question). gitmail only auto-triggers when the words above name email/mail/gitmail explicitly.
When the user enters via /gitmail, the argument parsing in commands/gitmail.md runs first.
Pre-flight
Binary check (run this first). The skill drives the viralman console-command and its subcommands (viralman gitmail, viralman save-creds, โฆ). The binary is created by /viralman-setup Step 0; if which viralman is empty, route the user to /viralman-setup and stop. Do not ls/find/probe ~/.claude/plugins/cache/viralman/** to find scripts directly โ the permission layer flags that traversal as credential discovery and blocks it. There is no longer a "switch to the repo" requirement: viralman works from any cwd.
Permission denial protocol. If a Bash invocation in this skill (e.g. viralman gitmail recipients --max-users 500) is denied by the Claude Code permission layer or auto-mode classifier โ typical reasons include "mass scraping" / "unsolicited cold email" classifications, even though the run is the user-initiated gitmail flow โ do not attempt to self-edit ~/.claude/settings.json or .claude/settings.local.json. Claude Code's harness blocks agent self-permission-grants and the second attempt will be denied for that reason instead. Surface the snippet to the user verbatim and stop:
๋ช
๋ น์ด ๊ถํ ๋ ์ด์ด์ ๋งํ์ต๋๋ค. ~/.claude/settings.json ์ permissions.allow
๋ฐฐ์ด์ ์๋๋ฅผ ์ถ๊ฐํ์๋ฉด (ํ ๋ฒ๋ง paste, ์ ์ธ์
๋ถํฐ ์ ์ฉ) ๋ค์๋ถํฐ๋
๊ฐ๋ฒผ์ด ๋ช
๋ น๋ค์ด ๋งค๋ฒ ๋ฌป์ง ์๊ณ ๋ฐ๋ก ์งํ๋ฉ๋๋ค:
"Bash(viralman:*)"
์์ธํ ์๋ด๋ /viralman-setup์ Step 5 ์ฐธ์กฐ. auto-mode classifier ๋ ๋ณ๊ฐ
๋ ์ด์ด๋ผ ๋๋ ๋ฐ์ก๋ฅ๋ ์ ๋ฃฐ์ ์ถ๊ฐํด๋ ํ ๋ฒ ๋ค์ด์ผ๋ก๊ทธ๊ฐ ๋ฐ ์ ์์ต๋๋ค โ
๊ทธ๋ ํ ๋ฒ๋ง Allow ๋๋ฌ์ฃผ์ธ์.
Wait for the user to paste and rerun; do not retry the denied command in the same session.
Run viralman save-creds --show-keys and confirm (never print the values):
GITHUB_TOKEN โ without it, GitHub API caps at 60 req/h.
- one of
ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY, or detect Claude Code CLI via which claude.
- For real send only:
SMTP_HOST, SMTP_USER, SMTP_PASSWORD, SMTP_FROM.
If anything is missing, route the user to /viralman-setup gitmail and stop. Never read or print .env values.
Step 1 โ Project intent capture
Follow skills/copy-prep/SKILL.md ยงProject intent capture for the URL parsing rule, the keyword derivation, and the 2โ3 line confirmation print. The struct it produces (url / name / description / keyword) feeds Step 3's viralman gitmail recipients --description "$DESC" --project-url "$URL".
Step 2 โ Batch question (AskUserQuestion, exactly once, before any viralman gitmail call)
Critical: do not run viralman gitmail recipients or send-from-recipients until the batch question is answered. Letting the user decide upfront is the core of this skill.
Use the AskUserQuestion tool to surface all four questions in one call (multiSelect=false on every one):
Q1 โ Language
Use the option list from skills/copy-prep/SKILL.md ยงLanguage picker. The selected value becomes a prefix to --tone (e.g. English โ --tone "in English, ..."). For Korean, the prefix is omitted.
Q2 โ Subject style
Use the 5-option table from skills/copy-prep/SKILL.md ยงSubject format presets. Pass each option's example into the preview field of AskUserQuestion. The manual (์ง์ ์
๋ ฅํ๊ธฐ) option must be last.
If the user picks manual, follow ยงManual override pattern from copy-prep โ gitmail's no-LLM path is --prewritten-subject "<subject>" --prewritten-body /tmp/gitmail_user_body.txt (placeholders {login}, {starred_repo}, {project_name}, {project_url} substitute per recipient). Save the user-supplied body to /tmp/gitmail_user_body.txt. Step 4 keeps --dry-run so the user still sees a literal preview before live send.
Q3 โ Targeting strategy
| option | behavior |
|---|
| Recommended seeds (Claude picks) | Claude proposes 3โ5 domain-specific repos based on the Step 1 analysis. Highest accuracy. |
| Keyword search | The user types keywords directly. Maximum flexibility. |
| Auto (LLM extracts) | viralman gitmail's analyse step decides on its own. Fast but average accuracy. |
For "Recommended seeds" โ Claude shows the chosen seed repos explicitly (e.g. "for JVM monitoring I'll go with jvm-profiling-tools/async-profiler, glowroot/glowroot, pinpoint-apm/pinpoint, prometheus/jmx_exporter"). Honor any pushback from the user; otherwise proceed.
Q4 โ Recipient count (max-users)
| option | description |
|---|
| 100 (recommended for first try) | First send. Safe on free Gmail / Workspace / any SMTP. |
| 500 | Exactly the free @gmail.com daily ceiling (500 msg / 24h rolling). The largest single-batch send. |
| 1000 | Workspace recommended (within 2,000/24h). On free Gmail this needs a 2-day split. |
| 1500 | GitHub collection cap (GraphQL+REST dual bucket). Free Gmail = 3-day split, Workspace = single day. |
| Other | User-supplied (1โ1500). Above 1500 will stall on GitHub rate limit. |
Two caps, separate concerns:
- Collection cap = 1,500 / run โ GitHub API budgets (GraphQL 5,000 pt/hr + REST 5,000 req/hr).
- Send cap = SMTP policy โ free @gmail.com is 500 msg / rolling 24h, Google Workspace is 2,000 msg / 24h per user.
Even after collecting 1,500, free Gmail can only deliver 500 in a day. When the SMTP daily limit is hit, step_send aborts cleanly, emits send_aborted plus a Korean stderr line, and counts the remainder as unprocessed (retry after the rolling 24h reset).
Recipient quality filters (natural-language โ flags)
These do not get a batch question. Translate from what the user says and pass the matching flags. If the user doesn't mention any of these, send no flags (default = no filter, current behavior).
| User says (any language) | Flag(s) to add |
|---|
| "ํ๋ก์ N๋ช
์ด์", "followers >= N", "influencer ์์ฃผ๋ก", "ํ๋ก์ ๋ง์ ์ฌ๋๋ง" | --min-followers N (default N=500 if unspecified for "influencer") |
| "ํ๋ก์ ์ ์ ์ฌ๋", "early adopter", "์ด๊ธฐ ์ฌ์ฉ์" | --max-followers 200 (or user-supplied number) |
| "ํ๋ก์ ๋๋ฌด ๋ง์ ์ฌ๋ ๋นผ", "follow ๋ด ๋นผ๊ณ " | --max-following 5000 |
| "ํ๋ฐํ ์ฌ๋๋ง", "active developer", "๊ณต๊ฐ ๋ ํฌ N๊ฐ ์ด์" | --min-public-repos 3 (or user N) |
| "๋ด ๋นผ๊ณ ", "์ ๋ น ๊ณ์ ๋นผ", "๊ณต๊ฐ ๋ ํฌ 0๊ฐ๋ ๋นผ" | --min-public-repos 1 |
| "์ ๊ท ๊ฐ์
์ ๋นผ", "์ค๋๋ ๊ณ์ ๋ง", "๊ฐ์
N์ผ ์ด์" | --min-account-age-days 90 (or user N) |
| "bio ์๋ ์ฌ๋ ๋นผ", "ํ๋กํ ๋น์ด์๋ ์ฌ๋ ๋นผ" | --require-bio |
The filter applies after the GraphQL bulk profile fetch, before the email-resolution loop โ so dropped users don't consume the REST PushEvent budget. The recipients_filtered event in the stream reports before/after/dropped plus a per-dimension breakdown.
Step 3 โ Recipient collection (collect phase)
After all four answers are in, run once:
viralman gitmail recipients \
--description "$DESC" \
--project-url "$URL" \
--max-users $MAX \
[--seed-repos "$SEEDS"]
[--keywords "$KW"]
[--min-followers N]
[--min-public-repos N]
[--min-account-age-days N]
[--require-bio]
> /tmp/gitmail_recipients.json 2>&1
stdout is a JSONL event stream followed by a final recipients array. Cut everything from the first line that begins with ^\[ and save as recipients_clean.json so the next step can read it directly.
When done, summarize briefly for the user (preview up to 8):
Collected N recipients.
1. @asyncuser โ alice@example.com (async-profiler โ
)
2. @graalfan โ bob@example.com (graalvm โ
)
... (up to 8)
Generate a body preview with this list? (yes / adjust count / cancel)
Step 4 โ Fast dry-run preview (template-only, 1 LLM call)
Critical: combine --template-only --dry-run so only one LLM call composes the body, then it's reused for all N recipients in the preview. Cuts a 50-recipient dry-run from 13 minutes down to ~16 seconds.
viralman gitmail send-from-recipients \
--recipients-file /tmp/gitmail_recipients_clean.json \
--project-name "$NAME" \
--description "$DESC" \
--project-url "$URL" \
--tone "$LANG_PREFIX$TONE" \
--emphasis "$EMPHASIS" \
--subject-style "$STYLE" \
--template-only --dry-run \
> /tmp/gitmail_dryrun.json 2>&1
Manual path (Q2 = manual / ์ง์ ์
๋ ฅํ๊ธฐ) โ skip the LLM entirely:
viralman gitmail send-from-recipients \
--recipients-file /tmp/gitmail_recipients_clean.json \
--project-name "$NAME" \
--description "$DESC" \
--project-url "$URL" \
--prewritten-subject "$USER_SUBJECT" \
--prewritten-body /tmp/gitmail_user_body.txt \
--dry-run \
> /tmp/gitmail_dryrun.json 2>&1
Pull the first body from the compose_done event and show it:
[Preview] First mail
TO: <first recipient email>
SUBJECT: <subject>
---
<body>
---
Reply with one of:
โข "๋ฐ์กํด์ค" / "send" / "go" โ send for real to all 50 (template_only fast path)
โข feedback (e.g. "make it shorter", "change the tone", "more direct subject") โ regenerate
โข "cancel" โ stop
Showing the body counts as composition agreement, NOT send agreement. Body agreement โ send agreement (๋ณธ๋ฌธ ํฉ์ โ ๋ฐ์ก ํฉ์) โ until the user explicitly says "๋ฐ์กํด์ค" / "send" / "go", do not call the real-send command.
Step 5 โ Real send (only after explicit user OK)
Run only when the user has explicitly signaled intent to send:
viralman gitmail send-from-recipients \
--recipients-file /tmp/gitmail_recipients_clean.json \
--project-name "$NAME" \
--description "$DESC" \
--project-url "$URL" \
--tone "$LANG_PREFIX$TONE" \
--emphasis "$EMPHASIS" \
--subject-style "$STYLE" \
--template-only \
> /tmp/gitmail_send.json 2>&1
Keep --template-only โ the body is the one the user already approved, so regenerating costs nothing useful. Calling the LLM 50 more times is wasted budget and time.
Manual path (Q2 = manual / ์ง์ ์
๋ ฅํ๊ธฐ) โ same as Step 4 minus --dry-run, no LLM call:
viralman gitmail send-from-recipients \
--recipients-file /tmp/gitmail_recipients_clean.json \
--project-name "$NAME" \
--description "$DESC" \
--project-url "$URL" \
--prewritten-subject "$USER_SUBJECT" \
--prewritten-body /tmp/gitmail_user_body.txt \
> /tmp/gitmail_send.json 2>&1
When done, summarize:
Send complete.
Sent: N
Failed: M (grouped by reason: 4xx / 5xx / unsubscribed / invalid-address)
Unsubscribe log: .viralman_unsubscribes.jsonl
No automatic retry on failures. Pass the failure reason through to the user verbatim.
Feedback loop (when the user requests a tweak in Step 4)
If the user says things like "shorter", "different subject", "change the tone":
- Map the feedback to the right argument:
- "shorter" / "less technical" โ strengthen
--emphasis or --tone
- "different subject" โ flip
--subject-style (or accept a free-form headline)
- "in Korean" / "in English" โ change the language prefix in
--tone
- Re-run Step 4 only (template-only dry-run) with the new args. Do NOT re-run Step 3 (collection).
- Show the new body and wait for the user's OK again.
Only return to Step 3 if the user signals they want to recollect (e.g. "use different seeds", "more recipients").
Boundaries
- Never call
send-from-recipients without --dry-run until the user has explicitly said "๋ฐ์กํด์ค" / "send" / "go". Body agreement โ send agreement (๋ณธ๋ฌธ ํฉ์ โ ๋ฐ์ก ํฉ์).
- Never strip the unsubscribe footer or the
List-Unsubscribe header.
- Never pass
--max-users greater than 1500 (the safe GraphQL 5,000 pt/hr + REST 5,000 req/hr ceiling at 3x oversample). For larger campaigns, advise the user to split runs across a secondary GitHub account token.
- When the collected count exceeds the SMTP daily limit, surface it: free @gmail.com 500/24h, Workspace 2,000/24h.
step_send will auto-abort and split the remainder into unprocessed โ tell the user when the rolling 24h window resets and how to use the retry-recipients file.
- For live progress during a send, run
viralman gitmail-watch --auto in a separate terminal/tab (auto-picks the newest /tmp/gitmail_send_*.json). Single-line carriage-return display; --once prints once and exits, suitable for a Claude Code statusLine command.
- Never read or print the contents of
~/.viralman/.env.
- Never invent or guess email addresses. Use only what the GitHub Users API / PushEvent endpoints return.
- Never auto-retry a failed send.
- Don't tweak per-minute rate limits without permission. Tell the user to set
SMTP_RATE_PER_MIN themselves if they want a faster send.
- Refuse private-repo commit-email scraping or GitHub rate-limit evasion requests.