Export your chatgpt.com and claude.ai conversations to Obsidian-friendly Markdown — one click, in the page, fully local.
An inkstone grinds raw pigment into ink for writing. Inkstone helps you grind raw model output into ink for your notes.
English · 简体中文
① Install Tampermonkey → ② Install Inkstone → ③ Open chatgpt.com or claude.ai, hit ⤓
Your ChatGPT history holds real work, but getting it into a vault is painful. The official export is a raw JSON dump: tool and system messages are stripped (Canvas and code-interpreter output simply aren't there), some attachments have already expired server-side, math arrives in \( \) delimiters Obsidian won't render, and web-search citations turn into private-use Unicode garbage. Copy-pasting by hand doesn't scale past ten conversations, let alone a thousand.
Inkstone runs inside the page and fetches conversations through the same backend API the app itself uses, then converts everything locally in your browser — nothing ever leaves the page. The result is Markdown that reads natively in Obsidian: real headings per turn, $ / $$ math, resolved citations, downloaded images, clean frontmatter.
| ChatGPT | Claude | |
|---|---|---|
| Export current conversation | ✅ | ✅ |
| Batch / export-all | ✅ | ✅ conservative safeguards |
| Incremental sync | ✅ | ✅ |
| Rich documents | Canvas patch replay | Artifact fold-up to final version |
| Thoughts / tool traces | ✅ opt-in | ✅ opt-in |
| Attachments | images and files downloaded | images, documents, and generated files downloaded; text extractions inlined |
Claude batch export uses a separate, deliberately conservative policy. Its rate-limit
profile is still not backed by a large real-world sample, so ChatGPT's settings are not
reused: one worker, spacing from 1500 ms, a 30-second rest every 40 requests, and no second
pass over failed items. One global Retry-After signal, three 429s, 1000 HTTP attempts, or
an abnormal failure ratio stops the batch. Successful conversations are still written;
unfinished ones do not advance the watermark and are picked up by the next incremental run.
| Panel UI | Advanced settings |
|---|---|
![]() |
![]() |
| Batch export all | Multi-select export |
|---|---|
![]() |
![]() |
| Export directly to Obsidian | Keep thoughts / tool traces |
|---|---|
![]() |
![]() |
| Keep math & links | Keep attachments & images |
|---|---|
![]() |
![]() |
# User/# ChatGPTtop-level headings separate turns; headings inside messages are demoted one level (H1–H6)\( \)/\[ \]→$/$$math-delimiter conversion (code-block aware); currency$escaped- Citations restored: web-search citations become inline
[source](url)links plus a# Sourcessection at the end; file citations become filename notes; anything unresolvable is stripped — no garbled markers, ever - Chain-of-thought and tool traces (code-interpreter code, search queries, run output) are wrapped in collapsed callouts and not written by default — opt in via advanced settings ("write thoughts" / "write tool traces"), or
--thoughts/--tool-tracesin the CLI - Unknown content types are preserved verbatim inside collapsed callouts — nothing is ever silently dropped
- Frontmatter:
title / chat_id / url / project / created / updated / model / tags
- Every image in a conversation is downloaded into an attachments subfolder next to the notes (default
conversations/attachments/, inlined with Obsidian![[wikilink]]) - User-uploaded files ≤ 2 MB are downloaded and linked; larger ones get a placeholder note
- Folder layout is customizable in settings: the notes subfolder (default
conversations, nestable asa/b, empty = vault root) and the attachments subfolder (defaultattachments, relative to the notes folder, empty = same level as notes). Attachment links are strict relative paths, so GitHub and VS Code previews work too - A "download attachments" toggle: turn it off for text-only export with drastically fewer requests
- Failed downloads leave a note in the conversation and the failure report, and remain eligible for incremental retry. An attachment shared across conversations is attempted once per fetch pass.
- After upgrading, Claude attachments without a UUID use new safe filenames when their conversations are re-exported. Notes that are not re-exported keep their existing links; existing attachment files are not automatically renamed or deleted.
Conversations inside ChatGPT Projects are covered by every scope. The main conversation-list endpoint only returns the flat sidebar "Chats" list, so Inkstone treats Chats and every project as separate paged streams and k-way merges them by update_time — one list in true recency order, deduplicated by id. Project conversations get a project: frontmatter field and a chatgpt.com/g/<gizmo-id>/c/<id> URL, and are labelled with the project name in the multi-select list.
The multi-select list also has a source dropdown — All / Chats / one specific project — so a single project's conversations are one click away instead of a scroll through everything.
A watermark records each conversation's update_time, so re-exports only fetch what changed (can be disabled; the panel has a "reset incremental state" button). The heavy full grab only ever happens once.
Besides Markdown, Inkstone exports a raw JSON zip — data insurance, and the source of converter fixtures.
- Install Tampermonkey
- Click the latest inkstone.user.js — the script header carries an update URL, so future releases auto-update
Or GreasyFork: inkstone
Or build from source:
bun install
bun run build # → dist/inkstone.user.js, drag it into TampermonkeyOpen chatgpt.com or claude.ai (logged in) → click the ⤓ button in the top bar → pick Markdown zip or raw JSON zip → unzip into your Obsidian vault.
- The button position is switchable (panel → advanced settings): next to Share in the top bar, or a glass button beside the input box
- The UI follows the host page's light/dark appearance; ChatGPT follows its selected accent, while Claude uses its brand orange
- Exports are cancelable; a single failed conversation never aborts the run — failures are summarized in
_failures.json - Claude batches run with one worker; after a protective abort, wait as instructed instead of restarting immediately
The zero-risk alternative: convert ChatGPT's official export zip entirely offline — it never touches the backend API.
bun run offline <export.zip | extracted-dir> [-o outdir]
[--thoughts] [--tool-traces] [--no-assets]
[--link-style wikilink|markdown] [--heading-mode demote|strip]
[--notes-dir <name>] [--attachments-dir <name>]The output layout matches the userscript's. Caveat: the official zip only contains visible user/assistant messages — tool/system payloads (Canvas, code interpreter) are stripped by OpenAI, and some referenced attachments are already expired server-side (placeholders are left in their place).
bun install
bun run dev # vite-plugin-monkey prints a one-time install URL; edits hot-reload
bun test # converter unit tests (pure TS, no browser needed)
bun run typecheck
bun run buildNote for claude.ai: its CSP may block the dev-server script, so verify Claude-side changes
against a real bun run build artifact loaded into Tampermonkey rather than bun run dev.
Architecture: src/core/ is site-agnostic (IR, renderer, throttled fetcher) and
src/sites/<site>/ holds everything that knows one provider's endpoints, fields and DOM.
Adding a site means adding an adapter, not touching the orchestration. See PLAN.md § P5.
An MV3 browser extension (no Tampermonkey, store release) and Gemini support. Already done: the multi-site adapter architecture, Claude batch and incremental export, direct-write to an Obsidian vault, settings panel, Canvas patch replay, Artifact fold-up, and the offline CLI. Details in PLAN.md (Chinese).







