Browse Source

feat(skills): add leveldb-ops — Chromium/Electron storage probing (v2.4.5)

Read and decode LevelDB stores used by every Electron app's Local
Storage, IndexedDB, and Session Storage. Pure-Python via
ccl_chromium_reader (GitHub-only); plyvel skipped — no Windows wheels.

- SKILL.md: setup, safety protocol (copy + remove LOCK), append-only
  gotcha, mutation paths, decision framework, anti-patterns
- scripts/: dump_localstorage.py, dump_indexeddb.py, extract_keys.py
- references/chromium-format.md: on-disk layout, locking, append
  semantics, when to reach for a different tool
- references/claude-desktop-state.md: probed state map for Claude
  Desktop v1.3109.0 — account-binding distinctions, key Local Storage
  entries, IndexedDB contents, MCP origin partitioning

Plugin bumped 2.4.4 → 2.4.5; README count 69 → 70 skills.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0xDarkMatter 1 month ago
parent
commit
032bfd0ff1

+ 3 - 2
.claude-plugin/plugin.json

@@ -1,7 +1,7 @@
 {
   "name": "claude-mods",
-  "version": "2.4.4",
-  "description": "Custom commands, skills, and agents for Claude Code - session continuity, 23 expert agents, 69 skills, 3 commands, 6 rules, 4 hooks, 13 output styles, modern CLI tools",
+  "version": "2.4.5",
+  "description": "Custom commands, skills, and agents for Claude Code - session continuity, 23 expert agents, 70 skills, 3 commands, 6 rules, 4 hooks, 13 output styles, modern CLI tools",
   "author": "0xDarkMatter",
   "repository": "https://github.com/0xDarkMatter/claude-mods",
   "license": "MIT",
@@ -75,6 +75,7 @@
       "skills/iterate",
       "skills/javascript-ops",
       "skills/laravel-ops",
+      "skills/leveldb-ops",
       "skills/log-ops",
       "skills/markitdown",
       "skills/mcp-ops",

File diff suppressed because it is too large
+ 4 - 1
README.md


+ 151 - 0
skills/leveldb-ops/SKILL.md

@@ -0,0 +1,151 @@
+---
+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

+ 89 - 0
skills/leveldb-ops/references/chromium-format.md

@@ -0,0 +1,89 @@
+# 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

+ 123 - 0
skills/leveldb-ops/references/claude-desktop-state.md

@@ -0,0 +1,123 @@
+# 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
+```

+ 57 - 0
skills/leveldb-ops/scripts/dump_indexeddb.py

@@ -0,0 +1,57 @@
+#!/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())

+ 58 - 0
skills/leveldb-ops/scripts/dump_localstorage.py

@@ -0,0 +1,58 @@
+#!/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())

+ 49 - 0
skills/leveldb-ops/scripts/extract_keys.py

@@ -0,0 +1,49 @@
+#!/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())