TL;DR
- Moved 12 GB of Claude Code data off a 99%-full C: drive in one afternoon.
- Set up two independent Claude contexts on the same Windows machine: Claude Desktop (Team account, day-job) and Claude CLI (Max account, personal projects) — separate skills, MCPs, memory graphs, session history.
- Discovered three things the docs don't tell you:
- Win Store apps follow NTFS junctions even inside their sandboxed user data folder.
CLAUDE_CONFIG_DIRalso scopes.claude.json(the MCP config) — it lives inside the config dir, not at$HOME.- The "sandboxed bash" you use in Claude Code is a real Linux VM with a 9.4 GB
rootfs.vhdx, plus a 2.3 GB compressed mirror.
- Total Claude Desktop downtime: ~15 minutes (during the VM bundle move). Everything else live.
👉 See also: docs/infra-inventory.md — the cross-session credential ledger pattern that complements this setup.
Two pressures stacked at once:
- Disk space crisis. C: drive at 2.0 GB free. Windows update margin is ~5 GB. One bad apt-equivalent operation away from a wedged system.
- Context bleed between work and personal. My day-job uses a Team-tier Anthropic account; my side projects (a few web platforms — CheckMate marketplace, Moodee, LottoChecker) run on a personal Max-tier account. Same Windows user → same
~/.claude/→ my work sessions kept seeing side-project skills, side-project memory entries, side-project MCPs. Mixing them was a billing-and-context smell.
The architectural goal:
- Free space on C:
- Two scoped Claude environments, each with its own credential, skills folder, MCP config, memory graph, and session history.
- One Windows user, one machine, simultaneous use OK.
The non-negotiable: zero data loss. Full backup before any move.
D:\ClaudeData\
├── .claude.bak-2026-05-15\ ← full backup of ~/.claude\ (23,349 files)
├── .claude-desktop\ ← copy 1: will become Desktop's home
└── .claude-personal\ ← copy 2: will become CLI's home
robocopy /MIR /MT:8 did this in about 6 minutes. 838 MB per copy.
The plan:
~/.claude/ → junction → D:\ClaudeData\.claude-desktop\
The catch: Claude Desktop session was running during the migration. When I mv ~/.claude ~/.claude.OLDREMOVE, the daemon immediately recreated ~/.claude/projects/ to write its session log. The folder kept reappearing.
Solution: do it atomically.
robocopyany freshly-written files from the recreated~/.claudeinto.claude-desktop\firstRemove-Item ~/.claudeimmediatelyNew-Item -ItemType Junction -Path ~/.claude -Target D:\ClaudeData\.claude-desktop
Race window: a few hundred milliseconds. Worked.
# Verify
PS> (Get-Item ~\.claude -Force).LinkType
Junction
PS> (Get-Item ~\.claude -Force).Target
D:\ClaudeData\.claude-desktopClaude Desktop kept running through the swap, didn't notice. Junction is at NTFS layer, transparent to the app.
Insight 1: Win Store apps follow junctions on user data. The Claude package lives in C:\Users\<u>\AppData\Local\Packages\Claude_pzs8sxrjxfjjc\ — a sandboxed user data folder. I expected Win Store integrity checks to detect a junction inside this folder and freak out. They don't. The sandbox is at the filesystem permission layer (per-app data isolation), not at a folder-structure-verification layer. Junctions are followed transparently.
Disk savings: 486 MB on C: (the original ~/.claude\ content size).
Created D:\ClaudeData\claude-personal.cmd:
@echo off
set "CLAUDE_CONFIG_DIR=D:\ClaudeData\.claude-personal"
set "MEMORY_FILE_PATH=D:\ClaudeData\.claude-personal\memory-graph.json"
claude %*Added D:\ClaudeData to user PATH. Now from any folder:
PS> claude-personal
[claude-personal] CLAUDE_CONFIG_DIR=D:\ClaudeData\.claude-personal
[claude-personal] MEMORY_FILE_PATH=D:\ClaudeData\.claude-personal\memory-graph.json
First run failed with:
Claude configuration file not found at: D:\ClaudeData\.claude-personal\.claude.json
Insight 2: CLAUDE_CONFIG_DIR scopes .claude.json too. Claude Code looks for .claude.json (the user-scope MCP config) at $CLAUDE_CONFIG_DIR/.claude.json, NOT at $HOME/.claude.json. The Anthropic docs I could find describe CLAUDE_CONFIG_DIR as redirecting "the config directory" but don't spell out that the MCP config file rides along.
This is good news for separation — copy ~/.claude.json into .claude-personal\.claude.json and the two contexts have fully independent MCP rosters.
(Aside: PowerShell profile auto-load via $PROFILE is broken on this machine because my Documents folder is locale-named in a non-ASCII script and PowerShell mangles the path. Side-stepped by going through cmd via PATH instead.)
du -sh on the Claude Win Store package:
14G Roaming/Claude/
├── vm_bundles/ 12 GB ← the big one
│ └── claudevm.bundle/
│ ├── rootfs.vhdx 9.4 GB
│ ├── rootfs.vhdx.zst 2.3 GB
│ ├── initrd 177 MB
│ ├── initrd.zst 174 MB
│ └── vmlinuz...
├── Claude Extensions/ 830 MB
├── Cache/ 631 MB
├── claude-code/ 217 MB
└── ...
Insight 3: Your "sandboxed bash" in Claude Code is a real Linux VM. Hyper-V or WSL2, depending on your machine. rootfs.vhdx is a 9.4 GB Linux root filesystem. The .zst companion is the compressed download source kept for fast-recovery if the extracted VHDX corrupts. Plus an initrd, a kernel, a small session-data disk.
When Claude Code's tool runs Bash, it mounts this VHDX as a virtual disk and shells in. Every command in every Claude session runs inside this Linux VM — that's why df -h /c returns Linux-style output and shows your Windows drives mounted at /c, /d. The sandbox is fully separate from your Windows process tree.
The migration plan: move the entire vm_bundles/ to D: via junction. Same trick as ~/.claude/. But this one needs Claude Desktop off — Hyper-V keeps rootfs.vhdx locked while a sandboxed command is running.
Full script with -Rollback flag: scripts/move-vm-bundles.ps1. The meaningful bits:
$src = "$env:LOCALAPPDATA\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\vm_bundles"
$dst = "D:\ClaudeData\vm_bundles"
# Pre-flight: Claude.exe must not be in Task Manager
# Pre-flight: VHDX files must not be locked (release after Hyper-V/WSL unmount)
foreach ($file in Get-ChildItem -Path $src -Filter "*.vhdx") {
try {
$stream = [System.IO.File]::Open($file.FullName, 'Open', 'Read', 'None')
$stream.Close()
} catch {
throw "Still locked: $($file.FullName)"
}
}
Move-Item -Path $src -Destination $dst -Force # ~12 minutes for 12 GB
New-Item -ItemType Junction -Path $src -Target $dstThe script also has a -Rollback flag that does the inverse. Wrote it in case something exploded; happily, didn't need it.
After reopening Claude Desktop: sandboxed bash worked instantly. The first tool call mounted rootfs.vhdx from D: instead of C: — invisible to the app.
Disk savings: 11.4 GB on C:. From 2.8 GB free to 14.1 GB free.
Now the personal/work split. Both .claude-desktop\ and .claude-personal\ were identical copies — same skills, same MCPs, same memory. Time to diverge.
Goal: Desktop session (= day-job) should load only work-relevant context. Personal CLI keeps everything.
Pruned from .claude-desktop\:
- Skills: 42 → 19. Removed side-project-specific skills (cohort metrics for one of my marketplaces, brand-vocab linter, multi-lane workflow orchestrators, ...), web platform builders (
web-platform-builder,nextjs-shadcn-v4,supabase-migration, ...), and other tools tied to my personal stacks. - MCPs: 15 → 9. Removed
github(personal repos),gmail(personal email),google-calendar,sentry(personal projects),stripe(personal marketplace),connect-apps(Composio for social profile setup). - Memory graph: Emptied. All 14 entities were side-project-related. Personal CLI's copy retains the full audit trail.
- Session histories: Removed 7 personal project session folders.
What .claude-desktop\ now sees:
Skills (19): work-domain skills (domain analysis · data lookup · etc.)
data-insights-analyst · excel-combine
html-design · frontend-design · professional-pptx
netlify-deploy · netlify-tracker
claude-ai-design-briefing · claude-ai-design-implement
session-close (work-tailored variant)
batch-task · claude-api · algorithmic-art
skill-checkup · skill-update
(plus a handful of domain-specific skills not listed)
MCPs (9): a11y · browserbase · chrome-devtools · cloudflare-docs
memory · ms365 · sequential-thinking · shadcn · time
Personal CLI keeps the full 42 skills + 15 MCPs.
Final touch — context awareness for each side. Added a section at the top of each CLAUDE.md:
## Identity / Context Scope (read first)
**You are running inside Desktop (day-job · Team account)**
- Config dir: ~/.claude/ → junction → D:\ClaudeData\.claude-desktop\
- Account: Anthropic Team
- In-scope: work-domain tasks (domain analysis · reports · presentations · ...)
- Out-of-scope: side-projects (CheckMate · Moodee · LottoChecker) → use CLI Personal
- To switch: open new terminal → claude-personalThe Personal CLAUDE.md has the mirrored block — "you are in CLI Personal · for day-job tasks open Claude Desktop".
This is the guardrail against context confusion. If I open Claude Desktop and ask it to "set up Stripe for [my marketplace]", it now reads its identity block and says "that's out-of-scope; use CLI Personal".
- Test
CLAUDE_CONFIG_DIRbefore depending on it. I assumed.claude.jsonwas at$HOMEbased on default behavior. Firstclaude-personalrun failed with a clear error — fine, fast feedback — but a--dry-runmode that prints all resolved paths would have caught it before I built the wrapper. - The
.zstfiles. I kept them. They're 2.5 GB of compressed source that mirrors the extracted VHDXes. If Claude Desktop ever corrupts the extracted disks, the.zstfiles are the recovery path before re-download. Deletable for another 2.5 GB win if you accept "VM corruption = re-download from network." - PowerShell profile injection. Tried first, abandoned because
$PROFILEpath corruption with non-ASCII folder names. Going through cmd via PATH was the right answer from the start.
A few things I considered and decided against:
- Auto-detecting context from
pwd— "if cwd contains 'CheckMate', use personal config." Fragile. A folder rename or path typo silently switches you to the wrong account. Explicit command names (claudevsclaude-personal) make every invocation auditable. - Overriding
claudeto be smart — same problem, plusPATHorder sensitivity. Other tools (Node-based scripts, CI hooks) that resolveclaudeviawhichcould bypass the wrapper. - Symlinking
.claude.jsonseparately —CLAUDE_CONFIG_DIRscoping handles this for free; no need to fight it.
- Junctions are the right tool when an app has a hardcoded path that points to a wrong volume. Don't bother editing registry, moving installs, or symlinking individual files. Junction the whole folder.
- Multi-context separation = config-dir-scoped + env-var-overridden, not auto-detected. Explicit beats clever for stateful operations.
- A heavy Win Store app is sometimes 95% runtime cache. Investigate before assuming you need to uninstall.
- The Claude Code sandboxed bash is not a clever VM-less trick. It's a Hyper-V/WSL Linux VM. Knowing this changes how you reason about command latency, file system access, and storage requirements.
- An identity-block at the top of CLAUDE.md is the cheapest, most effective guardrail against context confusion. Cheaper than auto-detection, more robust than memory.
This writeup is the Windows + Desktop-coexistence deep dive — junctions, the Win Store sandbox, the hidden Linux VM, and running Claude Desktop (Team) alongside Claude CLI (Max) on one machine.
If your problem is just CLI account-switching, that layer is already solved by dedicated tools:
- claude-code-profiles (38★, MIT) — cross-platform incl. Windows; one
CLAUDE_CONFIG_DIRper profile, no special launch command. - cloak (27★) — isolated config dir per identity, concurrent sessions, directory auto-switch.
…and the patterns behind them are catalogued in yurukusa's multi-account operator field guide (alias / direnv / symlink / SessionStart preflight / billing log).
-
Desktop + CLI on one machine, at the same time. The switchers above are CLI-only. The hard part here was making the Win Store Desktop app and the CLI hold two different accounts at once — junctions through a sandboxed package, the 12 GB VM bundle, zero sign-out churn.
-
The preflight hooks assume API-key mode. The popular
SessionStartaccount-guard decides which account by reading theANTHROPIC_API_KEYprefix. On a Max / Team OAuth subscription there is no API key — the guard has nothing to read. The signal that survives isCLAUDE_CONFIG_DIRitself. -
Architecture over discipline. A separate config dir + a wrapper on PATH (
claude-personal.cmd) + a per-context identity block inCLAUDE.mdremoves the "I forgot to switch" failure mode structurally — cheaper and harder to bypass than a runtime hook, because the two accounts never share a shell to begin with.
- 📘 docs/infra-inventory.md — A cross-session credential ledger pattern. The file-based companion that prevents duplicate PATs / MCPs across parallel Claude Code sessions. 5-minute setup. Pairs well with the multi-context setup in this README.
- Use at your own risk. I had a full backup before every step.
- Win Store integrity behavior is not formally documented. Junctions worked for me on Claude Desktop 1.7196 / Windows 11. Future versions may add tamper detection.
- The
vm_bundlesmove requires Claude Desktop to be fully closed and Hyper-V to release VHDX locks. Race conditions =Move-Itemfailures, not data loss.
scripts/move-vm-bundles.ps1— the 12 GB junction script (with-Rollbackflag)scripts/claude-personal.cmd— the CLI wrapper for the second context
If you've done a similar migration and hit something I missed — especially around Win Store integrity verification or CLAUDE_CONFIG_DIR edge cases — open an issue or a PR. Happy to learn.