{"id":51436486,"url":"https://github.com/davidmalko87/gemini-chat-exporter","last_synced_at":"2026-07-05T07:02:09.263Z","repository":{"id":368271886,"uuid":"1282257301","full_name":"davidmalko87/gemini-chat-exporter","owner":"davidmalko87","description":"Export \u0026 back up your Google Gemini chat history to JSON + Markdown - via browser userscript or Google Takeout - with a built-in completeness/corruption verifier. Local \u0026 private, no API key.","archived":false,"fork":false,"pushed_at":"2026-06-29T19:35:29.000Z","size":75,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-29T21:24:04.863Z","etag":null,"topics":["archiving","backup","chat-export","chat-history","cli","conversation-export","data-export","data-portability","gemini","gemini-ai","google-gemini","google-takeout","json","markdown","python","tampermonkey","userscript","web-scraping"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/davidmalko87.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-27T14:31:48.000Z","updated_at":"2026-06-29T19:35:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/davidmalko87/gemini-chat-exporter","commit_stats":null,"previous_names":["davidmalko87/gemini-chat-exporter"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/davidmalko87/gemini-chat-exporter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidmalko87%2Fgemini-chat-exporter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidmalko87%2Fgemini-chat-exporter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidmalko87%2Fgemini-chat-exporter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidmalko87%2Fgemini-chat-exporter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/davidmalko87","download_url":"https://codeload.github.com/davidmalko87/gemini-chat-exporter/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/davidmalko87%2Fgemini-chat-exporter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35145900,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["archiving","backup","chat-export","chat-history","cli","conversation-export","data-export","data-portability","gemini","gemini-ai","google-gemini","google-takeout","json","markdown","python","tampermonkey","userscript","web-scraping"],"created_at":"2026-07-05T07:02:07.807Z","updated_at":"2026-07-05T07:02:09.245Z","avatar_url":"https://github.com/davidmalko87.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Gemini Chat Exporter\n\n[![CI](https://github.com/davidmalko87/gemini-chat-exporter/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/davidmalko87/gemini-chat-exporter/actions/workflows/ci.yml)\n[![Python](https://img.shields.io/badge/python-3.10%2B-3776AB.svg?logo=python\u0026logoColor=white)](https://www.python.org/)\n[![Userscript](https://img.shields.io/badge/userscript-Tampermonkey-00485B.svg?logo=tampermonkey\u0026logoColor=white)](#install)\n[![Sources](https://img.shields.io/badge/sources-userscript%20%7C%20Takeout-success.svg)](#two-ways-to-export--and-why-it-matters)\n[![Output](https://img.shields.io/badge/output-JSON%20%7C%20Markdown-success.svg)](#what-gets-exported)\n[![Verified](https://img.shields.io/badge/corruption-verified-success.svg)](#-verified)\n[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](#)\n[![Last commit](https://img.shields.io/github/last-commit/davidmalko87/gemini-chat-exporter.svg)](https://github.com/davidmalko87/gemini-chat-exporter/commits/master)\n[![GitHub issues](https://img.shields.io/github/issues/davidmalko87/gemini-chat-exporter.svg)](https://github.com/davidmalko87/gemini-chat-exporter/issues)\n\n**Export \u0026 back up your Google Gemini chat history** to **JSON + Markdown** — from a\nhardened **browser userscript** or from **Google Takeout** — with a built-in\n**verifier** that refuses to let a silently corrupt export pass for a good one.\nFully local, no API key, no servers, no telemetry.\n\n---\n\n## Why?\n\nGetting your own Gemini conversations out, completely and faithfully, is harder than it\nshould be:\n\n- **Google Takeout used to omit full transcripts.** It now includes prompts *and*\n  responses (My Activity \u003e Gemini Apps), but the schema varies by account and is\n  under-documented — you have to verify what you actually got.\n- **The page HTML has no conversation content.** Gemini is an Angular SPA; transcripts\n  are rendered client-side after load, and `gemini.google.com` blocks iframing and\n  background fetches. So a scraper has to drive the real, logged-in UI.\n- **A naive scraper fails silently — and that is worse than failing loudly.** A prior\n  run of an earlier exporter scraped 473 conversations but only the first ~190 had real\n  bodies. The rest were overwritten with one duplicated stub, while it reported *zero\n  errors*. \"Zero exceptions\" is not \"zero wrong content\":\n\n```\n[FAIL] 285/473 conversations (60.3%) share a body with another conversation\n       - exceeds the 2.0% threshold. This is the duplicate-stub corruption fingerprint.\n[FAIL] contiguous duplicate run: conversations #193-#473 (281 in a row) all share\n       one body (\"Spheres for Self-Realization\") - classic stale-page corruption\nRESULT: FAIL\n```\n\nThat report is this tool's verifier running on the actual corrupt file. The whole\nproject is built so that failure mode can never go unnoticed again.\n\nFor backing up other platforms, see the sibling project\n[`jira-project-backup-restore`](https://github.com/davidmalko87/jira-project-backup-restore).\n\n---\n\n## How it works\n\nIt is **two steps**, and they use two different tools:\n\n1. **Capture** — a browser userscript adds an **\"Export all\"** button to Gemini.\n   It clicks through every conversation and streams them to one file,\n   `gemini_chats.ndjson`. *(This part runs in your browser because Gemini only\n   renders chats in the logged-in UI.)*\n2. **Process** — a small Python toolkit reads that file, **verifies** it isn't\n   corrupt, and **converts** it to readable per-conversation JSON + Markdown.\n\n```\n Browser (userscript)            Your computer (Python)\n ┌──────────────────┐            ┌───────────────────────────────┐\n │ \"Export all\" btn │ ─────────► │ gemini_chats.ndjson           │\n │  scrapes chats   │   saves    │   → python main.py --verify   │\n └──────────────────┘            │   → python main.py --convert  │\n                                 │   → out/*.json + *.md         │\n                                 └───────────────────────────────┘\n```\n\n\u003e Don't want to scrape? Skip step 1 and feed a **Google Takeout** export into\n\u003e step 2 instead (`--import-takeout`). See [Quick Start](#quick-start).\n\n---\n\n## Two ways to export — and why it matters\n\n| Route | Fidelity | Cost / risk | Role |\n|---|---|---|---|\n| **Userscript / console scraper** (JS) | Full multi-turn transcripts | Undocumented selectors; fragile; *can* lag the UI | Convenient live capture — hardened |\n| **Google Takeout** (Python importer) | Authoritative — Google generates it | Manual export; schema varies; My Activity is prompt-centric | Trustworthy / recovery route |\n| **Verifier** (Python) | — | — | The safety net: proves an export is distinct \u0026 complete before you trust it |\n\nUse the scraper for convenience, Takeout for ground truth, and **always run the\nverifier** on whatever you get. `reconcile` can diff the two and recover conversations\nthe scraper got wrong from Takeout.\n\n---\n\n## Features\n\n| Feature | Description |\n|---|---|\n| **Content-identity validation** | The scraper won't save a conversation until the URL id + first prompt match the target **and differ from the previous one** — the guard the old tool lacked |\n| **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 |\n| **Streaming writes + resume** | Conversations stream to disk one at a time (no giant in-memory string); already-saved ids are skipped on re-run |\n| **Fail loudly** | Zero links or zero conversation containers throws a clear \"DOM may have changed\" error instead of emitting empty output |\n| **Independent verifier** | `--verify` audits any export for duplicate bodies, ratio, contiguous runs, duplicate/missing ids; exits non-zero for cron/CI |\n| **Schema-tolerant Takeout import** | Handles both the My Activity array form and the conversations object form |\n| **JSON + Markdown + manifest** | Per-conversation files plus `manifest.json` with checksums and a `complete` flag tied to verification |\n| **Reconcile** | Diffs a scraper export against Takeout and flags which corrupt conversations are recoverable |\n\n---\n\n## Install\n\nYou install **two pieces** — the browser userscript (captures chats) and the\nPython toolkit (verifies + converts).\n\n### 1. Browser userscript — the capture step\n\n1. Install a userscript manager — **[Tampermonkey](https://www.tampermonkey.net/)**\n   (Chrome, Edge, Firefox, Safari) is the easiest.\n2. Open this raw file and click **Install** in the dialog Tampermonkey shows:\n   **[gemini-chat-exporter.user.js](https://raw.githubusercontent.com/davidmalko87/gemini-chat-exporter/master/userscript/gemini-chat-exporter.user.js)**\n3. In the Tampermonkey dashboard, confirm the script header reads\n   **`@version 0.1.3`** (or newer). To update later: Tampermonkey ▸ *Check for\n   userscript updates*.\n\n\u003e **No userscript manager?** You don't need one. Copy all of\n\u003e [`userscript/console-snippet.js`](userscript/console-snippet.js), open\n\u003e \u003chttps://gemini.google.com/\u003e, press **F12 ▸ Console**, paste it, and press Enter.\n\n### 2. Python toolkit — the verify / convert step\n\nRequires **Python 3.10+**.\n\n```bash\ngit clone https://github.com/davidmalko87/gemini-chat-exporter.git\ncd gemini-chat-exporter\npip install -r requirements.txt\n```\n\nCheck it works:\n\n```bash\npython main.py --help\n```\n\n---\n\n## Quick Start\n\n\u003e Install both pieces first (see [Install](#install)).\n\n### Step 1 — Capture your chats from the browser\n\n1. Open **\u003chttps://gemini.google.com/\u003e** and make sure you are **signed in**.\n2. Click the blue **\"GCE: Export all\"** button in the **bottom-right** of the page.\n3. Watch the button — it walks through these states:\n   - **`GCE: loading list... N`** — it opens the sidebar and scrolls your whole\n     chat list (can take a minute for hundreds of chats).\n   - A **save dialog** appears — choose where to put **`gemini_chats.ndjson`**.\n     *(If your browser doesn't support the picker, the file just downloads at the\n     end instead.)*\n   - **`GCE: 12/542`** — it exports each conversation in turn. **Keep the tab open\n     and in front.** A full history takes roughly **30–45 minutes**.\n4. When it's done you get an alert: *\"Done. N saved, M skipped, K failed...\"*.\n\nIt is built to be safe and unattended-friendly:\n\n- opens the sidebar if collapsed, and **re-opens it if it collapses mid-run**;\n- loads the **full** lazy-paginated list (not just the first page);\n- **skips chats already saved**, so you can stop and re-run any time (resume);\n- **never saves a stale or duplicate page** — those are marked `failed` and\n  retried on the next run, instead of silently corrupting your backup.\n\n\u003e **Prefer not to scrape?** Use Google Takeout as the source instead — it's the\n\u003e authoritative route. At [takeout.google.com](https://takeout.google.com/):\n\u003e *Deselect all* ▸ select **\"My Activity\"** ▸ set format to **JSON** ▸ click\n\u003e *\"All activity data included\"* and keep only **\"Gemini Apps\"** ▸ export.\n\u003e (The standalone **\"Gemini\"** product exports *Gems*, not your chats.) Then go\n\u003e to Step 3 and use `--import-takeout`.\n\n### Step 2 — Verify the capture (always do this)\n\n```bash\npython main.py --verify gemini_chats.ndjson\n```\n\nYou want **`RESULT: PASS`**. If it fails, the report names exactly what's wrong\n(duplicate bodies, a stale-page run, missing chats) — **don't trust the export\nuntil it passes**. (`--verify` exits non-zero on failure, so it fits cron/CI.)\n\n### Step 3 — Convert to JSON + Markdown\n\n```bash\n# from a scraper capture:\npython main.py --convert gemini_chats.ndjson --out ./out\n\n# ...or straight from a Google Takeout export:\npython main.py --import-takeout \"Takeout/My Activity/Gemini Apps/MyActivity.json\" --out ./out\n```\n\nYou get one `.json` and one `.md` per conversation in `./out/`, plus a\n`manifest.json` (checksums + a `complete` flag). To recover chats a scrape got\nwrong, diff against Takeout:\n\n```bash\npython main.py --reconcile gemini_chats.ndjson MyActivity.json\n```\n\n### Don't like flags? Use the menu\n\nRun `python main.py` with **no arguments** for an interactive menu:\n\n```\n==================================================\n  Gemini Chat Exporter v0.1.3\n==================================================\n  --- Verify ---\n  1) Verify / audit an export for corruption\n  --- Import \u0026 convert ---\n  2) Import a Google Takeout export\n  3) Convert an export to JSON + Markdown\n  --- Reconcile ---\n  4) Reconcile a scraper export against Takeout\n  0) Exit\n```\n\n---\n\n## Troubleshooting\n\n| What you see | Why, and what to do |\n|---|---|\n| **\"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. |\n| **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+.** |\n| **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). |\n| **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). |\n| **Where did the file go?** | Wherever you chose in the save dialog. If your browser lacks that dialog, it's in your **Downloads** as `gemini_chats.ndjson`. |\n| **`python: command not found`** | Try `python3`, or install **Python 3.10+**. |\n| **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. |\n| **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`](userscript/gemini-chat-exporter.user.js) (or open an issue). |\n\n---\n\n## What Gets Exported\n\n| File | Contents |\n|---|---|\n| `gemini_chats.ndjson` | Scraper output: one JSON conversation per line (`id`, `title`, `turns`, `source`, `flags`) — streaming-friendly and resumable |\n| `NNNN_\u003cslug\u003e_\u003cid\u003e.json` | One structured JSON file per conversation (ASCII filename; UTF-8 content) |\n| `NNNN_\u003cslug\u003e_\u003cid\u003e.md` | One readable Markdown transcript per conversation (David / Gemini headers) |\n| `manifest.json` | Per-file SHA-256 checksums, body hashes, counts, and a `complete` flag that is only `true` when verification passed |\n\n---\n\n## ✅ Verified\n\nThis tool's verifier is **proven against the real corrupt export** that prompted the\nproject: it correctly flags 285/473 conversations as sharing a duplicated body, names\nthe dominant stub (281x), and pinpoints the contiguous corruption run (#193-#473) — and\nexits non-zero. A clean synthetic export passes end-to-end (scrape format -\u003e verify -\u003e\nJSON/Markdown -\u003e manifest with `complete: true`). The regression test that asserts this\nruns in CI on every push.\n\nHonest framing: the **scraper is best-effort** and depends on undocumented Gemini\ninternals; **Google Takeout is the authoritative route** for guaranteed-complete\nhistory. The verifier exists precisely so you never have to take either source on faith.\nAn export is only \"verified\" once the verifier passes it — structural validity alone is\nnot enough.\n\n---\n\n## Known Limitations\n\nThese are platform/source constraints, stated honestly — not all of them are fixable:\n\n| Area | Status | Notes |\n|---|---|---|\n| Gemini DOM selectors | Will break over time | Undocumented Angular internals; update `CONFIG` in the userscript when scraping returns nothing. The tool fails loudly when this happens. |\n| Takeout \"My Activity\" | Prompt-centric | Often one record per prompt and frequently **no model response**; counted and reported. Prefer the per-conversation Takeout form or the scraper for full transcripts, then `reconcile`. |\n| Images / Canvas / Gems | Not captured | Image-only replies are flagged (empty model text); generated images, Canvas docs, and Gems are out of scope. |\n| Already-corrupted dumps | Not recoverable from themselves | Once a body was overwritten by a stub it is gone; recover those ids from Takeout via `reconcile`. |\n| Re-importing into Gemini | Not possible | There is no write API; this is export/backup only (no \"restore\"). |\n\n---\n\n## Project Structure\n\n```\ngemini-chat-exporter/\n├── main.py                       # Entry point - interactive menu + CLI flags\n├── userscript/\n│   ├── gemini-chat-exporter.user.js   # Hardened Tampermonkey scraper\n│   ├── console-snippet.js             # DevTools-paste fallback\n│   └── README.md                      # Browser-capture install + usage\n├── gemini_export/\n│   ├── models.py                 # Conversation/Turn model + body hashing (dup detection)\n│   ├── config.py                 # Optional .env loader and validation\n│   ├── parsers.py                # NDJSON / legacy-txt / JSON readers\n│   ├── verify.py                 # The safety net: corruption + completeness audit\n│   ├── takeout.py                # Schema-tolerant Google Takeout importer\n│   ├── convert.py                # Per-conversation JSON + Markdown + manifest (SHA-256)\n│   ├── reconcile.py              # Diff scraper export vs Takeout\n│   ├── cli.py                    # Argparse CLI (UTF-8-safe console)\n│   └── menu.py                   # Interactive menu\n├── tests/                        # 40 offline pytest cases (synthetic fixtures only)\n├── .github/workflows/ci.yml      # ruff + pytest (3.10-3.13) + node --check userscript\n└── PROMPTS.md                    # Agent-Coding Kit (the workflow that built this)\n```\n\n---\n\n## Configuration Reference\n\nThe tool needs no credentials. Optional tunables live in `.env` (copy from\n`.env.example`) or as CLI flags:\n\n| Setting | CLI flag | Default | Description |\n|---|---|---|---|\n| `OUTPUT_ROOT` | `--out DIR` | `./out` | Where converted output is written |\n| `DUP_RATIO_FAIL` | `--dup-ratio F` | `0.02` | Fail if more than this fraction of conversations share a body |\n| `MIN_RUN` | — | `5` | Min consecutive same-body run length to flag |\n| `WRITE_JSON` | `--md-only` disables | `true` | Emit per-conversation JSON |\n| `WRITE_MARKDOWN` | `--json-only` disables | `true` | Emit per-conversation Markdown |\n| — | `--dry-run` | off | Compute everything, write nothing |\n\n---\n\n## Requirements\n\n- Python **3.10+**\n- [`python-dotenv`](https://pypi.org/project/python-dotenv/) \u003e= 1.0 (the only runtime dependency)\n- A browser + userscript manager (for live scraping); the toolkit itself is offline\n\n---\n\n## Changelog\n\nSee [CHANGELOG.md](CHANGELOG.md) for the full version history.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md). In short: no personal data in the repo,\nsynthetic test fixtures only, ASCII console output, fail loudly, prove it with the\nverifier.\n\n## License\n\n[MIT](LICENSE)\n\n## Related\n\n- [`jira-project-backup-restore`](https://github.com/davidmalko87/jira-project-backup-restore)\n  — backup \u0026 restore individual Jira Cloud projects via REST API.\n\n\u003c!--\nGitHub repo \"About\" settings (the SEO layer, set in the repo's About panel):\nDESCRIPTION:\n  Export \u0026 back up your Google Gemini chat history to JSON + Markdown - via browser\n  userscript or Google Takeout - with a built-in completeness/corruption verifier.\n  Local \u0026 private, no API key.\nTOPICS:\n  google-gemini, gemini, gemini-ai, chat-export, conversation-export, chat-history,\n  backup, data-export, google-takeout, data-portability, userscript, tampermonkey,\n  markdown, json, web-scraping, cli, python, archiving\n--\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidmalko87%2Fgemini-chat-exporter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavidmalko87%2Fgemini-chat-exporter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidmalko87%2Fgemini-chat-exporter/lists"}