Skip to main content

Import your chat history

Claude Code, ChatGPT, Claude.ai, Mem0 and JSONL into memory, in one command.

6 min readUpdated November 2026New in 1.0

One command per source

Each message is stored verbatim under its original time, one session per transcript or conversation. auto tells the formats apart from what the file holds, not its name.

terminalbash
llmslim import claude-code                          # everything in ~/.claude/projects
llmslim import chatgpt ~/Downloads/chatgpt-export.zip
llmslim import claude-ai ~/Downloads/data-2026-10-31.zip
llmslim import mem0 mem0-export.json
llmslim import jsonl history.jsonl
llmslim import auto ~/Downloads/export.zip

The command comes with the base install. The turns go to the memory llmslim mcp reads, ~/.llmslim/memory.db (or --db, LLMSLIM_MEMORY_DB), under the user default (or --user-id, LLMSLIM_USER_ID). So after llmslim import claude-code, the recall tool in Claude Code finds what you said in earlier sessions.

Look before you import

--dry-run reads and counts everything and stores nothing; a SQLite file is opened read-only, and one that doesn't exist isn't created. One progress line per session goes to stderr, then a summary to stdout.

terminalbash
llmslim import claude-code --dry-run
output, for two transcripts (paths shortened)text
  22222222-2222-4222-8222-222222222222: 2 turns, 2 new, 0 already there
  11111111-1111-4111-8111-111111111111: 5 turns, 5 new, 0 already there
Would import Claude Code history from ~/.claude/projects
  into:        ~/.llmslim/memory.db (user default)
  files:       2
  sessions:    2
  turns:       7 to import, 0 already there, 13 skipped
  skipped:     attachment 1, meta 8, subagent 1, thinking 1, tool_output 2
  dry run: nothing was stored

Run it again without --dry-run to store the turns. Running it a second time stores only what is new: the summary says 0 imported, 7 already there. --json prints the report as JSON instead.

What each format gives

Only what the user and the assistant said is imported. Reasoning ("thinking") never is, nor are system prompts, custom instructions or other hidden context. Tool calls and results come in only with --include-tool-output, as tool turns: tool output is where secrets usually are.

FormatReadsA session isLeft out
claude-code~/.claude/projects/<project>/<session>.jsonl: one transcript, a project folder, the projects folder or the .claude folder (the default; $CLAUDE_CONFIG_DIR is honoured)One transcriptThinking, tool calls and results, slash commands and their output, injected <system-reminder> text, compaction summaries, subagent transcripts, images, and Claude Code's own bookkeeping records
chatgptThe export zip as downloaded, its folder, or conversations.jsonOne conversationBranches you moved away from (only the thread you saw last is read), reasoning, system messages, custom instructions, tool messages, images
claude-aiThe export zip, its folder, or conversations.jsonOne conversationThinking, tool use and results, attachments and files, older branches
mem0A JSON list of memories, {"results": [...]} as get_all returns it, {"memories": [...]}, or JSONLOne (user_id, agent_id, run_id) scopeNothing: each memory becomes one user turn
jsonlOne {"role", "content", "timestamp"?, "session_id"?, "user_id"?, "agent_id"?, "id"?} per line, in a file or a folder of .jsonl filesOne session_idsystem lines; tool lines without --include-tool-output

From Python

import_history takes the same options as the command and returns an ImportReport.

import_history.pypython
from llmslim.memory import Memory, import_history

memory = Memory("memory.db")
report = import_history("history.jsonl", memory, format="jsonl", user_id="asha")
print(report.imported, "imported,", report.duplicates, "already there,", report.skipped_total, "skipped")
print(memory.search("Which Postgres version is staging on?", user_id="asha").text)
history.jsonljson
{"role": "user", "content": "Our staging database is Postgres 16 on db-staging.internal.", "timestamp": "2026-09-14T10:02:00Z", "session_id": "ops-1"}
{"role": "assistant", "content": "Noted: staging runs Postgres 16 on db-staging.internal.", "timestamp": "2026-09-14T10:02:05Z", "session_id": "ops-1"}
outputtext
2 imported, 0 already there, 0 skipped
[2026-09-14 (Mon) 10:02] user: Our staging database is Postgres 16 on db-staging.internal.
[2026-09-14 (Mon) 10:02] assistant: Noted: staging runs Postgres 16 on db-staging.internal.

Whose memory, and when

--user-id stores every turn under that user, and --agent-id under that agent. Without them, a Mem0 or JSONL record's own user_id and agent_id are kept, and everything else goes to default. Timestamps keep their instant whatever their offset, and one without an offset is read as UTC. A message without a readable time takes the time of the message before it, so the order holds. --since and --until select by these times (since <= time < until).

Untrusted input, bounded

Every file is treated as untrusted: nothing in it is executed, nothing is extracted to disk, and every read is bounded (ImportLimits).

  • JSONL is read line by line, and a conversations.json one conversation at a time, so a large export never sits in memory whole.
  • A file over 1 GiB, a zip with more than 100,000 entries, a zip entry over 1 MiB that expands more than 100 times, and an entry name that is absolute or holds .. or a drive letter are refused.
  • A record over 32 MiB, nested deeper than 64 levels, or a turn over 100,000 characters is skipped and counted. So is a malformed line; --strict stops at the first one instead.
  • Credentials are redacted before anything is stored: API keys (sk-..., Stripe, Google, GitHub, Slack), AWS keys, private key blocks, bearer tokens and JWTs, passwords in URLs, and api_key = ... style values. --no-redact turns this off.

Options

Exit code 0 when every file was read (skipped and malformed records are counted, not errors), 1 when the import failed or a file couldn't be read, 2 for a usage error.

OptionWhat it does
--dbSQLite file, or a postgresql:// or mongodb:// URL (default ~/.llmslim/memory.db, or LLMSLIM_MEMORY_DB).
--user-id, --agent-idStore every turn under this user or agent.
--dry-runRead, count and report; store nothing.
--since, --untilOnly turns at or after, or before, this time (ISO-8601, such as 2025-01-31).
--limit NStop after N new turns; running again picks up where it stopped.
--include-tool-outputAlso import tool calls and results, as tool turns.
--strictStop at the first malformed or oversized record.
--no-redactStore text without the credential pass.
--jsonPrint the report as JSON on stdout.

Limits

The formats belong to other companies and aren't published specifications. Claude Code's transcripts and the ChatGPT and Claude.ai exports are read in the shapes they have in practice (October 2026); a record in a shape this release doesn't know is skipped and counted, never guessed at. Attachments aren't imported. If you import a Claude Code session while a reply is being written, the part written so far is what is stored, so import after the session ends.

Questions

Does importing call a model or upload anything?

No. It reads the files on your machine and writes to the store you name. Nothing is summarized, and nothing is sent anywhere.

What happens if I import the same export twice?

Only what is new is stored. Each turn's id comes from the source's message id, or from a hash of its role, time and text, so the second run counts the rest as already there.

Are secrets in old conversations stored?

Obvious credentials are replaced with [REDACTED:<kind>] first, and tool output is left out unless you ask for it. The redaction is best-effort: it knows common key shapes, not every secret.