leveldb-ops
Read and inspect LevelDB stores - especially Chromium/Electron app state (Local Storage, IndexedDB, Session Storage). Triggers on: leveldb, .ldb files, IndexedDB, Local Storage, Chromium storage, Electron app state, claude.ai cache, browser forensics, decode app state, claude des
Install
npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/leveldb-ops
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
git clone https://github.com/0xDarkMatter/claude-mods.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
LevelDB Operations
Read and decode LevelDB stores — primarily the Chromium/Electron storage layers (Local Storage, IndexedDB, Session Storage) used by every Electron app on disk: Claude Desktop, VS Code, Discord, Slack, Obsidian.
What is LevelDB
Embedded key-value store by Google. Sorted KV map, no SQL, no server. Format: a folder of .ldb (sorted runs), .log (write-ahead), MANIFEST-*, CURRENT, LOCK. Both keys and values are arbitrary bytes.
Chromium layers richer formats on top:
- Local Storage — flat key→string map, scoped per origin. Easiest to read.
- Session Storage — same shape, per-tab.
- IndexedDB — per-origin databases with object stores, indexes, versioned schemas. Encoded with v8 serialization. Needs a real reader.
When This Skill Triggers
- "What's in the Local Storage of
- "Decode IndexedDB" / "read .ldb files"
- "Why does the sidebar show X" / "where does the desktop app cache Y"
- "Reset / mutate Electron app state"
- Forensic-style probes of Chrome/Edge/Brave/Electron state
Critical Safety Protocol
LevelDB uses an exclusive LOCK file. A running app holds it. Trying to open a live store fails OR silently returns stale snapshots.
Always copy before reading:
# Copy the entire leveldb dir to a temp location
cp -r "$APPDATA/Claude/Local Storage/leveldb" /tmp/probe/local-storage-db
cp -r "$APPDATA/Claude/IndexedDB/https_claude.ai_0.indexeddb.leveldb" /tmp/probe/indexeddb
# Remove the copied LOCK file so the reader can open it
rm -f /tmp/probe/local-storage-db/LOCK /tmp/probe/indexeddb/LOCK
The cp -r will warn Device or resource busy for the LOCK file itself — that's fine, the data files copy successfully.
Never write to the live store while the app is running. It will corrupt the LSM and crash the app. Quit the app first if you need to mutate.
Setup
plyvel and similar require native compilation and lack Windows wheels. Use ccl_chromium_reader — pure Python, written for browser forensics.
uv venv .venv --python 3.13
source .venv/Scripts/activate # or .venv/bin/activate on Unix
uv pip install "git+https://github.com/cclgroupltd/ccl_chrome_indexeddb.git"
Not on PyPI — install direct from GitHub. Pulls in ccl-simplesnappy and brotli as transitive deps.
Reading Local Storage
Storage keys are origin URLs (https://claude.ai). Records are append-only — duplicate script_key entries mean older versions; the last record wins.
import pathlib
from ccl_chromium_reader import ccl_chromium_localstorage
ls = ccl_chromium_localstorage.LocalStoreDb(pathlib.Path("./local-storage-db"))
# List all origins
for origin in sorted(set(ls.iter_storage_keys())):
print(origin)
# Dump one origin, latest-value-wins
latest = {}
for rec in ls.iter_records_for_storage_key("https://claude.ai"):
latest[rec.script_key] = rec.value
for k, v in latest.items():
print(f"{k}: {repr(v)[:200]}")
See scripts/dump_localstorage.py for the full reusable script.
Reading IndexedDB
IndexedDB is more complex — wrapped object stores with v8-serialized values. ccl_chromium_reader parses it cleanly:
from ccl_chromium_reader import ccl_chromium_indexeddb
db = ccl_chromium_indexeddb.WrappedIndexDB(pathlib.Path("./indexeddb"))
for db_id in db.database_ids:
wdb = db[db_id.dbid_no]
print(f"DB: {wdb.name}")
for store_name in wdb.object_store_names:
store = wdb[store_name]
for rec in store.iterate_records():
print(f" {rec.key!r} -> {repr(rec.value)[:200]}")
See scripts/dump_indexeddb.py.
Common Chromium Storage Locations
| OS | Path |
|---|---|
| Windows | %APPDATA%\<App>\Local Storage\leveldb\ |
| Windows | %APPDATA%\<App>\IndexedDB\https_<host>_0.indexeddb.leveldb\ |
| macOS | ~/Library/Application Support/<App>/Local Storage/leveldb/ |
| Linux | ~/.config/<App>/Local Storage/leveldb/ |
For raw browsers, <App> is Google/Chrome/User Data/Default, BraveSoftware/Brave-Browser/User Data/Default, etc.
Mutation (Advanced)
Writing requires either:
- Quitting the app and using a leveldb writer (Node
levelpackage, or rebuild the dir manually) — or - Patching via the app itself — many Electron apps expose DevTools. Open with the
--remote-debugging-port=<n>flag, attach, and calllocalStorage.setItem(key, value). Survives the app's normal write path so it doesn't corrupt the LSM.
For Claude Desktop specifically, see references/claude-desktop-state.md for the discovered key map.
Decision Framework
| You want to | Do |
|---|---|
| Just see what's there | Copy + ccl_chromium_reader |
| Find a specific value | strings + grep first; reader if structure matters |
| Mutate while app runs | Don't. Use DevTools remote debugging. |
| Mutate while app is closed | Quit, then Node level package or write back via re-opened leveldb |
| Cross-account recovery | Read-only forensics; can't impersonate server-bound entries |
Anti-patterns
- Opening the live store directly → silently stale or open errors
- Forgetting to remove LOCK from copy → reader fails
- Trusting first hit on a key → leveldb is append-only; iterate all and keep the last
- Using
stringsfor structured analysis → misses keys, conflates duplicates, can't distinguish origins - Writing while app runs → LSM corruption, app crash, possible data loss
Reference
- scripts/dump_localstorage.py — full Local Storage dump
- scripts/dump_indexeddb.py — full IndexedDB dump
- scripts/extract_keys.py — targeted key extraction with latest-wins
- references/claude-desktop-state.md — Claude Desktop state map (storage keys, sidebar, sessions, account binding)
- references/chromium-format.md — leveldb on-disk format, locking, append semantics
- ccl_chromium_reader: https://github.com/cclgroupltd/ccl_chrome_indexeddb
- LevelDB spec: https://github.com/google/leveldb/blob/main/doc/impl.md
Files (claude-mods)
-
references
-
chromium-format.md 3.8 KB
# Chromium LevelDB Format Notes Quick reference for the on-disk layout and gotchas when reading Chromium's leveldb stores. ## File Layout A leveldb directory contains: | File | Purpose | |------|---------| | `*.ldb` | Sorted runs (immutable SST tables). Numbered by sequence. | | `*.log` | Write-ahead log. Contains recent writes not yet compacted into `.ldb`. | | `MANIFEST-<n>` | Metadata: which `.ldb` files are live, version edits | | `CURRENT` | Tiny pointer file naming the active MANIFEST | | `LOCK` | File lock; held by the running app. **Blocks readers.** | | `LOG`, `LOG.old` | Diagnostic text logs (human-readable) | To read while the app runs: copy the dir, delete the LOCK file from your copy, open the copy. ## Append-Only Semantics LevelDB is **log-structured**. A write to key `foo` doesn't overwrite — it appends a new entry. Compaction eventually drops older versions. When iterating raw records (as `ccl_chromium_reader` does), you can see multiple entries for the same key. **The last one in iteration order is the current value.** ```python # Wrong — uses first match for rec in ls.iter_records_for_storage_key(origin): if rec.script_key == "default-model": return rec.value # might be stale # Right — iterate all, keep last latest = {} for rec in ls.iter_records_for_storage_key(origin): latest[rec.script_key] = rec.value return latest["default-model"] ``` ## Chromium's Layers on Top ### Local Storage (simple) Keys in the underlying leveldb look like: ``` META:<origin>\x00\x00\x01<script_key> ``` Values are UTF-8 strings (the actual `localStorage.setItem` payload). `ccl_chromium_localstorage.LocalStoreDb` decodes the META prefix and yields `(origin, script_key, value)` triples. ### Session Storage (per-tab) Same shape but partitioned per browsing session/tab. Use `ccl_chromium_sessionstorage`. ### IndexedDB (complex) - Multiple databases per origin - Each DB has versioned object stores - Keys are typed (string, number, array, date, binary) - Values are **v8-serialized** (not JSON) — the same format Chromium's StructuredClone uses - Indexes maintained as separate keyspaces `ccl_chromium_indexeddb.WrappedIndexDB` handles all of this. Walk: `db.database_ids` → `db[id]` → `wdb.object_store_names` → `wdb[name].iterate_records()`. ## Locking Specifics - **Windows**: `LOCK` uses `LockFile` Win32 API. Robust, no stale locks. - **macOS/Linux**: `flock(2)` advisory lock. Crashed apps may leave stale LOCK files; safe to delete. - **Reading concurrently**: Strictly speaking, leveldb supports multiple readers if openers use the same lock. In practice, copy-and-read is safest because `ccl_chromium_reader` doesn't take any lock — it just reads the bytes. ## Encryption / DPAPI Note `Local State` JSON in the same parent dir contains `os_crypt.encrypted_key` — DPAPI-encrypted on Windows, used by Chrome to encrypt cookies and saved passwords. **Local Storage and IndexedDB themselves are NOT encrypted** at rest. They're plain leveldb. Only the cookie store and password store use that key. ## When to Reach for a Different Tool | Tool | When | |------|------| | `strings` + `grep` | Quick grep for known string. No structure needed. | | `ccl_chromium_reader` | Structured Local Storage / IndexedDB. Default choice. | | Node `level` package | You need to **write** to leveldb. Better wheels than Python on Windows. | | Electron remote DevTools | Mutate live state without taking the app down. `--remote-debugging-port=<n>` then DevTools Protocol. | | `leveldb-cli` (Go) | Raw leveldb, no Chromium decoding. Good for non-Chromium leveldb stores. | ## References - LevelDB design: https://github.com/google/leveldb/blob/main/doc/impl.md - Chromium IndexedDB schema: https://chromium.googlesource.com/chromium/src/+/main/content/browser/indexed_db/ - ccl_chromium_reader: https://github.com/cclgroupltd/ccl_chrome_indexeddb -
claude-desktop-state.md 6.2 KB
# Claude Desktop State Map Decoded from `%APPDATA%\Claude\Local Storage\leveldb\` and `%APPDATA%\Claude\IndexedDB\` using `ccl_chromium_reader`. Probed 2026-04-26 against Claude Desktop v1.3109.0 (Electron 41.2.0). ## Storage Locations (Windows) | Component | Path | |-----------|------| | Local Storage | `%APPDATA%\Claude\Local Storage\leveldb\` | | IndexedDB | `%APPDATA%\Claude\IndexedDB\https_claude.ai_0.indexeddb.leveldb\` | | Session Storage | `%APPDATA%\Claude\Session Storage\` | | Account/profile JSON | `~\.claude\.claude.json` | | CLI session transcripts | `~\.claude\projects\<encoded-cwd>\<uuid>.jsonl` | | Local State (DPAPI keys) | `%APPDATA%\Claude\Local State` | The Electron app loads `https://claude.ai` as its frontend, so all browser storage is keyed to that origin. MCP content servers also appear under their own `*.claudemcpcontent.com` origins. ## Account Binding — Critical Distinction | Storage | Account-bound? | Survives logout? | |---------|---------------|------------------| | `~\.claude\projects\*.jsonl` (CLI transcripts) | **No** — pure local files, zero account markers | **Yes** | | `~\.claude\.claude.json` (CLI account state) | Yes — `userID`, `oauthAccount` | Replaced on login | | Local Storage `react-query-cache-ls` | Server-fetched cache, **per-account** | Cleared on logout | | IndexedDB `keyval-store` `react-query-cache` | Same — server cache mirror | Cleared on logout | | Local Storage `dframe-store`, `epitaxy.*` | Local UI state (pin order, layouts) | Yes, but references server IDs | **Implication:** Desktop "Code" tab sessions (visible in the sidebar) are **server-stored under whichever account created them**. Switching accounts hides them; they cannot be re-registered locally because the new account's server doesn't know about them. CLI sessions are different — fully local, account-agnostic, resumable via `claude --resume <uuid>`. ## Session ID Schemes (Two Different Things!) | Surface | ID format | Example | Storage | |---------|-----------|---------|---------| | Desktop "Code" sidebar | `local_<uuid>` | `local_00000000-0000-0000-0000-000000000030` | Server + Local Storage references | | CLI `claude --resume` | `<uuid>` (no prefix) | `04093688-bcd7-423e-9cc6-675beab2805a` | `~/.claude/projects/<encoded-cwd>/<uuid>.jsonl` | These ID spaces do not overlap. The desktop's `local_` sessions are not the same as CLI session JSONL files. ## Key Local Storage Entries (origin: `https://claude.ai`) ### Read-only / observe | Key | Shape | Purpose | |-----|-------|---------| | `react-query-cache-ls` | JSON, often huge (10-20MB) | Cached server queries: conversation list, account info, model availability. Cleared on logout. | | `__qk_hint_account_uuid` | string | Last-active account UUID (multiple values across accounts) | | `lastLoginMethod` | string | e.g., `"google"` | | `default-model` | string | e.g., `"claude-opus-4-7"` | | `branch-status-cache` | JSON | Cached git branch status per repo | | `epitaxy.sidePaneStore.v1` | JSON | Tile layouts per `local_<uuid>` session | ### Writable / mutate | Key | Shape | What you can change | |-----|-------|----------------------| | `dframe-store` | `{state: {pinnedOrder, sidebarWidth, lastKnownMode, groupByByMode, ...}}` | Pin/unpin sessions in sidebar (`pinnedOrder: ["code:local_<uuid>"]`), resize, change grouping | | `ccd-session-store` | `{state: {selectedFolder, ...}}` | Programmatically set the active project folder | | `epitaxy-unread-v1` | `{state: {unreadIds: []}}` | Mark sessions read/unread | **To mutate safely:** 1. Quit Claude Desktop (drops the LOCK and stops the app from clobbering your write) 2. Use a leveldb writer (Node `level` package, or `plyvel` on platforms with wheels) 3. Restart the app OR use Electron remote DevTools (`--remote-debugging-port=<n>`) to call `localStorage.setItem` directly while the app runs. ## IndexedDB Contents `keyval-store` database, single `keyval` object store: | Key | Value | |-----|-------| | `react-query-cache` | Mirror of the Local Storage `react-query-cache-ls` — same buster/queries | Empty post-logout, populated post-login. No additional state worth poking. ## MCP Sandboxed Origins Claude Desktop renders MCP tool widgets in iframes scoped to per-session origins: ``` https://2829b00d401b181891a38dad5b2e3140.claudemcpcontent.com/^0https://claude.ai https://2ce089db561642ac93752a8a0e5fca3b.claudemcpcontent.com/^0https://claude.ai https://45dfbdaaa3edb5a1e9641febd6bdaf76.claudemcpcontent.com/^0https://claude.ai ``` The `^0` partition suffix is Chromium's storage partitioning. Each MCP iframe's localStorage is isolated. We observed Asana widget state (task drafts) cached per-widget here — survives across sessions. ## What You Cannot Do - **Re-register cross-account sessions in the sidebar** — they're server-locked - **Recover transcripts of desktop "Code" sessions from another account** — never stored locally in full; only metadata cached, and that's cleared on logout - **Forge sidebar entries** — they'd point at server-side IDs the new account can't open ## What You Can Do - **Find local CLI sessions** by scanning `~/.claude/projects/<encoded-cwd>/*.jsonl` and resuming via `claude --resume <uuid>` - **Inspect cached server data** from `react-query-cache-ls` if it hasn't been cleared yet (logout clears it) - **Mutate UI state** — sidebar pins, project folder, sidebar width — via `dframe-store` and `ccd-session-store` - **Audit what the app remembers about you** across MCP origins, account history, model preferences ## Probe Recipe ```bash # 1. Copy stores (warning on LOCK is fine) cp -r "$APPDATA/Claude/Local Storage/leveldb" /tmp/probe/local-storage-db cp -r "$APPDATA/Claude/IndexedDB/https_claude.ai_0.indexeddb.leveldb" /tmp/probe/indexeddb rm -f /tmp/probe/{local-storage-db,indexeddb}/LOCK # 2. Reader uv venv /tmp/probe/.venv --python 3.13 source /tmp/probe/.venv/Scripts/activate uv pip install "git+https://github.com/cclgroupltd/ccl_chrome_indexeddb.git" # 3. Dump python skills/leveldb-ops/scripts/dump_localstorage.py /tmp/probe/local-storage-db --origin https://claude.ai python skills/leveldb-ops/scripts/dump_indexeddb.py /tmp/probe/indexeddb # 4. Targeted extract python skills/leveldb-ops/scripts/extract_keys.py /tmp/probe/local-storage-db dframe-store ccd-session-store ```
-
-
scripts
-
dump_indexeddb.py 1.9 KB
#!/usr/bin/env python3 """Dump Chromium IndexedDB. Usage: python dump_indexeddb.py <leveldb-dir> [--store <name>] [--max <n>] The leveldb-dir is a copy of `IndexedDB/https_<host>_0.indexeddb.leveldb/`. Remove the LOCK file from the copy first. """ import argparse import pathlib import sys from ccl_chromium_reader import ccl_chromium_indexeddb def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("path", type=pathlib.Path) ap.add_argument("--store", help="Filter by object store name") ap.add_argument("--max", type=int, default=10, help="Max records per store to print") ap.add_argument("--max-bytes", type=int, default=300, help="Truncate value repr") args = ap.parse_args() if not args.path.exists(): print(f"error: {args.path} does not exist", file=sys.stderr) return 1 db = ccl_chromium_indexeddb.WrappedIndexDB(args.path) db_ids = list(db.database_ids) if not db_ids: print("no databases found") return 0 for db_id in db_ids: wdb = db[db_id.dbid_no] print(f"\n=== DB: {wdb.name} (id={db_id.dbid_no}) ===") for store_name in wdb.object_store_names: if args.store and args.store not in store_name: continue store = wdb[store_name] recs = list(store.iterate_records()) print(f" STORE: {store_name} ({len(recs)} records)") for i, rec in enumerate(recs[: args.max]): v = repr(rec.value) if len(v) > args.max_bytes: v = v[: args.max_bytes] + f"...[+{len(repr(rec.value)) - args.max_bytes}c]" print(f" [{i}] key={rec.key!r}") print(f" val={v}") if len(recs) > args.max: print(f" ... +{len(recs) - args.max} more") return 0 if __name__ == "__main__": sys.exit(main()) -
dump_localstorage.py 1.8 KB
#!/usr/bin/env python3 """Dump Chromium Local Storage. Usage: python dump_localstorage.py <leveldb-dir> [--origin <url>] [--key <pattern>] The leveldb-dir is a copy of the app's `Local Storage/leveldb/` dir. Copy first, remove the LOCK file, then point this at the copy. """ import argparse import pathlib import sys from ccl_chromium_reader import ccl_chromium_localstorage def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("path", type=pathlib.Path, help="Path to leveldb dir") ap.add_argument("--origin", help="Filter by origin (substring match)") ap.add_argument("--key", help="Filter by script_key (substring match)") ap.add_argument("--max-bytes", type=int, default=300, help="Truncate values longer than this") args = ap.parse_args() if not args.path.exists(): print(f"error: {args.path} does not exist", file=sys.stderr) return 1 ls = ccl_chromium_localstorage.LocalStoreDb(args.path) origins = sorted(set(ls.iter_storage_keys())) for origin in origins: if args.origin and args.origin not in origin: continue # latest-wins: leveldb is append-only latest: dict = {} for rec in ls.iter_records_for_storage_key(origin): latest[rec.script_key] = rec.value keys = sorted(latest.keys()) if args.key: keys = [k for k in keys if args.key in k] if not keys: continue print(f"\n=== {origin} ({len(keys)} keys) ===") for k in keys: v = latest[k] if isinstance(v, str) and len(v) > args.max_bytes: v = v[: args.max_bytes] + f"...[+{len(latest[k]) - args.max_bytes}b]" print(f" {k!r}") print(f" => {v!r}") return 0 if __name__ == "__main__": sys.exit(main()) -
extract_keys.py 1.4 KB
#!/usr/bin/env python3 """Extract specific keys from Local Storage with full values and JSON pretty-print. Usage: python extract_keys.py <leveldb-dir> <key1> [key2] [key3] ... Latest-wins per key. JSON-decodes values when possible. """ import argparse import json import pathlib import sys from ccl_chromium_reader import ccl_chromium_localstorage def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("path", type=pathlib.Path) ap.add_argument("keys", nargs="+", help="script_keys to extract (exact match)") ap.add_argument("--origin", default="https://claude.ai", help="Origin to scan") args = ap.parse_args() ls = ccl_chromium_localstorage.LocalStoreDb(args.path) latest: dict = {} for rec in ls.iter_records_for_storage_key(args.origin): if rec.script_key in args.keys: latest[rec.script_key] = rec.value for k in args.keys: v = latest.get(k) print(f"\n=== {k} ===") if v is None: print(" (not found)") continue size = len(v) if isinstance(v, str) else 0 print(f" size: {size} bytes") try: parsed = json.loads(v) print(json.dumps(parsed, indent=2)) except (json.JSONDecodeError, TypeError): print(repr(v)) return 0 if __name__ == "__main__": sys.exit(main())
-
-
tests
-
run.sh 1.8 KB
#!/usr/bin/env bash # Offline contract tests. The shipped readers intentionally have no profile # copy helper: callers must copy and remove LOCK before invoking them, so no # production lock-safety path exists to exercise without fabricating coverage. set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL="$(dirname "$HERE")" SCRIPTS="$SKILL/scripts" SB="$(mktemp -d)" trap 'rm -rf "$SB"' EXIT PYTHON="" for c in python python3 py; do if command -v "$c" >/dev/null 2>&1 && "$c" -c "" >/dev/null 2>&1; then PYTHON="$c"; break; fi done [[ -n "$PYTHON" ]] || { echo "python is required" >&2; exit 1; } PASS=0; FAIL=0 ok() { PASS=$((PASS+1)); printf ' PASS %s\n' "$1"; } no() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; } # Minimal import surface: --help exits during argparse before reader use. mkdir -p "$SB/stub/ccl_chromium_reader" printf '' > "$SB/stub/ccl_chromium_reader/__init__.py" printf '' > "$SB/stub/ccl_chromium_reader/ccl_chromium_indexeddb.py" printf '' > "$SB/stub/ccl_chromium_reader/ccl_chromium_localstorage.py" echo "=== leveldb-ops contract self-test ===" for script in dump_indexeddb.py dump_localstorage.py extract_keys.py; do PYTHONPYCACHEPREFIX="$SB/pycache" "$PYTHON" -m py_compile "$SCRIPTS/$script" \ && ok "py_compile $script" || no "py_compile $script" # Imports are stubbed so argparse help remains offline and does not require # the optional forensic reader package. out="$(PYTHONPATH="$SB/stub" "$PYTHON" "$SCRIPTS/$script" --help 2>&1)"; rc=$? if [[ "$rc" -eq 0 && "$out" == *"usage:"* ]]; then ok "--help usage contract $script" else no "--help usage contract $script" fi done echo "" echo "NOTE: lock safety is contract-only; scripts accept pre-copied stores and expose no copy helper." echo "=== $PASS passed, $FAIL failed ===" [[ "$FAIL" -eq 0 ]]
-
-
SKILL.md 6.7 KB
--- name: leveldb-ops description: "Read and inspect LevelDB stores - especially Chromium/Electron app state (Local Storage, IndexedDB, Session Storage). Triggers on: leveldb, .ldb files, IndexedDB, Local Storage, Chromium storage, Electron app state, claude.ai cache, browser forensics, decode app state, claude desktop state." license: MIT compatibility: "Pure Python via ccl_chromium_reader. Works on Windows/macOS/Linux. No native compilation." allowed-tools: "Read Write Bash" metadata: author: claude-mods --- # LevelDB Operations Read and decode LevelDB stores — primarily the Chromium/Electron storage layers (Local Storage, IndexedDB, Session Storage) used by every Electron app on disk: Claude Desktop, VS Code, Discord, Slack, Obsidian. ## What is LevelDB Embedded key-value store by Google. Sorted KV map, no SQL, no server. Format: a folder of `.ldb` (sorted runs), `.log` (write-ahead), `MANIFEST-*`, `CURRENT`, `LOCK`. Both keys and values are arbitrary bytes. Chromium layers richer formats on top: - **Local Storage** — flat key→string map, scoped per origin. Easiest to read. - **Session Storage** — same shape, per-tab. - **IndexedDB** — per-origin databases with object stores, indexes, versioned schemas. Encoded with v8 serialization. Needs a real reader. ## When This Skill Triggers - "What's in the Local Storage of <Electron app>" - "Decode IndexedDB" / "read .ldb files" - "Why does the sidebar show X" / "where does the desktop app cache Y" - "Reset / mutate Electron app state" - Forensic-style probes of Chrome/Edge/Brave/Electron state ## Critical Safety Protocol **LevelDB uses an exclusive `LOCK` file.** A running app holds it. Trying to open a live store fails OR silently returns stale snapshots. **Always copy before reading:** ```bash # Copy the entire leveldb dir to a temp location cp -r "$APPDATA/Claude/Local Storage/leveldb" /tmp/probe/local-storage-db cp -r "$APPDATA/Claude/IndexedDB/https_claude.ai_0.indexeddb.leveldb" /tmp/probe/indexeddb # Remove the copied LOCK file so the reader can open it rm -f /tmp/probe/local-storage-db/LOCK /tmp/probe/indexeddb/LOCK ``` The `cp -r` will warn `Device or resource busy` for the `LOCK` file itself — that's fine, the data files copy successfully. **Never write to the live store while the app is running.** It will corrupt the LSM and crash the app. Quit the app first if you need to mutate. ## Setup `plyvel` and similar require native compilation and lack Windows wheels. Use **`ccl_chromium_reader`** — pure Python, written for browser forensics. ```bash uv venv .venv --python 3.13 source .venv/Scripts/activate # or .venv/bin/activate on Unix uv pip install "git+https://github.com/cclgroupltd/ccl_chrome_indexeddb.git" ``` Not on PyPI — install direct from GitHub. Pulls in `ccl-simplesnappy` and `brotli` as transitive deps. ## Reading Local Storage Storage keys are origin URLs (`https://claude.ai`). Records are append-only — duplicate `script_key` entries mean older versions; the **last record wins**. ```python import pathlib from ccl_chromium_reader import ccl_chromium_localstorage ls = ccl_chromium_localstorage.LocalStoreDb(pathlib.Path("./local-storage-db")) # List all origins for origin in sorted(set(ls.iter_storage_keys())): print(origin) # Dump one origin, latest-value-wins latest = {} for rec in ls.iter_records_for_storage_key("https://claude.ai"): latest[rec.script_key] = rec.value for k, v in latest.items(): print(f"{k}: {repr(v)[:200]}") ``` See [scripts/dump_localstorage.py](scripts/dump_localstorage.py) for the full reusable script. ## Reading IndexedDB IndexedDB is more complex — wrapped object stores with v8-serialized values. `ccl_chromium_reader` parses it cleanly: ```python from ccl_chromium_reader import ccl_chromium_indexeddb db = ccl_chromium_indexeddb.WrappedIndexDB(pathlib.Path("./indexeddb")) for db_id in db.database_ids: wdb = db[db_id.dbid_no] print(f"DB: {wdb.name}") for store_name in wdb.object_store_names: store = wdb[store_name] for rec in store.iterate_records(): print(f" {rec.key!r} -> {repr(rec.value)[:200]}") ``` See [scripts/dump_indexeddb.py](scripts/dump_indexeddb.py). ## Common Chromium Storage Locations | OS | Path | |----|------| | Windows | `%APPDATA%\<App>\Local Storage\leveldb\` | | Windows | `%APPDATA%\<App>\IndexedDB\https_<host>_0.indexeddb.leveldb\` | | macOS | `~/Library/Application Support/<App>/Local Storage/leveldb/` | | Linux | `~/.config/<App>/Local Storage/leveldb/` | For raw browsers, `<App>` is `Google/Chrome/User Data/Default`, `BraveSoftware/Brave-Browser/User Data/Default`, etc. ## Mutation (Advanced) Writing requires either: 1. **Quitting the app** and using a leveldb writer (Node `level` package, or rebuild the dir manually) — or 2. **Patching via the app itself** — many Electron apps expose DevTools. Open with the `--remote-debugging-port=<n>` flag, attach, and call `localStorage.setItem(key, value)`. Survives the app's normal write path so it doesn't corrupt the LSM. For Claude Desktop specifically, see [references/claude-desktop-state.md](references/claude-desktop-state.md) for the discovered key map. ## Decision Framework | You want to | Do | |-------------|-----| | Just see what's there | Copy + ccl_chromium_reader | | Find a specific value | `strings` + grep first; reader if structure matters | | Mutate while app runs | Don't. Use DevTools remote debugging. | | Mutate while app is closed | Quit, then Node `level` package or write back via re-opened leveldb | | Cross-account recovery | Read-only forensics; can't impersonate server-bound entries | ## Anti-patterns - **Opening the live store directly** → silently stale or open errors - **Forgetting to remove LOCK from copy** → reader fails - **Trusting first hit on a key** → leveldb is append-only; iterate all and keep the last - **Using `strings` for structured analysis** → misses keys, conflates duplicates, can't distinguish origins - **Writing while app runs** → LSM corruption, app crash, possible data loss ## Reference - [scripts/dump_localstorage.py](scripts/dump_localstorage.py) — full Local Storage dump - [scripts/dump_indexeddb.py](scripts/dump_indexeddb.py) — full IndexedDB dump - [scripts/extract_keys.py](scripts/extract_keys.py) — targeted key extraction with latest-wins - [references/claude-desktop-state.md](references/claude-desktop-state.md) — Claude Desktop state map (storage keys, sidebar, sessions, account binding) - [references/chromium-format.md](references/chromium-format.md) — leveldb on-disk format, locking, append semantics - ccl_chromium_reader: https://github.com/cclgroupltd/ccl_chrome_indexeddb - LevelDB spec: https://github.com/google/leveldb/blob/main/doc/impl.md
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.