Gemini Chat Exporter
Export & back up your Google Gemini chat history to JSON + Markdown — from a hardened browser userscript or from Google Takeout — with a built-in verifier that refuses to let a silently corrupt export pass for a good one. Fully local, no API key, no servers, no telemetry.
Why?
Getting your own Gemini conversations out, completely and faithfully, is harder than it should be:
- Google Takeout is prompt-centric. My Activity > Gemini Apps includes your prompts, but often no model response, and the schema varies by account and is under-documented — you have to verify what you actually got.
- The page HTML has no conversation content. Gemini is an Angular SPA; transcripts
are rendered client-side after load, and
gemini.google.comblocks iframing and background fetches. So a scraper has to drive the real, logged-in UI. - A naive scraper fails silently — and that is worse than failing loudly. A prior run of an earlier exporter scraped 473 conversations but only the first ~190 had real bodies. The rest were overwritten with one duplicated stub, while it reported zero errors. "Zero exceptions" is not "zero wrong content":
[FAIL] 285/473 conversations (60.3%) share a body with another conversation
- exceeds the 2.0% threshold. This is the duplicate-stub corruption fingerprint.
[FAIL] contiguous duplicate run: conversations #193-#473 (281 in a row) all share
one body ("Spheres for Self-Realization") - classic stale-page corruption
RESULT: FAIL
That report is this tool's verifier running on the actual corrupt file. The whole project is built so that failure mode can never go unnoticed again.
For backing up other platforms, see the sibling project
jira-project-backup-restore.
How it works
It is two steps, and they use two different tools:
- Capture — a browser userscript adds an "Export all" button to Gemini.
It clicks through every conversation and streams them to one file,
gemini_chats.ndjson. (This part runs in your browser because Gemini only renders chats in the logged-in UI.) - Process — a small Python toolkit reads that file, verifies it isn't corrupt, and converts it to readable per-conversation JSON + Markdown.
Browser (userscript) Your computer (Python)
┌──────────────────┐ ┌───────────────────────────────┐
│ "Export all" btn │ ─────────► │ gemini_chats.ndjson │
│ scrapes chats │ saves │ → gemini-export --verify │
└──────────────────┘ │ → gemini-export --convert │
│ → out/*.json + *.md │
└───────────────────────────────┘
Don't want to scrape? Skip step 1 and feed a Google Takeout export into step 2 instead (
--import-takeout). See Quick Start.
Two ways to export — and why it matters
| Route | Fidelity | Cost / risk | Role |
|---|---|---|---|
| Userscript / console scraper (JS) | Full multi-turn transcripts | Undocumented selectors; fragile; can lag the UI | Convenient live capture — hardened |
| Google Takeout (Python importer) | Authoritative — Google generates it | Manual export; schema varies; My Activity is prompt-centric | Trustworthy / recovery route |
| Verifier (Python) | — | — | The safety net: catches duplicate-body / stale-page corruption and duplicate ids before you trust an export |
Use the scraper for convenience, Takeout for ground truth, and always run the
verifier on whatever you get (--import-takeout runs it for you on Takeout files).
reconcile diffs the two and lists which corrupt scraper conversations Takeout can
replace.
Features
| Feature | Description |
|---|---|
| Content-identity validation | The scraper won't save a conversation until the URL id matches the target and the first prompt is present and differs from the previous one — the guard the old tool lacked |
| Duplicate-body guard | Each body is SHA-256 hashed; a body identical to the previous one is retried once, then marked failed instead of saved |
| Streaming writes + resume | Each conversation is written as one NDJSON line to the file you picked (or buffered and downloaded at the end when there is no save dialog); the file is committed when the run finishes. Already-saved ids are skipped on re-run |
| Fail loudly | Zero sidebar links aborts with a clear error instead of emitting empty output; a chat whose content never renders is marked failed, not saved |
| Independent verifier | --verify audits any export for duplicate bodies, ratio, contiguous runs and duplicate ids; exits non-zero for cron/CI |
| Schema-tolerant Takeout import | Handles both the My Activity array form and the conversations object form |
| JSON + Markdown + manifest | Per-conversation files plus manifest.json with checksums and a complete flag tied to verification |
| Reconcile | Diffs a scraper export against Takeout and flags which corrupt conversations are recoverable |
Install
You install two pieces — the browser userscript (captures chats) and the Python toolkit (verifies + converts).
1. Browser userscript — the capture step
- Install a userscript manager — Tampermonkey (Chrome, Edge, Firefox, Safari) is the easiest.
- Open this raw file and click Install in the dialog Tampermonkey shows: gemini-chat-exporter.user.js
- In the Tampermonkey dashboard, confirm the script header reads
@version 0.1.3(or newer). To update later: Tampermonkey ▸ Check for userscript updates.
No userscript manager? You don't need one. Copy all of
userscript/console-snippet.js, open https://gemini.google.com/, press F12 ▸ Console, paste it, and press Enter.
2. Python toolkit — the verify / convert step
Requires Python 3.10+.
Via PyPI (recommended) — provides the gemini-export command:
pip install gemini-chat-exporter
Or as an isolated app with pipx — keeps its dependencies out of your global Python:
pipx install gemini-chat-exporter
Upgrade:
pip install -U gemini-chat-exporter # pipx: pipx upgrade gemini-chat-exporter
Or clone and run from source (development):
git clone https://github.com/davidmalko87/gemini-chat-exporter.git
cd gemini-chat-exporter
pip install -r requirements.txt
python main.py --help # same flags as gemini-export
Where files live: everything is relative to the folder you run
gemini-exportfrom. An optional.envis read only from that folder (variables already set in your shell win),--verify/--reconcilewriteverify_report.json/reconcile_report.jsoninto it (or into--out DIR, which must already exist), and converted output goes to./out(change it with--out DIRorOUTPUT_ROOT). There is no log file and nothing is written to your home or install folder; a clone behaves the same way.
Check it works:
gemini-export --help # all flags
gemini-export --version # installed version
From a clone, use python main.py in place of gemini-export — the flags are identical.
Quick Start
Install both pieces first (see Install).
Step 1 — Capture your chats from the browser
- Open https://gemini.google.com/ and make sure you are signed in.
- Click the blue "GCE: Export all" button in the bottom-right of the page.
- Watch the button — it walks through these states:
GCE: loading list... N— it opens the sidebar and scrolls your whole chat list (can take a minute for hundreds of chats).- A save dialog may appear — choose where to put
gemini_chats.ndjson. (If it doesn't — your browser lacks the picker, or loading a long list took too long — the file just downloads at the end instead.) GCE: 12/542— it exports each conversation in turn. Keep the tab open and in front. A full history takes roughly 30–45 minutes.
- When it's done you get an alert: "Done. N saved, M skipped, K failed...".
It is built to be safe and unattended-friendly:
- opens the sidebar if collapsed, and re-opens it if it collapses mid-run;
- loads the full lazy-paginated list (not just the first page);
- skips chats already saved when you run it again (resume) — but the file is only written when a run finishes, so keep the tab open until the "Done" alert (see Troubleshooting if a run was interrupted);
- refuses stale or duplicate pages it detects — those are marked
failedand retried on the next run, instead of silently corrupting your backup.
Prefer not to scrape? Use Google Takeout as the source instead — it's the authoritative route. At takeout.google.com: Deselect all ▸ select "My Activity" ▸ set format to JSON ▸ click "All activity data included" and keep only "Gemini Apps" ▸ export. (The standalone "Gemini" product exports Gems, not your chats.) Then go to Step 3 and use
--import-takeout.
Step 2 — Verify the capture (always do this)
Run this in the folder that holds gemini_chats.ndjson, or pass the file's full
path (e.g. ~/Downloads/gemini_chats.ndjson):
gemini-export --verify gemini_chats.ndjson
You want RESULT: PASS. If it fails, the report names exactly what's wrong
(duplicate bodies, a stale-page run, duplicate ids) — don't trust the export
until it passes. (--verify exits non-zero on failure, so it fits cron/CI. It
also saves the report as verify_report.json in the current folder.)
A PASS means no corruption was found, not that every chat was captured: chats the
userscript marked failed only show up as a "no model text" warning, so also check
the K failed count in the final alert.
Step 3 — Convert to JSON + Markdown
# from a scraper capture:
gemini-export --convert gemini_chats.ndjson --out ./out
# ...or straight from a Google Takeout export:
gemini-export --import-takeout "Takeout/My Activity/Gemini Apps/MyActivity.json" --out ./out
You get one .json and one .md per conversation in ./out/, plus a
manifest.json (checksums + a complete flag). To see which corrupt chats
Takeout can replace, diff the capture against Takeout:
gemini-export --reconcile gemini_chats.ndjson MyActivity.json
The console shows counts; the ids of the recoverable conversations are listed in
reconcile_report.json (written to the current folder). Reconcile only reports —
it does not change or merge either file.
More examples
Verify:
gemini-export --validate gemini_chats.ndjson # same as --verify
gemini-export --verify gemini_chats.ndjson --dup-ratio 0.05 # custom duplicate threshold (0..1)
Convert / import:
gemini-export --convert gemini_chats.ndjson --out ./out --md-only # Markdown only (--json-only: JSON only)
gemini-export --convert gemini_chats.ndjson --dry-run # verify and count, write no files
gemini-export --import-takeout takeout-export.zip --out ./out # a Takeout .zip or extracted folder
Accepted inputs: --verify / --convert read the scraper's .ndjson, a JSON
array (or {"conversations": [...]}), or the legacy delimited .txt export.
--import-takeout takes MyActivity.json, a Takeout .zip, or the extracted
folder — use the .zip / folder only when the export contains just My Activity ▸
Gemini Apps (as in the recipe above); otherwise point it at MyActivity.json.
Don't run --verify on a Takeout file; --import-takeout verifies it for you.
Don't like flags? Use the menu
From a clone, run python main.py with no arguments for an interactive menu
(the installed gemini-export command has no menu — with no arguments it prints
its help):
==================================================
Gemini Chat Exporter v0.1.3
==================================================
--- Verify ---
1) Verify / audit an export for corruption
--- Import & convert ---
2) Import a Google Takeout export
3) Convert an export to JSON + Markdown
--- Reconcile ---
4) Reconcile a scraper export against Takeout
0) Exit
Troubleshooting
| What you see | Why, and what to do |
|---|---|
| "No conversation links found" | The left sidebar was collapsed (only an issue on v0.1.1 and earlier). Update to v0.1.3+ — it opens the sidebar automatically. One-off workaround: open the left sidebar so your chats are visible, then click Export. |
Total looks too low (e.g. X/333 when you have more chats) |
An old version didn't load the lazy-paginated list. Update to v0.1.2+. |
Looks stuck; FAIL keeps climbing |
On a long run the sidebar collapsed and an old version couldn't recover. Update to v0.1.3+ (it re-opens the sidebar and continues). |
A few chats marked failed |
Normal for very large conversations that don't finish rendering in time. Click "Export all" again — it retries only the failures (resume skips the rest). The re-run's file holds only the retried chats: keep it next to the first one (saving over gemini_chats.ndjson replaces your first capture), then verify and convert it separately (e.g. --out ./out-retry). |
| A run was interrupted (tab closed, browser crashed) | The file is only written when a run finishes, but the chats it reached are already remembered as saved, so a re-run would skip them. Before running again, clear the resume state: F12 ▸ Application ▸ Local Storage ▸ https://gemini.google.com ▸ delete gce_saved_ids_v1. |
| Where did the file go? | Wherever you chose in the save dialog. If no save dialog appeared, it's in your Downloads as gemini_chats.ndjson. |
python: command not found |
Try python3, or install Python 3.10+. |
gemini-export: command not found |
pip put the command in a scripts folder that isn't on your PATH. Use pipx install gemini-chat-exporter (then pipx ensurepath and open a new terminal), or activate the virtual environment you installed it into. From a clone, use python main.py instead. |
error: externally-managed-environment |
Your system Python (e.g. Debian/Ubuntu, Homebrew) blocks global pip install (PEP 668). Use pipx install gemini-chat-exporter, or create a virtual environment first: python3 -m venv .venv, activate it, then pip install gemini-chat-exporter. |
Verify says FAIL |
That's the safety net doing its job. Read the report, then re-capture or recover the flagged chats from Takeout before trusting the backup. |
| Selectors broke after a Gemini update | If a capture suddenly returns nothing, Gemini changed its DOM. Update the CONFIG selectors at the top of userscript/gemini-chat-exporter.user.js (or open an issue). |
What Gets Exported
| File | Contents |
|---|---|
gemini_chats.ndjson |
Scraper output: one JSON conversation per line (id, title, turns, source, flags) — streaming-friendly and resumable |
NNNN_<slug>.json |
One structured JSON file per conversation (ASCII filename slugged from the title, or the conversation id when the title has no Latin letters or digits; UTF-8 content) |
NNNN_<slug>.md |
One readable Markdown transcript per conversation (David / Gemini headers) |
manifest.json |
Per-file SHA-256 checksums, body hashes, counts, and a complete flag that is only true when verification passed |
verify_report.json |
Full audit from --verify / --validate, written to the current folder (or --out DIR) |
reconcile_report.json |
Counts and the recoverable / corrupt conversation ids from --reconcile, written to the current folder (or --out DIR) |
✅ Verified
This tool's verifier is proven against the real corrupt export that prompted the
project: it correctly flags 285/473 conversations as sharing a duplicated body, names
the dominant stub (281x), and pinpoints the contiguous corruption run (#193-#473) — and
exits non-zero. That file is personal data and is not in the repo, so CI runs a
synthetic regression fixture with the same shape (a duplicated stub in a contiguous run)
on every push to master and every pull request, plus a clean synthetic export that
must pass. Both fixtures use the
legacy .txt format; there is no NDJSON (scrape-format) fixture yet.
Honest framing: the scraper is best-effort and depends on undocumented Gemini internals; Google Takeout is the authoritative route for guaranteed-complete history. The verifier exists precisely so you never have to take either source on faith. An export is only "verified" once the verifier passes it — structural validity alone is not enough.
Known Limitations
These are platform/source constraints, stated honestly — not all of them are fixable:
| Area | Status | Notes |
|---|---|---|
| Gemini DOM selectors | Will break over time | Undocumented Angular internals; update CONFIG in the userscript when scraping returns nothing. If the sidebar links stop matching, the export aborts with an error; if the conversation content stops matching, chats are marked failed (watch the failed count). |
| Takeout "My Activity" | Prompt-centric | Often one record per prompt and frequently no model response; counted and reported (without_response). Use the scraper for full transcripts, and reconcile to cross-check. |
| Images / Canvas / Gems | Not captured | Image-only replies are flagged (empty model text); generated images, Canvas docs, and Gems are out of scope. |
| Already-corrupted dumps | Not recoverable from themselves | Once a body was overwritten by a stub it is gone; reconcile lists which of those ids Takeout still has. |
| Re-importing into Gemini | Not possible | There is no write API; this is export/backup only (no "restore"). |
Project Structure
gemini-chat-exporter/
├── main.py # From a clone: interactive menu (no args) + CLI flags
├── pyproject.toml # Package metadata; installs the gemini-export command
├── requirements.txt # Runtime dependency for a clone (python-dotenv)
├── .env.example # Optional settings template
├── userscript/
│ ├── gemini-chat-exporter.user.js # Hardened Tampermonkey scraper
│ ├── console-snippet.js # DevTools-paste fallback
│ └── README.md # Browser-capture install + usage
├── gemini_export/
│ ├── __init__.py # Package version
│ ├── models.py # Conversation/Turn model + body hashing (dup detection)
│ ├── config.py # Optional .env loader and validation
│ ├── parsers.py # NDJSON / legacy-txt / JSON readers
│ ├── verify.py # The safety net: corruption + duplicate-id audit
│ ├── takeout.py # Schema-tolerant Google Takeout importer
│ ├── convert.py # Per-conversation JSON + Markdown + manifest (SHA-256)
│ ├── reconcile.py # Diff scraper export vs Takeout
│ ├── cli.py # Argparse CLI behind gemini-export (UTF-8-safe console)
│ └── menu.py # Interactive menu
├── tests/ # 40 offline pytest cases (synthetic fixtures only)
├── .github/
│ ├── workflows/ci.yml # ruff + pytest (3.10-3.13) + node --check userscript
│ ├── workflows/publish.yml # Publishes to PyPI when a GitHub Release is published
│ └── dependabot.yml # Monthly dependency update PRs
├── CHANGELOG.md # Version history
├── CONTRIBUTING.md # Ground rules + release steps
├── LICENSE # MIT
└── PROMPTS.md # Agent-Coding Kit (the workflow that built this)
Configuration Reference
The tool needs no credentials. Optional tunables can be set as environment
variables, in a .env file in the folder you run gemini-export from, or as CLI
flags (flags win over both; variables already set in your shell win over .env).
.env.example is not included in the PyPI package, so create .env yourself:
OUTPUT_ROOT=./out
DUP_RATIO_FAIL=0.02
MIN_RUN=5
…or download the template (from a clone: cp .env.example .env):
curl -o .env https://raw.githubusercontent.com/davidmalko87/gemini-chat-exporter/master/.env.example
| Setting | CLI flag | Default | Description |
|---|---|---|---|
OUTPUT_ROOT |
--out DIR |
./out |
Where converted output is written (--out also moves the report files) |
DUP_RATIO_FAIL |
--dup-ratio F |
0.02 |
Fail if more than this fraction (0..1) of conversations share a body |
MIN_RUN |
— | 5 |
Min consecutive same-body run length to flag |
| — | --json-only |
off | Write only the per-conversation JSON (skip Markdown) |
| — | --md-only |
off | Write only the Markdown transcripts (skip JSON) |
| — | --dry-run |
off | For --convert / --import-takeout: verify and count, but write no files (--verify / --reconcile still write their report) |
WRITE_JSON / WRITE_MARKDOWN from older .env files no longer change the output
(and setting both to false is rejected as invalid); remove them and use
--json-only / --md-only.
Requirements
- Python 3.10+
python-dotenv>= 1.0 (the only runtime dependency)- A browser + userscript manager (for live scraping); the toolkit itself is offline
Changelog
See CHANGELOG.md for the full version history.
Contributing
See CONTRIBUTING.md. In short: no personal data in the repo, synthetic test fixtures only, ASCII console output, fail loudly, prove it with the verifier.
License
Related
jira-project-backup-restore— backup & restore individual Jira Cloud projects via REST API.
Metadata
Release files for gemini-chat-exporter 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| gemini_chat_exporter-0.1.3.tar.gz | 33.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gemini_chat_exporter-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 64.5 kB
Release files / gemini_chat_exporter-0.1.3.tar.gz
| Download URL | gemini_chat_exporter-0.1.3.tar.gz |
|---|---|
| Size | 33.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
87891c9dd1283cb173a6dfb2108ccda83d0504703bf5f0b29a3b8bf989ea7f61
|
|
BLAKE2b-256 checksum How to use checksums |
a0f638edb99368954136e863f7f02b741e7bf7ebc30f76855f72bc6aca50850d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logRelease files / gemini_chat_exporter-0.1.3-py3-none-any.whl
| Download URL | gemini_chat_exporter-0.1.3-py3-none-any.whl |
|---|---|
| Size | 31.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fd7b068833056efcaae6c7f43daff9d3ea57c026911bf4f2b0b840458cee9f16
|
|
BLAKE2b-256 checksum How to use checksums |
5ff165bee78e4c9db5a2e9075d9d82ffaafbb8df926556635e57b66888683153
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency log