Backing up your Claude conversations with a Chrome extension
So you can search through them and actually find them again later
I often want to revisit a conversation that I've had with Claude -- maybe I remember that we generated some useful code or artifacts that I want to reuse now, or maybe I want to pick up a chat where we left off, or maybe I just want to re-read the transcript.
If you aren't methodically curating your conversations as you go by renaming them all and organizing them into projects1, it can be quite hard to find them later! The https://claude.ai/recents
screen just lists all conversations by name in reverse chronological order, and in my experience the "search your chats" widget on that screen is quite basic & rarely finds what I'm looking for.2
This morning, while searching with rising frustration for a conversation that I know used the word “quadratic”, I realized this problem seems suspiciously similar to the one I solved for myself about Substack drafts back in January.
So I worked with Claude to build a new Chrome extension that would automatically back up all my Claude conversations. This way I'd have local copies I could search through, revisit, and analyze with my own tools.
How it works
The extension does a few simple things:
Every eight hours, it automatically fetches a full list of your Claude conversations
It then downloads each conversation's full content as a JSON file
Everything is saved to your downloads folder in a structure that you configure
Since it's running in your browser with your session, no authentication hassles
The underlying API requests are quite straightforward — Anthropic doesn't have an official public API for the web interface, but from watching requests in my browser I saw that the structure is clean and consistent. We just need to:
Fetch the list of your organizations (which may be more than one, if you have an API account for example) and find the one with "chat" listed in its capabilities
Get a list of all conversations for that organization, using pagination
For each conversation, request its full content with message history
The API responses come back with a ton of useful metadata — when the conversation was created, what model was used, all the messages, artifacts, and context.
Let's look at the code
Here's how the extension works behind the scenes:
// Set up a recurring alarm every eight hours
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create('fetchConversationsAlarm', { periodInMinutes: 480 });
});
// Listen for the alarm and fetch conversations
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'fetchConversationsAlarm') {
fetchAndSaveConversations();
}
});
// Also allow the user to manually trigger a backup from the options page
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'backupNow') {
fetchAndSaveConversations();
}
return true;
});
The meat of the extension is in the fetchAndSaveConversations
function, which does the actual API calls:
async function fetchAndSaveConversations() {
chrome.storage.sync.get(['downloadDir'], async (settings) => {
const downloadDir = settings.downloadDir || 'claude-conversations';
try {
const orgsResponse = await fetch('https://claude.ai/api/organizations');
if (!orgsResponse.ok) {
throw new Error(`Failed to fetch organizations: ${orgsResponse.status}`);
}
const organizations = await orgsResponse.json();
const chatOrg = organizations.find(org =>
org.capabilities && org.capabilities.includes('chat')
);
if (!chatOrg) {
throw new Error('No organization with chat capabilities found');
}
// Step 2: Fetch all conversations by paging through until we don't see any more
let allConversations = [];
let offset = 0;
const limit = 50;
while (true) {
const conversationsUrl = `https://claude.ai/api/organizations/${chatOrg.uuid}/chat_conversations?limit=${limit}&offset=${offset}`;
const conversationsResponse = await fetch(conversationsUrl);
if (!conversationsResponse.ok) {
throw new Error(`Failed to fetch conversations: ${conversationsResponse.status}`);
}
const conversations = await conversationsResponse.json();
if (!conversations.length) {
break;
}
allConversations = [...allConversations, ...conversations];
offset += limit;
}
// Step 3: Fetch full details for each conversation
for (const conversation of allConversations) {
const detailUrl = `https://claude.ai/api/organizations/${chatOrg.uuid}/chat_conversations/${conversation.uuid}?tree=True&rendering_mode=messages&render_all_tools=true`;
const detailResponse = await fetch(detailUrl);
if (!detailResponse.ok) {
console.error(`Failed to fetch conversation ${conversation.uuid}: ${detailResponse.status}`);
continue;
}
const conversationDetail = await detailResponse.json();
await saveConversationToFile(
conversationDetail,
downloadDir,
`${(conversation.name || 'Untitled Conversation').replace(/[^a-z0-9]/gi, '_').toLowerCase()}_${conversation.uuid}`
);
}
} catch (error) {
console.error('Error fetching Claude.ai conversations', error);
}
});
}
The key thing to note is that -- just like in our earlier Substack version -- we're doing this all from the browser with an already-authenticated session, so we're able to fetch everything we need in exactly the way that the Claude.ai web interface does.
So many downloads
The one slightly annoying thing about this extension is that Chrome visibly downloads a file for each conversation you have. I've already run into this problem with my Substack extension and just kind of got into the habit of ignoring it; if you have hundreds of conversations, you're going to see a lot of little floating download icons if you're paying attention to your Chrome window at the right time. It's a bit distracting, but I decided it was worth the tradeoff for having all my conversations backed up and searchable.
I've played around a bit with possible approaches to make this go away, but haven't come up with anything that seems to work reliably. So for now, I'm sticking with the simplest approach.
Only download what changes
That said, after witnessing the download notification flood happen a second time during the periodic run, I decided I should put in another few minutes to optimize it. Claude's API includes updated_at
timestamps for each conversation, so we can compare those against our previous backup and only download conversations that have changed.
To make this work we’ll need to track when we downloaded each conversation. A Chrome extension can’t read the contents or mtimes of any files — even ones in the downloads folder that it previously created — so we’ll build up a metadata index and put it in Chrome’s storage:
const lastBackupState = settings.lastBackupState || {};
const newBackupState = {};
for (const conversation of allConversations) {
const lastDownloaded = lastBackupState[conversation.uuid];
const updatedAt = new Date(conversation.updated_at);
if (lastDownloaded && new Date(lastDownloaded) >= updatedAt) {
newBackupState[conversation.uuid] = lastDownloaded;
continue;
}
// fetch the conversation...
newBackupState[conversation.uuid] = new Date().toISOString();
}
chrome.storage.local.set({ lastBackupState: newBackupState });
Note that we want to put this in chrome.storage.local
, not chrome.storage.sync
. The latter would save the metadata to Google cloud storage and sync across devices, which doesn’t actually make sense when we’re trying to track whether our local filesystem has an up-to-date download. For simplicity I also switched the downloadDir
to local-only storage at the same time, though this one probably could stay synced.
Now each successive periodic run only downloads the smaller number of conversations that you’ve actually touched in the past eight hours, which is a lot less annoying.
Development workflow
My workflow for building this was delightfully straightforward; I just:
Started a new conversation in Claude.ai
Uploaded a PDF of my blog post about building a Chrome extension to back up Substack drafts
Connected the Github repository for that extension and put all the files in the chat context
Told Claude I now wanted to build an analogous extension for Claude conversations
Provided example URLs and payloads from the three relevant unofficial API endpoints based on watching the XHR requests in Chrome DevTools as I click around —
https://claude.ai/api/organizations
,https://claude.ai/api/organizations/${orgId}/chat_conversations?limit=${limit}&offset=${offset}
, andhttps://claude.ai/api/organizations/${orgId}/chat_conversations/${chatId}?tree=True&rendering_mode=messages&render_all_tools=true
3
Claude 3.7 in “Extended” mode thought for eight seconds4, then gave me back a complete, fully-working implementation as well as installation instructions5 on the first try. (We then chatted through some of the details to implement the incremental downloads feature.)
Try it yourself
If you're interested in trying this extension, you can find the code on GitHub. To install it:
Clone the repository
Open Chrome and go to
chrome://extensions/
Enable "Developer mode" (top right)
Click "Load unpacked" and select the extension directory
Click on the now-installed extension to view details
Click the "extension options" to set your download directory and, optionally, kick off a first run right away
Feel free to fork, modify, and improve it!
(Note: while the extension is intended only to back up your own conversations and by design can’t really be used for any other purpose, this type of automated access may technically fall into a gray area of Anthropic’s Terms of Service, which say you cannot “harvest data” or “access the Services through automated or non-human means” without an API key. We believe this personal backup tool is consistent with the spirit of user data ownership — as the Terms state: “you retain any right, title, and interest that you have in the Inputs you submit... we assign to you all of our right, title, and interest—if any—in Outputs” — but, of course, use this tool at your own discretion.)
Future improvements
There are a few things I'd like to add in the future:
Fetching any referenced file attachments in a conversation and downloading them as well
Building a local browsing interface that gives you a nice index screen with (usable) search and tagging/grouping features, and then links out directly to the relevant conversation on
https://claude.ai
when you want to read a conversation in full or continue itI dunno, maybe ways to continue the conversation on equivalent chat interfaces using the Claude API if I reach my usage cap and don’t want to wait? ¯\_(ツ)_/¯ (I've been meaning to try out LibreChat and Open WebUI, this might nudge me into finally doing so.)
But even in its current form, it's already quite useful since I can just use tools like grep
or llm
to find past chats more reliably and “chat with my conversations”6.
Let me know if you have any questions or suggestions for improvement. Meanwhile, it's already helped me find that chat from January 4 about quadratic growth patterns. Yay!
And even when you are, if you have enough of them.
And works in fairly mysterious ways; if I search for a word that I know is rare across my chats, it will sometimes turn up a conversation that didn't use that word, and fail to find the one that did.
Those query string parameters are important — without them, the API response will not be a complete representation of the conversation. In particular the ?rendering_mode=messages
seems to control whether the JSON will include a content
array of all text, thinking, or tool messages per assistant-response, similar to the official Claude API; without this, it just returns a simplified flat text
string per reply, which will be missing some of Claude’s actual replies. In my very cursory testing I haven’t seen what difference the other two parameters make, but from their names I assume they are important when tools and/or branching conversations are involved.
Here’s Claude’s thought process:
Alright, so I'm being asked to help create a Chrome extension that backs up Claude.ai conversations automatically, similar to the Substack backup extension the user previously built. Let's analyze what I'll need to do:
Understand the Claude.ai API endpoints provided:
First, we fetch organizations and identify the one with 'chat' capabilities
Then, we fetch paginated conversation listings
For each conversation, we fetch the full conversation details
Create a Chrome extension that:
Runs on a schedule (every eight hours)
Makes these API requests using the user's authenticated session
Saves the conversations as JSON files
Reuse the structure and logic from the Substack extension, adapting it for Claude.ai
Let's start by modifying the files one by one:
manifest.json - This needs minimal changes, just updating the name and description
background.js - This needs the main logic changes for fetching from Claude.ai
options.html/js - Update to include any Claude.ai specific settings
Now let's implement each file:
manifest.json
Update the name
Keep the same permissions
background.js
Modify the
fetchAndSaveDrafts
function to instead fetch Claude conversationsAdd logic to:
Fetch organizations
Identify organization with chat capabilities
Paginate through conversations
Fetch each individual conversation
Save everything as JSON
options.html/js
Update to remove Substack-specific options
Add Claude-specific options if needed (likely just the download directory)
Let me implement these changes:
I really like that “Alright, so…” opening.
Which I put verbatim in the project’s README on Github.
Like cat backing_up_claude_chat_history.json | llm -s 'summarize this conversation'
thanks, another feedback (other may find useful) is when running the extension on Chromium (the Ungoogled one) there was constant popup confirmation for each file, which is a pain when you have multiple updates, so you have to click twice for each file. I had to disable:
chrome://settings/downloads -> Ask where to save each file before downloading= OFF
thanks, would be useful to have offline html browser to handle those downloaded json files