Export all of your ChatGPT conversations to local Markdown files, organized by project. Includes images, project files, citations, memories, and custom instructions.
ChatGPT has no cross-conversation search within projects. This scraper creates a local, searchable archive of everything — so you can grep across your entire history.
Your conversations with AI are some of the most valuable digital artifacts you produce — they contain your thinking process, your decisions, your creative work, and your problem-solving in raw form. Yet they live entirely on someone else's servers, subject to someone else's terms of service.
OpenAI can change their data retention policies, shut down access, or alter the export format at any time. The built-in "Export Data" feature gives you a bulk JSON dump that's barely human-readable. There's no way to search across conversations within a project — a glaring omission for a tool people use as an extension of their working memory.
This matters because:
- You should own your own thinking. If you've spent hundreds of hours working through problems with ChatGPT, that history has real intellectual value. It shouldn't be locked inside a platform you don't control.
- Services disappear. Companies pivot, get acquired, or go under. Your data shouldn't evaporate with them.
- Searchability is basic infrastructure. Being unable to search your own conversation history is not a missing feature — it's a missing right.
- Local copies are the only reliable backup. Cloud-only data is a single point of failure. A local archive in plain Markdown files will outlast any proprietary format.
This tool is a small act of digital self-determination: take what's yours, store it in a format you control, and make it actually useful.
| Data | Format | Location |
|---|---|---|
| Conversations | Markdown (.md) |
projects/{Project Name}/ |
| Images from conversations | Original files (.jpg, .png, etc.) |
projects/{Project Name}/_assets - {title}/ |
| Project files (knowledge files) | Original format (.docx, .pdf, .txt, etc.) |
projects/{Project Name}/_project_files/ |
| ChatGPT memories | Markdown + JSON | projects/_account/memories.md |
| Custom instructions | Markdown + JSON | projects/_account/custom_instructions.md |
| Archived conversations | Markdown | projects/[Archived]/ |
| Non-project conversations | Markdown | projects/[No Project]/ |
projects/
├── _account/
│ ├── memories.md
│ ├── memories.json
│ ├── custom_instructions.md
│ └── custom_instructions.json
├── [Archived]/
│ └── {title}.md
├── [No Project]/
│ └── {title}.md
├── My Project/
│ ├── _project_files/
│ │ ├── uploaded_doc.docx
│ │ └── reference.pdf
│ ├── _assets - Some Conversation/
│ │ ├── photo.jpg
│ │ └── screenshot.png
│ ├── Some Conversation.md
│ └── Another Conversation.md
Each conversation is saved as a Markdown file with metadata and full message history:
# Conversation Title
**Created:** 2025-03-04 10:17:19 UTC
**Updated:** 2025-03-04 10:17:53 UTC
**Model:** gpt-4o
**Conversation ID:** 69a80685-0a40-838e-a26f-7bb5cad90173
---
## You
Your message here...
## ChatGPT
Response with **markdown**, `code`, and:
```python
code_blocks_preserved()
```
Some text with a citation[^1]
---
### References
[^1]: https://example.com/source- Node.js (v18+)
- curl (comes with most systems; on Windows, Git Bash includes it)
- Chrome with a cookie export extension (e.g., Cookie-Editor)
- Log in to chatgpt.com in Chrome
- Open the Cookie-Editor extension
- Click Export → choose JSON format
- Save the output to
cookies.txtin this directory
The file should look like this:
[
{
"domain": ".chatgpt.com",
"name": "__Secure-next-auth.session-token",
"value": "eyJhbGci...",
...
},
...
]Use your exported cookies to request a session token:
curl -s 'https://chatgpt.com/api/auth/session' \
-H "Cookie: $(cat cookies.txt | node -e "const c=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(c.map(x=>x.name+'='+x.value).join('; '))")" \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36" \
-H "Accept: application/json" \
-H "Referer: https://chatgpt.com/" \
| node -e "process.stdin.on('data',d=>{const j=JSON.parse(d);console.log(j.accessToken)})"Save the output (a long JWT string) to a file called .token:
# Paste the token into .token (no newline at end)
echo -n "eyJhbGci..." > .tokenFind any conversation ID from your ChatGPT URL (e.g., https://chatgpt.com/c/69a80685-0a40-838e-a26f-7bb5cad90173) and test:
node scraper.js --conv 69a80685-0a40-838e-a26f-7bb5cad90173This saves one conversation to projects/[No Project]/. Check that it looks right before doing a full scrape.
node scraper.js 2>&1 | tee scrape.logThis will:
- Enumerate all your projects
- Download all project files (knowledge files uploaded to projects)
- Scrape every conversation in every project
- Scrape all non-project conversations
- Scrape archived conversations
- Export your ChatGPT memories and custom instructions
Expect this to take a while depending on how many conversations you have. Progress is printed as it goes. The scraper uses a 1.2-second delay between API calls and backs off for 30 seconds if rate-limited.
After the initial full scrape, use the incremental scraper for subsequent runs:
node incremental.jsOn the first run, it builds a manifest.json from your existing files (without re-downloading). On subsequent runs, it only fetches conversations that are new or have been modified since the last run.
The incremental scraper also tracks:
- Moved conversations — if you move a conversation to a different project, the old file is prefixed with
[moved] -and a fresh copy is saved in the new project folder - Deleted conversations — if a conversation is deleted from your account, the local file is prefixed with
[deleted] -(nothing is ever removed from your archive) - Project files — re-downloads if file size changes, skips if unchanged
- Memories & custom instructions — always refreshed
ChatGPT's web app uses internal API endpoints under https://chatgpt.com/backend-api/. Auth requires:
- Session cookies exported from your browser
- A Bearer token obtained from the
/api/auth/sessionendpoint
The Bearer token expires periodically. If the scraper starts failing, re-export cookies and generate a new token.
| Endpoint | Purpose |
|---|---|
/gizmos/snorlax/sidebar?owned_only=true |
List all projects (paginated) |
/gizmos/{project_id}/conversations?cursor=0 |
List conversations in a project |
/conversations?offset=N&limit=N&order=updated |
List non-project conversations |
/conversations?is_archived=true |
List archived conversations |
/conversation/{id} |
Full conversation with message tree |
/files/download/{file_id}?inline=false |
Get signed URL for conversation images |
/files/{file_id}/download?gizmo_id={project_id} |
Get signed URL for project files |
/memories?limit=500 |
All stored memories |
/user_system_messages |
Custom instructions |
- Projects are internally called "gizmos" with IDs prefixed
g-p- - Conversations contain a message tree (not a flat list) accessed via a
mappingobject - Messages are walked backwards from
current_nodeto root viaparentpointers - Message content is in
message.content.parts[]as plain strings (already Markdown-formatted) - Images use
content_type: "image_asset_pointer"withasset_pointer: "sediment://file_XXXX" - Image download is two-step: get a signed URL from the API, then download from that URL
- Project file download also two-step, but requires passing
gizmo_idas a query parameter - Citations contain invisible Unicode characters (PUA range U+E000–U+F8FF) that must be stripped before parsing
- Citation source URLs are in
search_result_groupsmetadata, mapped byref_index
Cloudflare's bot protection blocks requests from Node.js HTTP clients based on TLS fingerprinting. Using curl via child_process.execSync with browser-like headers bypasses this. This is the same approach used by tools like yt-dlp.
- 1.2-second delay between API calls
- If the API returns 429 (rate limited), the scraper waits 30 seconds and retries once
- Conversations are fetched sequentially, not in parallel
- In practice, a full scrape of ~1,500 conversations completes without hitting rate limits
The Bearer token expires after some time (usually a few hours). If you see authentication errors mid-scrape:
- Re-export cookies from Cookie-Editor (you may need to refresh chatgpt.com first)
- Save the new cookies to
cookies.txt - Generate a new token using the curl command above
- Save to
.token - Re-run the scraper — it will skip conversations that already have files on disk (for
incremental.js) or overwrite them (forscraper.js)
Once scraped, you can search across all conversations with standard tools:
# Search for a term across all conversations
grep -r "some topic" projects/
# Search within a specific project
grep -r "error message" projects/System\ Build/
# Find conversations mentioning a specific tool
grep -rl "Docker" projects/ | head -20
# Count conversations per project
for d in projects/*/; do echo "$(ls "$d"/*.md 2>/dev/null | wc -l) $d"; done | sort -rnMIT