claude-desktop-internals.md 22 KB

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>.jsonemailAddress field.

Account UUIDs are assigned by Anthropic's server. To identify them: read ~/.claude/.claude.jsonoauthAccount.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

{
  "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.

{
  "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):

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

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

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

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

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