| name | twitter-reply |
| description | Find tweets where it's natural to reply "I built this thing โ want to take a look?", and either inspect the candidates in the dashboard or send a personalized reply. The skill drives a two-step flow โ scrape first, then optionally reply โ and pushes the candidate list to the dashboard's /twitter-reply page so the user can browse cards (body / author / link / engagement) before deciding which ones to engage. |
| level | 3 |
twitter-reply Skill
Goal: when the user types /twitter-reply <project> or natural language like "์ด ํ๋ก์ ํธ ํธ์ํฐ ๋ต๊ธ๋ก ํ๋ณดํ ๋งํ ๊ฑฐ ์ฐพ์์ค", search recent X / Twitter posts for ones where a reply mentioning the user's project would land naturally, push the candidates to the dashboard for inspection, and (only after explicit confirmation) post per-candidate replies.
Trigger phrases
Auto-trigger on:
/twitter-reply, /x-reply
- "ํธ์ํฐ ๋ต๊ธ๋ก ํ๋ณด", "x์ ๋ต๊ธ๋ก ํ๋ณด", "ํธ์ํฐ ๋ต๊ธ ๋ง์ผํ
", "๋ต๊ธ๋ก ์๋ฆด ๋งํ ํธ์ ์ฐพ์์ค"
- "find tweets to reply to", "reply marketing on X", "scrape tweets I can reply to with my project"
If the user says only "ํธ์ํฐ์ ์ฌ๋ ค์ค" / "x์ ์ฌ๋ ค์ค" โ that's the existing viral skill's posting flow, NOT this skill. This skill is specifically for reply-marketing (engaging on someone else's tweet).
Pre-flight
Binary check (run this first). The skill drives the viralman console-command and its subcommands (viralman twitter-reply, 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/** โ 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 twitter-reply find or viralman twitter-reply reply) is denied by the Claude Code permission layer or auto-mode classifier โ typical classifier reasons include "automated reply at scale" or "unsolicited engagement", even though the run is the user-initiated twitter-reply 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, ์ ์ธ์
๋ถํฐ ์ ์ฉ) ๋ค์๋ถํฐ๋
twitter-reply ๋ช
๋ น๋ค์ด ๋งค๋ฒ ๋ฌป์ง ์๊ณ ๋ฐ๋ก ์งํ๋ฉ๋๋ค:
"Bash(viralman:*)"
์์ธํ ์๋ด๋ /viralman-setup์ Step 5 ์ฐธ์กฐ. auto-mode classifier ๋ ๋ณ๊ฐ
๋ ์ด์ด๋ผ ์ค์ reply ๋ฐ์ก ๊ฐ์ ๋ช
๋ น์ ์ ๋ฃฐ์ ์ถ๊ฐํด๋ ํ ๋ฒ ๋ค์ด์ผ๋ก๊ทธ๊ฐ
๋ฐ ์ ์์ต๋๋ค โ ๊ทธ๋ ํ ๋ฒ๋ง 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:
TWITTER_OAUTH2_BEARER โ required for both search (tweet.read) and reply (tweet.write). The dashboard PKCE login covers both scopes.
- one of
ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY, or detect Claude Code CLI via which claude (used to compose per-candidate reply bodies).
If the bearer is missing, route the user to /viralman-setup twitter (PKCE login) and stop. Never read or print .env values.
Step 1 โ Project intent capture
Follow skills/copy-prep/SKILL.md ยงProject intent capture. The struct it produces (url / name / description / keyword) feeds the search query in Step 3.
Step 2 โ Batch question (AskUserQuestion, exactly once)
Surface all four upfront decisions in one call before any search runs:
Q1 โ Mode
| option | description |
|---|
| Scrape only (recommended) | Find candidates, push them to the dashboard /twitter-reply page, stop. Reply step is a separate decision. |
| Scrape + reply | Same scrape, but also compose per-candidate replies and walk the user through send confirmations one tweet at a time. |
Q2 โ Search query
| option | description |
|---|
| Auto from project | Use keyword + 2โ3 high-signal nouns from the project description as the v2 search query. Default. |
| Free-form | The user types their own query string (v2 recent-search syntax allowed: "jvm monitoring" -is:retweet lang:en). |
Q3 โ Engagement floor
| option | description |
|---|
| 0 (raw recency) | No floor. Cheaper signal, more noise. |
| 5 (default) | Likes+retweets+replies+quotes โฅ 5. Filters out pure shouts-into-void. |
| 25 (high signal) | Tweets that already have traction. Reply gets more eyes but the topic may already be saturated. |
Q4 โ Candidate count
| option | description |
|---|
| 10 | Quick first pass. |
| 20 (default) | Standard inspection batch. |
| 50 | Deeper sweep. v2 recent-search caps any single call at 100 results. |
Follow skills/copy-prep/SKILL.md ยงLanguage picker if the reply needs a non-default language. The picked language maps to the v2 lang: operator on the search and to the reply composition prompt.
Step 3 โ Scrape (the find subcommand)
Run once after the batch question:
viralman twitter-reply find \
--query "$QUERY" \
--keywords "$KEYWORDS" \
--lang "$LANG" \
--max-candidates "$MAX" \
--min-engagement "$MIN_ENGAGEMENT" \
--out /tmp/twitter_candidates.json \
> /tmp/twitter_find.json 2>&1
The script emits a JSONL event stream and writes the final candidates array to --out. Print a 2โ4 line summary back to the user (count + top 3 candidates with handle + first 80 chars).
Step 4 โ Hand off to the dashboard
After the scrape completes, tell the user:
N candidates ready. Open the dashboard at http://localhost:8765/twitter-reply to inspect them as cards (body, author, link, engagement). The page reads /tmp/twitter_candidates.json directly โ refresh to see updates if you re-run scrape.
If the dashboard isn't running, suggest one of:
./bin/viralman dashboard (preferred), or
- the
/dashboard skill, which boots the Flask server on :8765.
If the user picked Scrape only in Q1, stop here. The dashboard view is the deliverable.
Step 5 โ Reply (only when user picked "Scrape + reply" AND explicitly confirms each)
For each candidate the user wants to engage:
- Compose a reply body via LLM (or via
viral-writer agent for tone control). Pass: candidate's text, project name + URL, the language picked in Step 2. Output โค 280 chars, no marketing slop, references the candidate's content concretely (not "great post! check out my thing"), ends with the project URL.
- Pipe through
ai-tell-sniffer once. If flagged 3 times, surface the flags and ask the user instead of auto-posting.
- Show the draft + candidate side by side, ask "send / edit / regen / skip". Body agreement โ send agreement โ only send after the user explicitly says "send" / "๋ฐ์ก".
- On send:
echo "$REPLY_BODY" | viralman twitter-reply reply \
--tweet-id "$TWEET_ID" --body -
- Record the outcome to
~/.viralman/posts.jsonl with {ts, platform: "x-reply", in_reply_to, url}.
Boundaries
- Never post a reply without an explicit per-candidate "send" / "๋ฐ์ก" from the user. Body agreement โ send agreement.
- Never invent a tweet ID, a username, or a candidate's text โ they all come from the v2 search response.
- Never scrape with
--include-retweets unless the user asks for it explicitly. Replying to a retweet creates a low-quality reply chain and most platforms suppress it.
- Never auto-retry a failed reply. v2 returns 4xx for things like "you've already replied to this tweet" or "rate limited" โ those need a human decision, not a retry loop.
- Search uses the v2 recent-search endpoint (last 7 days). For longer windows the user needs the Twitter "all archive" tier โ out of scope for this skill.
- Reply rate limits: v2 user-context allows 50 replies / 15-min window for the standard Basic tier. The skill never sends more than 5 in a single batch without re-confirming.
- Refuse private-tweet scraping or any attempt to evade Twitter's rate-limit headers.