Import your chat history
Claude Code, ChatGPT, Claude.ai, Mem0 and JSONL into memory, in one command.
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.
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.zipThe 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.
llmslim import claude-code --dry-run 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 storedRun 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.
| Format | Reads | A session is | Left 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 transcript | Thinking, 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 |
chatgpt | The export zip as downloaded, its folder, or conversations.json | One conversation | Branches you moved away from (only the thread you saw last is read), reasoning, system messages, custom instructions, tool messages, images |
claude-ai | The export zip, its folder, or conversations.json | One conversation | Thinking, tool use and results, attachments and files, older branches |
mem0 | A JSON list of memories, {"results": [...]} as get_all returns it, {"memories": [...]}, or JSONL | One (user_id, agent_id, run_id) scope | Nothing: each memory becomes one user turn |
jsonl | One {"role", "content", "timestamp"?, "session_id"?, "user_id"?, "agent_id"?, "id"?} per line, in a file or a folder of .jsonl files | One session_id | system lines; tool lines without --include-tool-output |
From Python
import_history takes the same options as the command and returns an ImportReport.
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){"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"}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.jsonone 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;
--strictstops 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, andapi_key = ...style values.--no-redactturns 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.
| Option | What it does |
|---|---|
--db | SQLite file, or a postgresql:// or mongodb:// URL (default ~/.llmslim/memory.db, or LLMSLIM_MEMORY_DB). |
--user-id, --agent-id | Store every turn under this user or agent. |
--dry-run | Read, count and report; store nothing. |
--since, --until | Only turns at or after, or before, this time (ISO-8601, such as 2025-01-31). |
--limit N | Stop after N new turns; running again picks up where it stopped. |
--include-tool-output | Also import tool calls and results, as tool turns. |
--strict | Stop at the first malformed or oversized record. |
--no-redact | Store text without the credential pass. |
--json | Print 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.