Claude Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_leveldb-ops-3dfaf0b.zip · 11 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/leveldb-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git 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:

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

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.

No comments yet.

Reviews (0)

No reviews yet.

Related