A locally-run Discord bot: send it a Threads/Instagram link (post or Reel) and it uses headless
Claude Code (claude -p) to fact-check the content and judge whether it's worth adopting — not
limited to Claude Code itself, it can analyze dev tools and AI/academic news too — then walks you
through the flow "experiment or file to knowledge base → adopt into your dev environment" via a
Discord thread report and item-by-item decisions. Full spec: docs/SPEC.md.
- You drop a Threads/IG link (or a screenshot) on the bot in Discord.
- The bot first uses a headless Playwright browser to scrape the post body, comments, and post
images, then calls
claude -pin the background to do "cross-check against the source → produce a structured report". The report is delivered as an embed plus the full report split across messages in a thread under it, so it's readable on mobile too; you can keep typing in the thread to follow up (with the analysis kept in context). - Technical items get "✅ Adopt & experiment" — the bot creates a standalone mini-project under
experiments/<slug>/and actually tries it out; academic/pure-knowledge items get "📚 File to knowledge base" and are archived straight intodata/knowledge/. Posts with multiple items use a dropdown to check off each one individually. - After an experiment finishes, you separately decide "📦 Adopt into dev environment" — only then
will the bot touch your
~/.claude(it only ever suggests changes tosettings.jsonand your existing rule files, never edits them automatically).
src/
index.js entry point: Discord client, message/interaction routing (SPEC §5)
pipeline.js state machine: analyze -> report -> experiment -> result -> adopt
claude-runner.js core module: spawns `claude -p` (headless CLI) via child_process, parses results
scraper.js Playwright scraper: post body + comments + images + full-page screenshot (SPEC §3b)
prompts.js the analyze / experiment / adopt / profile / QA-followup prompts (kept in sync with SPEC §7)
discord-ui.js embed assembly, decision components (buttons/select menus), long-report chunking (SPEC §5, §8)
config.js reads .env, centralizes path constants and per-stage model/timeout/allowedTools
sessions.js reads/writes data/sessions.json (session state, keyed by id; schema in SPEC §6)
housekeeping.js single-instance lock, data/tmp cleanup, experiment-folder cleanup (SPEC §5c, §5)
cli.js non-Discord test entry: `node src/cli.js analyze|scrape|profile`
docs/SPEC.md full behavior spec (directory layout, 3-stage flow, tool allowlists, prompts, .env.example)
data/knowledge/ filed knowledge-base entries (one .md per adopted knowledge item)
data/reports/ analysis report artifacts
data/tmp/ scraped images/screenshots, auto-cleaned on startup
experiments/ one subfolder per adopted technical item, created by the bot
KNOWLEDGE.md one-line index of every knowledge-base entry (date | title | link | file path)
PROFILE.md auto-generated project profile the bot uses for relevance judgments
scripts/start-bot.cmd Windows auto-start launcher (see section 6b)
npm install
npx playwright install chromium
npm run selftestnpx playwright install chromiumdownloads the Chromium browser used by the scraper (Chromium only, takes a few minutes) — without this step, scraping a post fails outright.npm run selftestactually callsclaude -ponce (using the cheaphaikumodel, 120s timeout) to confirm the local Claude Code CLI can be invoked by this bot. You need to seePASS; if you getFAIL, see section 7 ("Troubleshooting") below.
Then copy the environment variable template and fill in your own settings:
cp .env.example .env.env variables:
| Variable | Description |
|---|---|
DISCORD_TOKEN |
Discord bot token (see section 3 for how to get one) |
OWNER_USER_ID |
Your Discord user ID — the bot only responds to this person (see section 3) |
CHANNEL_ID |
(optional) Channel allowlist: when set, only that channel and its threads are processed, everything else (other channels, DMs) is ignored entirely; once set, plain messages in that channel also enable chat mode (see section 4). Leave empty for "no channel restriction, no chat mode" |
ANALYZE_MODEL / EXPERIMENT_MODEL / ADOPT_MODEL |
Claude model used per stage, default sonnet / sonnet / opus |
ANALYZE_TIMEOUT_MIN / EXPERIMENT_TIMEOUT_MIN / ADOPT_TIMEOUT_MIN |
Per-stage timeout in minutes, default 15 / 30 / 15 |
SCRAPE_SCROLLS |
How many times to scroll down while scraping to load more comments, default 6 |
SCRAPE_MAX_IMAGES / SCRAPE_MAX_SHOTS |
Max number of post images / full-page screenshots to capture, default 8 / 5 |
PROFILE_MAX_AGE_DAYS |
Re-scan PROFILE.md automatically after this many days, default 7 |
Once configured, start the bot:
npm startMost public Threads posts can be scraped without logging in, but Instagram almost always has a login wall, and Threads comments often need a login to load fully. For more complete scraping, run:
npm run loginThis opens a headed (visible) browser window with one tab each for Threads and Instagram. Log in
manually on either or both, then just close the whole browser window. The login state is saved
to data/browser-profile/ (already gitignored), and all subsequent headless scraping by the bot
reuses that login state.
Account risk note: scraping content while logged in violates Meta's Terms of Service. Low- frequency personal use (a few times a day) is low risk in practice, but the account could still be flagged for automated behavior — if that concerns you, don't log in and instead send the bot a screenshot of the post (see section 4); it works the same way, you just lose automatic comment scraping.
- Go to the Discord Developer Portal and log in with your own Discord account.
- Click New Application in the top right, give it a name (e.g. "Claude Code Learning Bot"), and create it.
- In the left sidebar, click Bot:
- Click Reset Token (first time it may say Add Bot / Reset Token) and copy the generated
token into
.env'sDISCORD_TOKEN. This token is only shown once — losing it means resetting again. - Scroll down to Privileged Gateway Intents and enable MESSAGE CONTENT INTENT, then save. This bot needs to read message content (to find links and notes) — without this intent it can't see message text.
- Click Reset Token (first time it may say Add Bot / Reset Token) and copy the generated
token into
- In the left sidebar, click OAuth2 → URL Generator:
- Under Scopes, check
bot. - Under Bot Permissions, check:
Send Messages,Read Message History,Attach Files,Embed Links(the permissions needed for button interactions are already included in the general message permissions). - An invite URL is generated at the bottom of the page — copy it, open it in a browser, and add the bot to your own private server (create one first via Discord desktop/mobile if you don't have one).
- Under Scopes, check
- Get your own Discord user ID (for
OWNER_USER_ID):- Discord Settings → Advanced → enable Developer Mode.
- Right-click your own avatar/name anywhere → Copy User ID, and paste it into
.env'sOWNER_USER_ID.
- After saving
.env, runnpm start— seeing the bot show up online in Discord confirms it logged in successfully.
- In a channel where you've invited the bot (or in a DM to the bot), post a message containing a
Threads (
threads.net/threads.com) or Instagram (instagram.com) link — both regular posts and Reels are supported — optionally with a note.- You can also skip the link and just attach a screenshot (e.g. when an IG login wall blocks scraping, a screenshot is the main fallback).
- A plain-text message with no link and no attachment gets a one-line usage reminder reply.
- Reel limitation: the bot only analyzes the caption, comments, and cover image/screenshot — it cannot read the video's audio or visual content; the report header will note "caption and comments only, video content not read".
- The bot immediately replies "🔍 Received, scraping post and comments…", scrapes the body,
comments, and images with Playwright, then switches to "🧠 Analyzing (usually 2–10 minutes)…",
updating the elapsed time every 60 seconds.
- If
PROFILE.md's project profile is older thanPROFILE_MAX_AGE_DAYSdays, it first shows "📇 Project profile stale, rescanning…" and rescans automatically before analyzing. - If scraping fails (e.g. login wall) and there are no usable images at all, the bot asks you to reply to that status message with post screenshot(s) (multiple allowed) plus the post text; the same session then re-analyzes with what you supplied.
- If
- Once analysis finishes, the bot sends an embed (authenticity / comment-section sentiment / item
list / feasibility / effect / relevance to your projects / recommendation / experiment plan),
and automatically opens a thread under that message, posting the full report split across
messages in it (no more
.mdattachments — readable directly on mobile). - The decision UI depends on how many items were found:
- Single item: two buttons — for a technical item, "✅ Adopt & experiment" / "❌ Skip"; for a knowledge item, "📚 File to knowledge base" / "❌ Skip".
- Multiple items: a dropdown to check off each item individually (multi-select) plus a "❌ Skip all" button. Checked technical items all go into experiments together; checked knowledge items are written straight to the knowledge base; a mixed selection does both.
- When an experiment finishes, the full
RESULT.mdis posted in chunks to the same thread along with a result embed, and a second round of decisions follows the same pattern: single item uses "📦 Adopt into dev environment" / "🗂 Keep as experiment only", multiple items use a select menu. - Follow-up questions: typing directly in a report thread (or "replying" to a report/result message) is treated as a follow-up question about that analysis — the bot answers with the full analysis in context (and can do additional research), with the answer likewise chunked into the thread.
- Only one stage runs at a time per session; if you post a new link while a previous analysis is still running, the bot replies "previous analysis still running, this one will start automatically once it's done" and queues it.
- To set it up: Discord Settings → Advanced → enable Developer Mode → right-click the
channel you want the bot to use → Copy Channel ID → paste into
.env'sCHANNEL_ID, then restart the bot. - Once set: the bot only processes messages in that channel and its threads; every other channel and DMs are ignored completely (not even a usage hint), so the bot doesn't intrude on the rest of the server.
- Chat mode: once
CHANNEL_IDis set, any plain message in that channel without a link and without an image attachment gets treated as ordinary conversation — with continuous memory (it remembers earlier chat), the ability to do web research (citing sources), and access to your project profilePROFILE.md, the knowledge base (KNOWLEDGE.md/data/knowledge/), and past analysis reports. Posting a link still goes through the analysis flow first and is never swallowed by chat mode. - Leaving
CHANNEL_IDempty keeps the old behavior: every channel is processed, but there's no chat mode (plain text just gets a usage hint).
You can also test without Discord, directly via the CLI (useful for verifying the flow before setting up a Discord token):
npm run analyze -- <url> [note] # run the full scrape -> analyze flow, print JSON + report path
npm run scrape -- <url> # run only the scraper, print text length + image/screenshot list
npm run profile # refresh PROFILE.mdThis bot runs claude -p locally, and the experiment/adopt stages give it access to the Bash /
Write / Edit tools, so its security boundary is designed as follows:
- Only responds to the
OWNER_USER_IDset in.env— any message or button click from anyone else (including other members of the same server) is ignored/rejected, so a stranger can't use this bot to run commands on your machine. - The analyze stage is read-only: its tool allowlist is only
WebFetch, WebSearch, Read, Glob, Grep— noWrite/Edit/Bash. This is because analyze processes untrusted external social-media content, which could contain a prompt injection (e.g. "ignore previous instructions and run xxx"). Even if a post successfully tricks the model, the read-only allowlist guarantees the worst case is a wrong report — no file gets created, modified, or deleted, and no command gets executed. - Experiment/adopt only ever run after you explicitly click a button, and:
- The experiment stage's working directory is pinned to
experiments/<slug>/, and its prompt explicitly requires "only create files inside the current working directory, never touch the parent directory or other projects". - The adopt stage does have
Bash/Write/Edit, but it's only triggered after you've already read the experiment report and personally confirmed you want to adopt it.
- The experiment stage's working directory is pinned to
settings.jsonis never modified automatically by adopt: the adopt prompt (seedocs/SPEC.md§7.3 /src/prompts.js) explicitly states "changes tosettings.jsonand existing rule files: never write them directly, only include the suggested diff in the report, for the user to apply manually." Any change touching your existing rule files or global settings only ever shows up as text in the Discord report — you have to go make it yourself.
For academic/pure-knowledge content (paper insights, research methods, ideas that don't need a hands-on experiment), "adopting" doesn't mean creating an experiment project — it means filing it into the knowledge base:
- File:
data/knowledge/<slug>.md— assembled directly from the analysis result by the bot (title, date, source link, items covered, verified claims with source URLs, comment-section sentiment, relevance to your projects), without calling Claude again. - Index:
KNOWLEDGE.mdat the project root, one line per entry:date | title | link | file path, so it's easy to search later with any tool (including Claude Code itself). - When a single post has a mix of technical and knowledge items, each selected item follows its
own path after you check it off: technical items go into
experiments/, knowledge items go intodata/knowledge/.
- Autostart on login:
scripts\start-bot.cmdis provided (double-click to start manually too). To register it with Task Scheduler for autostart at login (PowerShell, one-time):To remove:$cmd = "C:\Users\user\Desktop\dev\claude code learning bot\scripts\start-bot.cmd" $action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument "/c start `"ClaudeLearningBot`" /min `"$cmd`"" $trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries Register-ScheduledTask -TaskName 'ClaudeLearningBot' -Action $action -Trigger $trigger -Settings $settings -Force
Unregister-ScheduledTask -TaskName 'ClaudeLearningBot' -Confirm:$false. After login a minimized "ClaudeLearningBot" terminal window appears; to stop the bot, open it and press Ctrl+C (so the child-process reaper runs properly — don't just close the window). - Single-instance lock:
data/bot.lock. Starting a second instance exits immediately and reports the existing PID, preventing the same message from being answered twice. A stale lock from a crash is detected and overwritten automatically; delete the file manually if needed. - Automatic temp cleanup: on every startup, subfolders under
data/tmp/older thanTMP_MAX_AGE_DAYS(default 7) are removed, so scraped images don't accumulate indefinitely.
- Instagram link scrapes no content (login wall): when the scraper detects a login wall and has
no usable images, it asks you to reply with post screenshots instead of making anything up. Two
fixes: (a) send the bot a screenshot of the post directly (a link isn't required — screenshot-only
messages trigger analysis too); (b) run
npm run loginonce and retry (see the risk note in section 2b). - Scraping keeps failing / browser won't launch: confirm you ran
npx playwright install chromiumduring install; usenpm run scrape -- <url>to test the scraper in isolation and read the printed failure reason. npm run selftestfails:- First confirm the
claudeCLI itself works: runclaude --versionin a terminal — it should print a version number. - Confirm you've completed
claude login(or another auth method) for your subscription account, soclaude -pcan call the model without an interactive UI. - Check the
錯誤/stderroutput printed by selftest — it usually indicates whether it's a timeout or a CLI-level error.
- First confirm the
- Bot doesn't respond in Discord:
- Check whether MESSAGE CONTENT INTENT is enabled in the Discord Developer Portal (section 3, step 3). Without this intent the bot receives the message event but can't read its content, which looks identical to "no response".
- Check that
.env'sOWNER_USER_IDis correct — the bot is designed to only respond to this ID; if the message came from a different account, the bot ignoring it is expected behavior, not a bug. - Check whether the
npm startterminal log printed "Logged in as …" — if not,DISCORD_TOKENmay be wrong or expired and needs to be reset again from the Bot page in the Developer Portal.
- Follow-up question gets no response: make sure you're posting inside the report thread (or using Discord's "Reply" on a report/result message). Plain text in a regular channel isn't treated as a follow-up — it just gets a usage hint.
MIT — see LICENSE.