| English | 日本語 |
A Chrome extension (Manifest V3) that copies a thread on X (formerly Twitter) to the clipboard as Markdown, ready to paste into an LLM, Notion or Obsidian.
storage, clipboardWrite, and access to x.com / twitter.com onlythread-to-markdown-for-x-<version>.zip into any folderchrome://extensions in Chrome and turn on “Developer mode” (top right)manifest.json)Requires Node.js 22.2 or later (packaging uses crc32 from node:zlib).
npm install
npm run build # builds the extension into dist/ (load this folder in step 3 above)
npm run package # builds and creates thread-to-markdown-for-x-<version>.zip
On a post page (https://x.com/<user>/status/<id>), use any of the following.
| Method | How |
|---|---|
| In-page button | The “Copy as Markdown” button below the actions (reply, like, …) of the main post |
| Popup | Extension icon in the toolbar → “Copy thread as Markdown” |
| Shortcut | Alt+Shift+M (can be changed at chrome://extensions/shortcuts) |
When the copy finishes, the result (e.g. ✅ 5 posts copied as Markdown) appears at the bottom right. If copying fails, the Markdown is shown in a text area so you can copy it by hand.
Open “Options” in the popup, or “Details” → “Extension options” in chrome://extensions. Changes are saved immediately and apply from the next copy.
| Setting | Choices (default in bold) | Description |
|---|---|---|
| Posts to copy | Thread by the author only / All posts from the main post on | “All posts” includes replies by others and adds — @handle to every heading (also switchable in the popup) |
| Auto-scroll | Off / On | Scrolls down automatically to collect posts beyond the screen (also switchable in the popup) |
| Image size | Large / Original | The name= parameter of image URLs |
| Date and URL of each post | HTML comment / Normal line | HTML comments are hidden in Obsidian and Notion previews |
| Obsidian | No frontmatter / Add YAML frontmatter | Adds YAML frontmatter (title, author, source, date, posts, tags) at the top |
# X Thread by @username (2026-10-14)
- Author: Display Name (@username)
- Source: https://x.com/username/status/1234567890
- Posts: 2
- Extracted: 2026-10-14 21:30 (+09:00)
---
### Tweet 1 (Main)
<!-- 2026-10-14 09:12 +09:00 · https://x.com/username/status/1234567890 -->
Post text…
- Attached Media:
- [image] https://pbs.twimg.com/media/XXXX?format=jpg&name=large
- [video] (poster) https://pbs.twimg.com/amplify_video_thumb/...
> **Quoted post by @other** (2026-10-13)
> Quoted text…
> Source: https://x.com/other/status/987
### Tweet 2
<!-- 2026-10-14 09:15 +09:00 · https://x.com/username/status/1234567891 -->
Reply text…
Post text is output almost as is. Only line-leading characters that would break the Markdown structure (# that would become a heading, >, ---, …) are escaped.
Source: line is omitted)All dependencies on X’s DOM are collected in src/content/extract/selectors.ts. They do not rely on obfuscated class names (css-xxxxx, r-xxxxx), aria-label, or displayed text (which changes with the UI language).
Open a page where extraction fails and run the following in the DevTools console (it copies the page’s main element, with its URL, to the clipboard)
copy('<!-- url: ' + location.href + ' -->\n' + document.querySelector('main').outerHTML)
tests/fixtures/raw/<case>.html (this folder is excluded from Git because it contains real user names and posts)Create the anonymized fixture. The script fails if anonymization leaves any original value behind
node scripts/anonymize.mjs tests/fixtures/raw/<case>.html tests/fixtures/<case>.html
npm test to see which extraction broke. To add the fixture to the tests, import it like the existing ones: import html from './fixtures/<case>.html?raw'selectors.ts (comment the purpose of any new selector)npx vitest run -unpm run build and check in the browser| Command | Description |
|---|---|
npm run build |
Rebuild dist/ |
npm run package |
Build and create the release zip |
npm test |
Tests (Vitest + jsdom) |
npm run typecheck |
Type check |
npm run lint |
ESLint |
npm run format |
Prettier |
npm run check:no-network |
Check that src/ contains no fetch( / XMLHttpRequest |
The design is in PLAN.md, the development workflow in CLAUDE.md, and the work tickets in docs/ (all in Japanese).