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