# Import your chat history

> **Upcoming in 1.0 (preview). These pages describe the main branch of LLMSlim Core, which becomes the 0.13 release candidate and then 1.0. `pip install llmslim` installs 0.12.0 today, which doesn't have everything described here.** Docs for what pip installs: https://www.llmslim.app/docs.md

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

llmslim import reads Claude Code transcripts, ChatGPT and Claude.ai exports, Mem0 exports and JSONL, and stores each message verbatim under its original time, with no model call.

Category: Guides & Strategies · Version: 1.0 · Updated: November 2026 · 6 min read

## 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.

terminal:

```bash
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.

terminal:

```bash
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.

| 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`.

import_history.py:

```python
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.jsonl:

```json
{"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"}
```

output:

```text
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.

> **Redaction is best-effort:** It knows these shapes only, and it can't recognise a secret that looks like ordinary text. Keep tool output off unless you need it.

## 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.

## FAQ

### 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.

## Related

- [Migrate from Mem0](https://www.llmslim.app/docs/1.0/migrate-from-mem0.md)
- [Coding agents](https://www.llmslim.app/docs/1.0/coding-agents.md)
- [MCP quickstart](https://www.llmslim.app/docs/1.0/quickstart-mcp.md)

Canonical page: https://www.llmslim.app/docs/1.0/import-history
