Browse Source

docs: add claude-desktop-internals reference

Comprehensive map of Claude Desktop's file system layout, session
storage architecture, and cross-account session continuity.

Key findings documented from live probing (2026-04-26):
- Three session systems: CLI JSONL, Desktop Code-tab, Desktop Cowork
- Cross-account session transfer VALIDATED: copying only the metadata
  JSON into target account's claude-code-sessions/ dir is sufficient
- Sidebar populated via filesystem scan on login (not server cache) —
  confirmed by probing react-query-cache-ls: 7/8 transferred sessions
  were ABSENT from server cache but appeared in sidebar
- Ctrl+R reloads renderer only; logout→login triggers filesystem scan
- Live file creation triggers fs.watch immediately (no restart needed)
- Bulk copy script and account UUIDs for this machine included

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0xDarkMatter 1 month ago
parent
commit
eb1abfa468
1 changed files with 461 additions and 0 deletions
  1. 461 0
      docs/references/claude-desktop-internals.md

+ 461 - 0
docs/references/claude-desktop-internals.md

@@ -0,0 +1,461 @@
+# Claude Desktop Internals — File System, Storage, and Session Architecture
+
+> Probed 2026-04-26 against Claude Desktop v1.3109.0 (Electron 41.2.0) on Windows 11.
+> Discovery method: live file system exploration + ccl_chromium_reader leveldb reads.
+> See `skills/leveldb-ops/` for reusable probe scripts.
+
+---
+
+## Overview
+
+Claude Desktop is an Electron app that embeds `https://claude.ai` as its frontend. It has three tabs — Chat, Cowork, and Code. This document focuses entirely on the **Code tab**, which runs Claude Code sessions.
+
+The app maintains **two parallel storage systems**: a Chromium-backed browser storage (leveldb for Local Storage and IndexedDB), and a structured file hierarchy in `%APPDATA%\Claude\`. The browser storage caches server-fetched data. The file hierarchy stores the durable local state — session metadata, transcripts, worktrees, extensions.
+
+---
+
+## 1. File System Layout
+
+All app data lives under `%APPDATA%\Claude\` on Windows:
+
+```
+%APPDATA%\Claude\
+  Local Storage/leveldb/          ← Chromium Local Storage (browser-side, leveldb)
+  IndexedDB/                      ← Chromium IndexedDB (browser-side, leveldb)
+  Session Storage/                ← Chromium Session Storage (per-tab, ephemeral)
+  Partitions/                     ← Per-origin sandboxed storage for MCP iframes
+
+  claude-code-sessions/           ← Session metadata registry (one JSON per session)
+    <account-uuid>/
+      <workspace-uuid>/
+        local_<session-uuid>.json
+
+  local-agent-mode-sessions/      ← Session transcripts (JSONL conversation logs)
+    <account-uuid>/
+      <workspace-uuid>/
+        local_<session-uuid>/
+          audit.jsonl
+          .claude/projects/
+            -sessions-<human-name>/
+              <cli-session-uuid>.jsonl
+              subagents/
+                agent-<id>.jsonl
+                agent-acompact-<id>.jsonl
+
+  claude-code-vm/                 ← VM/sandbox state for remote sessions
+  agents/                         ← Installed agent extensions
+  Claude Extensions/              ← Installed connector extensions (MCP wrappers)
+  Claude Extensions Settings/     ← Per-extension config
+  git-worktrees.json              ← Registry of active git worktrees (all accounts)
+  config.json                     ← UI preferences (theme, scale, locale, allowlists)
+  buddy-tokens.json               ← Daily token usage counter
+  claude_desktop_config.json      ← MCP server declarations
+  window-state.json               ← Last window position/size
+  Local State                     ← Chromium DPAPI key store (for cookie encryption)
+  logs/                           ← Diagnostic logs
+  Crashpad/                       ← Crash reports
+```
+
+**Example account UUIDs:**
+
+| UUID prefix | Account | Email | Session type | Sessions |
+|-------------|---------|-------|--------------|----------|
+| `aabbccdd` | account-a | user@example.com | Local | 10 |
+| `11223344` | account-b | user@company.com | Local | 22 |
+| `55667788` | account-c | user@gmail.com | **Remote (cloud VM)** | 40 |
+
+Account emails are stored in `local-agent-mode-sessions/<account-uuid>/<workspace-uuid>/local_<session-uuid>.json` → `emailAddress` field.
+
+Account UUIDs are assigned by Anthropic's server. To identify them: read `~/.claude/.claude.json` → `oauthAccount.accountUuid` for the currently logged-in CLI account.
+
+---
+
+## 2. Session ID Schemes — Two Separate Namespaces
+
+Claude Code has **two distinct session systems** that share the brand name but are architecturally different.
+
+| Property | Desktop Code-tab sessions | CLI terminal sessions |
+|----------|--------------------------|----------------------|
+| ID format | `local_<uuid>` | `<uuid>` (no prefix) |
+| Where shown | Desktop sidebar | `claude --resume` picker |
+| Storage | `%APPDATA%\Claude\claude-code-sessions\` + server cache | `~/.claude/projects/<encoded-cwd>/*.jsonl` |
+| Account-bound? | Metadata is account-scoped (stored in account subfolder) | No — pure local files |
+| Server-dependent? | Sidebar list fetched from server; transcript stored locally | Fully local |
+| Resume flag | No native CLI flag | `claude --resume <uuid>` |
+
+**Critical:** these IDs don't overlap. A `local_<uuid>` session never appears in the CLI picker and vice versa.
+
+---
+
+## 3. `claude-code-sessions/` — Session Metadata Registry
+
+Each Desktop session has one JSON file at:
+
+```
+%APPDATA%\Claude\claude-code-sessions\<account-uuid>\<workspace-uuid>\local_<session-uuid>.json
+```
+
+### Full Schema
+
+```json
+{
+  "sessionId":        "local_<session-uuid>",
+  "cliSessionId":     "<cli-session-uuid>",
+  "cwd":              "C:\\Projects\\my-project",
+  "originCwd":        "C:\\Projects\\my-project",
+  "createdAt":        1777168830743,
+  "lastActivityAt":   1777171587818,
+  "model":            "claude-opus-4-7[1m]",
+  "effort":           "medium",
+  "isArchived":       false,
+  "title":            "dev",
+  "titleSource":      "user",
+  "permissionMode":   "acceptEdits",
+  "completedTurns":   5,
+  "enabledMcpTools":  { "<tool-hash>": true, ... },
+  "remoteMcpServersConfig": [...],
+  "alwaysAllowedReasons": [...]
+}
+```
+
+### Key Fields
+
+| Field | Purpose |
+|-------|---------|
+| `sessionId` | Desktop session ID — matches sidebar, worktree registry, leveldb references |
+| `cliSessionId` | UUID pointing to the JSONL transcript in `local-agent-mode-sessions/`. This is a plain UUID (no `local_` prefix) — same format as CLI session IDs |
+| `cwd` | Working directory when session was created |
+| `titleSource` | `"user"` (manually renamed) or `"auto"` (generated) |
+| `permissionMode` | `"default"`, `"acceptEdits"`, `"plan"`, `"auto"`, `"bypassPermissions"` |
+| `isArchived` | Set to `true` when session is archived (hidden from sidebar by default) |
+| `completedTurns` | Number of user→assistant exchange pairs |
+| `enabledMcpTools` | Map of `<tool-hash>: true` for each enabled MCP tool this session |
+
+The `workspace-uuid` in the path is a Desktop-internal grouping UUID — not user-visible, not the session ID.
+
+---
+
+## 4. `local-agent-mode-sessions/` — Session Transcripts
+
+The full conversation transcript for each Desktop session lives at:
+
+```
+%APPDATA%\Claude\local-agent-mode-sessions\
+  <account-uuid>\<workspace-uuid>\local_<session-uuid>\
+    audit.jsonl
+    .claude\projects\-sessions-<human-name>\
+      <cliSessionId>.jsonl          ← main transcript
+      subagents\
+        agent-<id>.jsonl            ← subagent transcripts
+        agent-acompact-<id>.jsonl   ← compacted subagent transcripts
+```
+
+**The `<cliSessionId>` in the filename matches `cliSessionId` in the metadata JSON.** This is the bridge between the two storage locations.
+
+### JSONL Record Types
+
+The transcript JSONL contains multiple record types, identical in shape to CLI session JSONL files:
+
+```
+type="queue-operation"   keys: type, operation, timestamp, sessionId, content
+type="user"              keys: parentUuid, isSidechain, userType, cwd, sessionId,
+                               version, gitBranch, type, message, uuid, timestamp,
+                               permissionMode
+type="assistant"         keys: parentUuid, isSidechain, userType, cwd, sessionId,
+                               version, gitBranch, message, requestId, type, uuid,
+                               timestamp
+```
+
+The `user` and `assistant` records are structurally identical to CLI JSONL records — the transcript format is shared.
+
+### `audit.jsonl`
+
+Separate from the main transcript. Records operational events (session start, tool calls, errors) at a lower level. Not needed for conversation resume.
+
+---
+
+## 5. `git-worktrees.json` — Global Worktree Registry
+
+Single file at `%APPDATA%\Claude\git-worktrees.json`. Tracks all active worktrees across all sessions and accounts.
+
+```json
+{
+  "worktrees": {
+    "<worktree-name>": {
+      "name":         "<worktree-name>",
+      "path":         "C:\\Projects\\my-project\\.claude\\worktrees\\<worktree-name>",
+      "leasedBy":     "local_<session-uuid>",
+      "baseRepo":     "C:\\Projects\\my-project",
+      "branch":       "claude/<worktree-name>",
+      "sourceBranch": "main",
+      "createdAt":    1777163079076
+    }
+  }
+}
+```
+
+`leasedBy` contains the Desktop `local_<uuid>` session ID that owns the worktree.
+
+---
+
+## 6. Chromium Local Storage — LevelDB Schema
+
+Location: `%APPDATA%\Claude\Local Storage\leveldb\`  
+Origin: `https://claude.ai`  
+Format: Chromium-encoded LevelDB. Requires `ccl_chromium_reader` to read (not `plyvel` — no Windows wheels).
+
+**Safety rule:** Always copy the dir and delete the `LOCK` file before reading. Never open the live store.
+
+### Key Entries (origin: `https://claude.ai`)
+
+| Key | Survives logout? | Shape | Purpose |
+|-----|-----------------|-------|---------|
+| `react-query-cache-ls` | No — cleared on logout | JSON, 10–20MB | Server query cache: conversation list, account info, model availability |
+| `react-query-cache` (IndexedDB mirror) | No | Same content | Duplicate of above in IndexedDB `keyval-store` |
+| `dframe-store` | Yes | `{state: {pinnedOrder, sidebarWidth, lastKnownMode, groupByByMode}}` | Sidebar layout — pin order, width, grouping mode |
+| `ccd-session-store` | Yes | `{state: {selectedFolder}}` | Active project folder |
+| `epitaxy.sidePaneStore.v1` | Yes | `{state: {tileLayoutBySession: {local_<uuid>: layout}}}` | Per-session pane layouts (keyed by session ID) |
+| `epitaxy-unread-v1` | Yes | `{state: {unreadIds: []}}` | Unread session markers |
+| `__qk_hint_account_uuid` | Yes (multi-account) | string | Last-active account UUID — multiple records may exist across accounts |
+| `lastLoginMethod` | Yes | string | e.g. `"google"` |
+| `default-model` | Yes | string | e.g. `"claude-opus-4-7"` |
+| `branch-status-cache` | Yes | JSON | Cached git branch status per repo |
+
+**Append-only gotcha:** LevelDB appends new writes; old values persist until compaction. Always iterate all records and keep the last value per key — never trust the first hit.
+
+**Mutation:** Quit Claude Desktop → edit via Node `level` package or direct leveldb write → restart. Or use `--remote-debugging-port=<n>` for live DevTools access.
+
+---
+
+## 7. MCP Iframe Partitioning
+
+MCP tool widgets run in iframes scoped to per-session origins:
+
+```
+https://<hash>.claudemcpcontent.com/^0https://claude.ai
+```
+
+Each MCP iframe has its own isolated Local Storage (Chromium's `^0` storage partition suffix). State persists across sessions for that widget — e.g. Asana task drafts cache per-widget. These are stored in `%APPDATA%\Claude\Partitions\`.
+
+---
+
+## 8. Local vs Remote Session Execution
+
+Desktop sessions run in one of three environments (set at session creation):
+
+| Environment | `cwd` pattern | Transcript stored locally? |
+|-------------|--------------|---------------------------|
+| **Local** | `C:\...` / `X:\...` (Windows path) | Yes — JSONL in `local-agent-mode-sessions/<account>/<ws>/local_<id>/.claude/projects/` |
+| **Remote** (Anthropic cloud VM) | `/sessions/<name>/mnt` | No — server-side only |
+| **SSH** | `/<remote-path>` | No — server-side only |
+
+**Local sessions** write a full JSONL transcript to disk as the session runs. These transcripts are account-agnostic bytes — any account can read and replay them.
+
+**Remote sessions** (Anthropic-hosted, selected via the "Remote" environment option) run in an ephemeral cloud VM. The conversation transcript is stored server-side under the creating account. The local `local-agent-mode-sessions` directory for remote sessions contains only workspace environment files (`.claude/.claude.json`, debug logs) — no conversation JSONL.
+
+This distinction determines what cross-account session transfer is possible:
+- Local → any account: transcript file copy + leveldb pin is feasible
+- Remote → different account: server-gated, no local transcript to copy
+
+**Observed pattern:**
+- Accounts running **local** sessions have cwd like `C:\Projects\...` (a Windows path)
+- Accounts running **remote** sessions have cwd like `/sessions/<vm-name>/mnt`
+
+## 8b. Account Binding — What Survives Account Switching
+
+| Storage | Account-scoped? | Survives logout? | Notes |
+|---------|----------------|-----------------|-------|
+| `~/.claude/projects/*.jsonl` | No | Yes | CLI transcripts — pure local, zero account markers |
+| `~/.claude/.claude.json` | Yes | Replaced on login | CLI auth state |
+| `claude-code-sessions/<account-uuid>/` | Yes (by path) | Yes | Metadata files remain on disk, accessible by path |
+| `local-agent-mode-sessions/<account-uuid>/` | Yes (by path) | Yes | Transcript JSONLs remain on disk |
+| `react-query-cache-ls` | Yes | No — cleared | Server-fetched sidebar list |
+| `dframe-store.pinnedOrder` | No | Yes | Pin order persists; references server IDs |
+| `epitaxy.sidePaneStore.v1` | No | Yes | Tile layouts persist; reference local session IDs |
+| `git-worktrees.json` | No | Yes | Worktree registry persists |
+
+**Key insight:** When you switch accounts, the Desktop sidebar repopulates from the new account's server-fetched `react-query-cache-ls`. Prior sessions are not gone — their metadata and transcripts remain on disk under the old account's UUID subdirectory.
+
+---
+
+## 9. Cross-Account Session Continuity (Local Sessions Only)
+
+### What's possible
+
+**CLI resume of a Desktop session (account-agnostic):**
+
+Any Desktop session can be resumed in the CLI terminal from any account, because the transcript JSONL is a local file:
+
+1. Find the session in `claude-code-sessions/<account-uuid>/*/local_<session-uuid>.json`
+2. Extract `cliSessionId` from that JSON
+3. Find the JSONL at `local-agent-mode-sessions/<account-uuid>/*/local_<session-uuid>/.claude/projects/-sessions-*/< cliSessionId>.jsonl`
+4. Copy the JSONL to `~/.claude/projects/<encoded-cwd>/<cliSessionId>.jsonl`
+5. Run `claude --resume <cliSessionId>` — works regardless of logged-in account
+
+**Desktop-to-Desktop cross-account (VALIDATED — 2026-04-26):**
+
+Confirmed working: copying only the session metadata JSON from account A's `claude-code-sessions/` dir into account B's `claude-code-sessions/` dir is sufficient for the session to appear in account B's sidebar and accept new messages.
+
+**How Desktop resolves the transcript:** Desktop reads `cliSessionId` from the metadata JSON, then loads the transcript from `~/.claude/projects/<encoded-cwd>/<cliSessionId>.jsonl` — which is account-agnostic. It does NOT validate session ownership against the server on click. No server gating observed.
+
+**Minimal procedure (battle-tested):**
+
+```bash
+# 1. Find sessions to transfer
+A_SESSIONS="$APPDATA/Claude/claude-code-sessions/<A-uuid>/"
+B_WORKSPACE="$APPDATA/Claude/claude-code-sessions/<B-uuid>/<any-existing-workspace-uuid>/"
+
+# 2. For each session, verify the JSONL bridge exists
+cliSessionId=$(jq -r '.cliSessionId' session.json)
+ls ~/.claude/projects/<encoded-cwd>/$cliSessionId.jsonl  # must exist
+
+# 3. Copy just the metadata JSON
+cp "$A_SESSIONS/<ws>/local_<uuid>.json" "$B_WORKSPACE/local_<uuid>.json"
+
+# 4. Sessions appear in account B's sidebar immediately (no Desktop restart needed)
+#    Desktop scans the filesystem for sessions — does NOT rely on dframe-store.pinnedOrder
+```
+
+**What is NOT needed:**
+- Copying `local-agent-mode-sessions/` transcript directories
+- Modifying `dframe-store.pinnedOrder` in leveldb
+
+**Sidebar refresh behaviour (important):**
+- `Ctrl+R` — reloads the renderer from cached state only. Does NOT retrigger filesystem scan. Copied sessions will NOT appear.
+- **Logout → Login** — triggers fresh `react-query-cache-ls` server fetch AND full filesystem rescan. Copied sessions WILL appear. This is the required step.
+- Live file creation while Desktop is open — fs.watch fires immediately, sessions appear without restart.
+
+**Sidebar discovery — how it actually works (validated via leveldb probe):**
+
+The sidebar is populated from two sources, unioned together:
+1. **Server session list** — fetched from Anthropic's server on login, cached in `react-query-cache-ls` (20MB blob). Contains only sessions the server knows about for the current account.
+2. **Filesystem scan** — on login, Desktop scans `claude-code-sessions/<current-account-uuid>/` and loads all `local_*.json` files found. These appear in the sidebar regardless of whether the server knows about them.
+
+Transferred sessions (copied from another account's dir) appear via path #2 — the filesystem scan. Confirmed: 7 of 8 transferred sessions were ABSENT from `react-query-cache-ls` but appeared in the sidebar. The 8th appeared in the cache only as a previously-cached transcript (`epitaxy.local.transcript.*` key), not as a session list entry.
+
+`dframe-store.pinnedOrder` only controls pinned-to-top ordering — all other sessions are shown below, sorted by `lastActivityAt`. Confirmed: `pinnedOrder` had 2 entries while 40+ sessions appeared in the sidebar.
+
+**Validated transfers (2026-04-26):**
+- account-a → account-b: single session (29 turns, 1.8MB JSONL) — appeared, opened, accepted new messages ✓
+- account-a → account-b: 8× sessions from a single repo (0–132 turns) — bulk copied ✓
+
+**Bulk copy script:**
+
+```bash
+EVO="$APPDATA/Claude/claude-code-sessions/<A-uuid>"
+TARGET="$APPDATA/Claude/claude-code-sessions/<B-uuid>/<workspace-uuid>"
+JSONL_DIR="$HOME/.claude/projects/<encoded-cwd>"
+
+for ws in "$EVO"/*/; do
+  for f in "$ws"*.json; do
+    [ -f "$f" ] || continue
+    cwd=$(jq -r '.cwd // ""' "$f")
+    [ "$cwd" = "C:\\Projects\\TargetProject" ] || continue
+    cli=$(jq -r '.cliSessionId' "$f")
+    [ -f "$JSONL_DIR/$cli.jsonl" ] || { echo "SKIP (no JSONL): $f"; continue; }
+    cp "$f" "$TARGET/$(basename "$f")"
+    echo "OK: $(jq -r '.title' "$f") → $(basename "$f")"
+  done
+done
+```
+
+### What's not possible
+
+- Accessing account A session **content** through the server while logged into account B — server-gated
+- Re-registering Desktop sidebar entries server-side from a different account — server-gated
+- Resuming a Desktop session via `claude --resume` using the `local_<uuid>` ID — wrong format; CLI only accepts plain UUIDs
+- Transferring **remote** sessions (cwd: `/sessions/<vm-name>/mnt`) — no local JSONL exists to bridge
+
+---
+
+## 10. Probe Recipes
+
+### List all Desktop sessions across all accounts
+
+```python
+import json, pathlib, sys
+
+base = pathlib.Path(r'C:/Users/<user>/AppData/Roaming/Claude/claude-code-sessions')
+sessions = []
+for account_dir in sorted(base.iterdir()):
+    if not account_dir.is_dir(): continue
+    for ws in account_dir.iterdir():
+        if not ws.is_dir(): continue
+        for f in ws.glob('local_*.json'):
+            try:
+                d = json.loads(f.read_text(encoding='utf-8'))
+                sessions.append({
+                    'account': account_dir.name[:8],
+                    'sessionId': d['sessionId'],
+                    'cliSessionId': d.get('cliSessionId', ''),
+                    'title': d.get('title', '?'),
+                    'cwd': d.get('cwd', ''),
+                    'isArchived': d.get('isArchived', False),
+                    'lastActivity': d.get('lastActivityAt', 0),
+                    '_path': str(f)
+                })
+            except: pass
+
+sessions.sort(key=lambda x: x['lastActivity'], reverse=True)
+for s in sessions:
+    print(f"[{s['account']}] {s['title']!r:<20} cli:{s['cliSessionId'][:8]} {s['cwd'][-40:]}")
+```
+
+### Find JSONL for a Desktop session
+
+```python
+def find_transcript(appdata, account_uuid, desktop_session_uuid, cli_session_id):
+    base = pathlib.Path(appdata) / 'Claude' / 'local-agent-mode-sessions'
+    pattern = f'**/local_{desktop_session_uuid}/**/{cli_session_id}.jsonl'
+    results = list((base / account_uuid).rglob(f'{cli_session_id}.jsonl'))
+    return results[0] if results else None
+```
+
+### Read leveldb Local Storage
+
+```bash
+# Copy live store (LOCK copy will warn — fine)
+cp -r "$APPDATA/Claude/Local Storage/leveldb" /tmp/ls-probe
+rm -f /tmp/ls-probe/LOCK
+
+# Install reader (GitHub only, not on PyPI)
+uv pip install "git+https://github.com/cclgroupltd/ccl_chrome_indexeddb.git"
+
+# Dump all keys
+python skills/leveldb-ops/scripts/dump_localstorage.py /tmp/ls-probe --origin https://claude.ai
+
+# Extract specific keys
+python skills/leveldb-ops/scripts/extract_keys.py /tmp/ls-probe dframe-store ccd-session-store
+```
+
+---
+
+## 11. Feature Flags (GrowthBook / Tengu Flags)
+
+Stored in `~/.claude/.claude.json` → `cachedGrowthBookFeatures`. The app uses these to gate experimental features. Prefix is `tengu_`. Some notable ones observed:
+
+| Flag | Likely purpose |
+|------|---------------|
+| `tengu_worktree_mode` | Git worktree session isolation |
+| `tengu_ccr_bridge_multi_session` | Multi-session bridge mode |
+| `tengu_session_memory` | Session memory features |
+| `tengu_auto_mode_config` | Auto permission mode |
+| `tengu_kairos_loop_dynamic` | Dynamic loop pacing |
+| `tengu_sm_compact` | Smart compaction |
+| `tengu_harbor` | Sidebar/session management features |
+| `tengu_relay_chain_v1` | Agent relay chaining |
+| `tengu_mcp_elicitation` | MCP tool elicitation |
+
+These are read-only observations — modifying them locally has unknown effects.
+
+---
+
+## 12. Related Files
+
+| File | Path | Contents |
+|------|------|----------|
+| `claude-desktop-state.md` | `skills/leveldb-ops/references/` | Full Local Storage key map for `https://claude.ai` origin |
+| `chromium-format.md` | `skills/leveldb-ops/references/` | LevelDB on-disk format, locking, append semantics |
+| `dump_localstorage.py` | `skills/leveldb-ops/scripts/` | Full Local Storage dump |
+| `extract_keys.py` | `skills/leveldb-ops/scripts/` | Targeted key extraction |
+| `dump_indexeddb.py` | `skills/leveldb-ops/scripts/` | IndexedDB dump |