---
name: windows-environment-quirks
description: When placing files, shortcuts, or pinned icons on the user's Windows 11 laptop "Connie" (or any Windows box with OneDrive/Copilot folder redirection), do NOT hardcode standard shell paths — the Desktop, Documents, etc. may be redirected somewhere unexpected. Resolve the real path with [Environment]::GetFolderPath first. Also covers pinning a CLI/terminal tool to the taskbar (a CLI isn't directly pinnable — wrap it in a .lnk that opens PowerShell with the command), and the Claude product-surface taxonomy (which Claude apps write local session files the VPS agent can read vs which are cloud-only). Trigger whenever a shortcut/file "isn't on the desktop" after a save, when asked to "put X on my taskbar", or when the user asks whether the agent can see a Claude session.
---

# Windows environment quirks — redirected folders, taskbar pinning, Claude surfaces

Rob's laptop "Connie" (Windows 11) has non-standard folder redirection. Standard assumptions about where the Desktop / Documents live break silently: a file saves without error but the user can't see it. This skill is the "don't guess the path" discipline plus the recurring pin-a-CLI pattern and the Claude-surface map.

## Rule 1 — never hardcode `$env:USERPROFILE\Desktop` (or Documents) — resolve it

Connie's Desktop was hijacked into OneDrive by a Copilot "remember my preferences" flow. `[Environment]::GetFolderPath('Desktop')` returned:

```
C:\Users\Rob\OneDrive\Microsoft Copilot Chat Files\Desktop
```

NOT `C:\Users\Rob\Desktop` and NOT `C:\Users\Rob\OneDrive\Desktop`. A `.lnk` saved to the guessed local path "succeeded" but was invisible on the real desktop. Cost a full round-trip of "it's not there."

**The pattern — resolve before you write:**

```powershell
$desktop = [Environment]::GetFolderPath('Desktop')
# then build the target path from $desktop, never from a literal
```

Same applies to `Documents`, `Pictures`, etc. — use `[Environment]::GetFolderPath('Documents')`, not `$env:USERPROFILE\Documents`.

**Confirm the visible desktop, not just the folder:** after saving, the file may still not render because (a) the desktop needs F5, (b) it sorted off-view, or (c) OneDrive sync lag puts a cloud overlay on it. Quickest confirmation that the file is truly on the user's real desktop: `Get-ChildItem ([Environment]::GetFolderPath('Desktop'))\*.lnk | Select Name` — if it's listed there, it IS on their desktop even if they can't spot the icon yet.

**Flag for the user (don't silently accept):** a Desktop redirected into a `"Microsoft Copilot Chat Files"` folder is a Copilot/OneDrive known-folder-move stunt. It means desktop files sync through OneDrive and can hit sync quirks. Mention it once as a "worth unwinding later" note — don't revert it mid-task (reverting changes the path again and orphans anything you just placed there; place shortcuts AFTER any revert, or re-place them).

## Rule 2 — a CLI tool is not directly pinnable to the taskbar; wrap it in a .lnk

Claude Code, `syncthing`, `npm`, etc. are commands that run *inside* a terminal, not standalone apps — there's no exe icon to pin. To give the user a one-click taskbar icon that opens a terminal already running the tool:

```powershell
$desktop = [Environment]::GetFolderPath('Desktop')
$ws = New-Object -ComObject WScript.Shell
$sc = $ws.CreateShortcut("$desktop\Claude Code.lnk")
$sc.TargetPath = "powershell.exe"
$sc.Arguments  = "-NoExit -Command claude"     # -NoExit keeps the window open on the tool
$sc.WorkingDirectory = "$env:USERPROFILE"
$sc.Description = "Claude Code terminal"
$sc.Save()
```

Then the user: right-click the new desktop shortcut → **show more options** → **pin to taskbar** (Win11 hides pin under "show more options"). The taskbar pin survives deleting the desktop copy.

Gotcha: the shortcut opens in `$WorkingDirectory`. For a context-reading tool like Claude Code, tell the user to `cd` into a project folder first thing for project work; for pure chat it doesn't matter.

## Rule 3 — Claude product surfaces: which ones the VPS agent can actually see

Rob conflates three DIFFERENT Claude products; whether the agent can read a session depends entirely on which one produced it. State this table when asked "can you see my Claude session?":

| Surface | What it is | Session files on the local disk? | Agent-visible? |
|---|---|---|---|
| **Claude Code** (terminal `claude`) | CLI agent | ✅ `C:\Users\Rob\.claude\projects\<cwd-slug>\<session-id>.jsonl` | ✅ yes — via the `claude-code-sessions` Syncthing mirror to `/root/.hermes/mirrors/claude-code-sessions` (VPS receive-only) |
| **Claude Desktop app** | Electron/MSIX wrapper around the web chat | ❌ chats live on Anthropic's servers | ❌ no — BUT the user's real Desktop work (Projects) can still be bridged via the shared vault: `Hermes Activity/` + `Claude Activity/` folders synced by Syncthing (see `syncthing-folder-sync` → "Hermes ↔ Claude Desktop bridge"), plus a remote-MCP live channel (see `operating-hermes-gateway` → "Exposing Hermes as a remote MCP server") |
| **claude.ai in browser** | website | ❌ same servers | ❌ no |

Rule of thumb for the user: **terminal (Claude Code) = captured automatically; Desktop app / website = cloud-only, manual copy-paste.** If the user wants ideas captured, do the brainstorming in Claude Code (same Pro subscription, same models) rather than the Desktop app — then it lands in the mirror with no extra step.

Related: the `.jsonl` transcripts are line-delimited JSON; read with `python3 -c "import json; [json.loads(l) for l in open(f)]"`. Early lines are metadata (`last-prompt`, `mode`, `permission-mode`), not messages.

## Rule 4 — NEVER hand the user raw JSON at a PowerShell prompt

PowerShell is a shell, not a text editor: paste a `{ "key": ... }` block at `PS C:\>` and it executes as code — `ParserError: Unexpected token ':' in expression or statement`. Worse, PowerShell's continuation prompt (`>>`) makes it LOOK like multi-line input is being accepted, so the user types the whole config, hits Enter, and gets a wall of red. (Hit 2026-08-06 handing Rob a `claude_desktop_config.json` payload with no delivery vehicle; his Claude Code context-warning system even parsed a truncated JSON string as a file reference and warned "file not found" — the misfire confused both sides.)

Every JSON payload for a Windows user needs one of these vehicles, no exceptions:

1. **Config file that may not exist yet / must create-if-absent** (one-liner, safe to paste):
   ```powershell
   $p = "$env:APPDATA\Claude\claude_desktop_config.json"; if (Test-Path $p) { notepad $p } else { New-Item -ItemType Directory -Force (Split-Path $p) | Out-Null; @'
   { ...json here, single-quoted here-string means \\ stays literal... }
   '@ | Set-Content $p -Encoding UTF8; "written: $p" }
   ```
2. **Merge into an existing config** → the `ConvertFrom-Json` / `Add-Member` / `ConvertTo-Json -Depth 10` pattern (see operating-hermes-gateway → "Editing a JSON config file safely in PowerShell"). Never text find-and-replace.
3. **Hand-edit** → `notepad $p` first, THEN the JSON in a clearly-labeled "paste this into Notepad" block — never as a bare code block with no stated destination.

Companion probes for this class of task: `node --version; npx --version` (is the runner installed?) and `Get-Content $p -Raw | ConvertFrom-Json | Out-Null; "JSON OK"` (validate before telling the user to restart the app). And per-user rule: **Claude Desktop config changes need a full system-tray Quit, not closing the window** — the app keeps running in the tray and never re-reads the file.

## Rule 5 — Claude Desktop: WHICH config file is real, and when JSON config is dead entirely

- The documented path `%APPDATA%\Claude\claude_desktop_config.json` is only real for the **non-Store install**. Identify the build BEFORE touching any config: `Get-Process claude | Select Path`. If the path is `C:\Program Files\WindowsApps\Claude_*` → **MSIX/Store package**, whose real config is `%LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude_desktop_config.json` — the `%APPDATA%` file is silently ignored (verified 2026-08-06 after three rounds of "JSON VALID, server never loads").
- **Extension-based MCP servers bypass the JSON entirely.** Apify appeared in Settings → Developer with `Using built-in Node.js` in the logs while absent from the JSON — it was installed via the UI and lives in `extensions-installations.json`. So: a server in the UI ≠ a server in the JSON, and a server in the JSON ≠ a server this build will load.
- **Some MSIX builds cannot spawn local stdio MCP servers at all.** On build 1.25927, Settings → Connectors → "Add custom connector" offers ONLY "Remote MCP server URL" + OAuth fields — no command/args field. If that's the dialog, the `npx` filesystem-server route is dead on that build; the path forward is a remote MCP endpoint (see `operating-hermes-gateway` → "Exposing Hermes as a remote MCP server").
- **Log-path diagnosis before more config surgery** (all under the sandbox `...\LocalCache\Roaming\Claude\`):
  - No `logs\mcp-server-<name>.log` for your server → Desktop never attempted to spawn it (config ignored/filtered).
  - `logs\mcp.log` shows only extension servers initializing → the JSON isn't being read at all.
  - `Select-String -Path logs\main.log -Pattern '<servername>'` empty → total invisibility confirmed.
- A server missing from **Settings → Developer** with a valid config file = failed to launch → read the error text on that screen; check both **Settings → Developer** (status) and **Settings → Connectors** (per-chat toggle).
- MCP servers DO NOT hot-reload: full tray Quit + relaunch after every config change.
- If Claude answers a "what's in X folder" question by searching Drive/Gmail/etc. instead of the filesystem, the filesystem server didn't load or isn't enabled in that chat — its connected-apps list is the tell. Rephrase the test as "Using the vault filesystem tools, list the files in..." to force tool routing.

## Pitfalls

- **A clean `.Save()` with no error does NOT mean the user can see the shortcut.** Verify the target dir is the *resolved* shell folder, and have the user F5 / sort-by-name before concluding it's missing.
- **OneDrive Known Folder Move can make `[Environment]::GetFolderPath` return a path that itself doesn't exist yet** (e.g. `OneDrive\Desktop` when KFM is half-configured) — `DirectoryNotFoundException` on `.Save()`. When that happens, re-query and check `Test-Path` on the parent; fall back to creating the dir or asking the user where their desktop actually shows files.
- **`Register-ScheduledTask` needs an elevated (ADMIN) PowerShell** — `Access is denied` from a normal window. Tell the user to open "Terminal (Admin)" / "Run as administrator" FIRST, then re-run the whole block (admin windows start in `C:\Windows\System32`; `$env:` user vars still resolve to the elevating user's profile).
- **Don't batch a revert of folder redirection together with placing new shortcuts.** Revert changes the shell path; anything saved to the old redirected path stays behind. Sequence: revert → confirm new path → then place.
- **The Desktop folder can be entirely ABSENT, not just redirected — and this hits the "write output to a file for the user" pattern.** On the Surface tablet (user `rkbla`, 2026-07-19), `Set-Content "$env:USERPROFILE\Desktop\file.txt"` threw `DirectoryNotFoundException` because the profile has no Desktop folder at all. So the discipline is two-step: (1) resolve with `[Environment]::GetFolderPath('Desktop')`, (2) `Test-Path` the result before writing; if missing, create the dir deliberately or pick a visible fallback (the Syncthing vault works). This matters most for the Surface workflow where copy-from-terminal is unreliable and a written file IS the output channel — a write that dies on a missing Desktop silently loses the result the user was supposed to read.

## Rule 7 — HTML file delivery on Connie: MEDIA: forces download, use URLs instead

When the user is on the Hermes Desktop app (Windows) and needs to view an `.html` file from the vault, **do not use the `MEDIA:` prefix** — Windows treats it as a file download rather than opening it in the browser. Instead, serve the vault via HTTPS and give the user a URL to paste into their browser.

**Working stack (vault-browser skill):**
1. Python HTTP server on VPS: `vault-browser.service` systemd unit serving `/root/.hermes/vault` on `127.0.0.1:9124`
2. nginx reverse proxy: `vault.robblake.cloud` → `127.0.0.1:9124` with Let's Encrypt cert
3. DNS A record: `vault.robblake.cloud` → `2.25.172.164`

**URLs to give the user:**
```
https://vault.robblake.cloud/index.html
https://vault.robblake.cloud/Projects/Active/motion-canvases/premium-line-bail-bonds.html
```

**Fallback if HTTPS is unavailable:**
```
http://2.25.172.164:9124/index.html
```

**Why MEDIA: fails on Windows:** The Hermes Desktop Electron app downloads attached files to the user's Downloads folder rather than opening them in the browser. This is platform behavior, not a bug. Linux/macOS handle `MEDIA:` correctly by opening inline.

**Why nginx can't serve `/root/.hermes/vault` directly:** `/root` is mode `700`. nginx runs as `www-data` and cannot traverse into `/root/.hermes/` regardless of inner file permissions, ACLs, or bind mounts. The proxy-to-localhost pattern bypasses this entirely — nginx never touches `/root`, the Python server (running as root via systemd) does.

**Diagnosing nginx proxy issues:** If `curl -sk https://vault.robblake.cloud/` returns empty:
1. Check the Python server is running: `systemctl status vault-browser.service`
2. Check for duplicate server blocks: `nginx -T | grep "server_name"`
3. Remove stale configs in `/etc/nginx/sites-enabled/` and reload

## Related

- `vault-browser` skill — full setup, DNS, nginx config, systemd unit, troubleshooting
- `vault-indexing` skill — maintaining the clickable HTML index for the vault

## Rule 6 — OneDrive uninstall recovery: user-folder repoint (2026-08-06)

After Rob uninstalled OneDrive and deleted `C:\\Users\\Rob\\OneDrive`, TWO shell folders broke because they had been KFM-redirected into the deleted tree:

- **Desktop icons vanished** — Desktop pointed at the deleted OneDrive Desktop
- **Pictures vanished from the Explorer sidebar** — the `C:\\Users\\Rob\\Pictures` folder itself was gone (it had only existed inside OneDrive)

**The recovery sequence (Command Prompt, all paste-safe one-liners):**

1. Recreate missing folders (safe to run for all six — existing ones just print "already exists"): `mkdir "%USERPROFILE%\\Desktop"` / `Documents` / `Pictures` / `Downloads` / `Music` / `Videos`
2. Repoint the shell registry (the critical step — the folder existing is NOT enough):
   ```
   reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders" /v Desktop /t REG_EXPAND_SZ /d "%USERPROFILE%\\Desktop" /f
   reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders" /v Personal /t REG_EXPAND_SZ /d "%USERPROFILE%\\Documents" /f
   reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders" /v "My Pictures" /t REG_EXPAND_SZ /d "%USERPROFILE%\\Pictures" /f
   ```
3. Restart Explorer: `taskkill /f /im explorer.exe` then `start explorer.exe` (screen flashes, taskbar returns).
4. Verify: `reg query "HKCU\\...\\User Shell Folders" /v "My Pictures"` + `dir "%USERPROFILE%\\Pictures"`.

**If a folder still doesn't appear in the sidebar after registry+restart:** the *library* file is stale. `%APPDATA%\\Microsoft\\Windows\\Libraries\\Pictures.library-ms` — delete it and recreate. **Never paste XML into cmd** (every line errors with "The syntax of the command is incorrect" — hit live). Instead: `notepad Pictures.library-ms`, paste the XML into Notepad, Save, close, restart Explorer. Same Notepad-vehicle rule as Rule 4 (raw JSON at PowerShell) — cmd is a shell, not a file editor.

**Disaster guardrail — the `AppData\\Local` near-miss:** when directing Rob to delete `AppData\\Local\\Microsoft\\OneDrive`, he initiated a delete of **`AppData\\Local`** itself and cancelled mid-flight. Consequences were recoverable (Claude/Chrome/vault survived) but Pictures content was lost. When handing him ANY delete command inside AppData: (a) give the full absolute path in the block, never a "navigate to AppData\\Local, find Microsoft, find OneDrive" prose path; (b) prefer `ren "<path>" "OneDrive-DELETED"` (rename-first) over `rmdir /s /q` for anything under AppData — a rename is reversible, a delete is not; (c) state explicitly what must NOT be deleted ("AppData holds ALL your app settings — we touch only the OneDrive subfolder").

**Crisis-mode turn discipline:** during the recovery Rob sent "you there" x3 — multi-minute agent silences while composing long fix blocks read as abandonment mid-disaster. In live damage-control, send short acknowledgment turns between steps ("I'm here — next command coming") rather than one giant silent compose.

## Rule 7 — Secrets folders under the user profile must be excluded from cloud sync (2026-08-15)

If the user creates a secrets folder under their user profile — e.g. `C:\Users\Rob\secrets` — Google Drive Desktop, OneDrive, or Dropbox may sync it automatically because it lives inside the profile tree.

**Checklist when a secrets folder is created or discovered:**

1. Check whether a sync client is running:
   ```powershell
   Get-Process -Name GoogleDriveFS,OneDrive,Dropbox -ErrorAction SilentlyContinue | Select-Object ProcessName,Path
   ```
2. If any are running, exclude the folder from sync:
   - **Google Drive Desktop:** tray icon → Preferences → Google Drive tab → exclude folder
   - **OneDrive:** Settings → Account → Choose folders → uncheck the secrets folder
   - **Dropbox:** Preferences → Sync → Selective Sync → uncheck the folder
3. If the folder already synced to the cloud, delete it from the web interface and verify it is gone from the cloud before assuming it is safe.

**Why this matters:** a single `.env` or config file containing API keys inside a synced folder can be scanned by the provider even without public sharing. OpenAI's leak-detection caught a Drive-uploaded `gpt-image-2` skill file containing a literal key.

**When to trigger this rule:** any time the user mentions a secrets folder, `.env` file, or API key storage under their Windows user profile.

## Rule 8 — HTML file delivery on Connie: MEDIA: forces download, use URLs instead

When the user is on the Hermes Desktop app (Windows) and needs to view an `.html` file from the vault, **do not use the `MEDIA:` prefix** — Windows treats it as a file download rather than opening it in the browser. Instead, serve the vault via HTTPS and give the user a URL to paste into their browser.

**Working stack (vault-browser skill):**
1. Python HTTP server on VPS: `vault-browser.service` systemd unit serving `/root/.hermes/vault` on `127.0.0.1:9124`
2. nginx reverse proxy: `vault.robblake.cloud` → `127.0.0.1:9124` with Let's Encrypt cert
3. DNS A record: `vault.robblake.cloud` → `2.25.172.164`

**URLs to give the user:**
```
https://vault.robblake.cloud/index.html
https://vault.robblake.cloud/Projects/Active/motion-canvases/premium-line-bail-bonds.html
```

**Fallback if HTTPS is unavailable:**
```
http://2.25.172.164:9124/index.html
```

**Why MEDIA: fails on Windows:** The Hermes Desktop Electron app downloads attached files to the user's Downloads folder rather than opening it in the browser. This is platform behavior, not a bug. Linux/macOS handle `MEDIA:` correctly by opening inline.

**Why nginx can't serve `/root/.hermes/vault` directly:** `/root` is mode `700`. nginx runs as `www-data` and cannot traverse into `/root/.hermes/` regardless of inner file permissions, ACLs, or bind mounts. The proxy-to-localhost pattern bypasses this entirely — nginx never touches `/root`, the Python server (running as root via systemd) does.

**Diagnosing nginx proxy issues:** If `curl -sk https://vault.robblake.cloud/` returns empty:
1. Check the Python server is running: `systemctl status vault-browser.service`
2. Check for duplicate server blocks: `nginx -T | grep "server_name"`
3. Remove stale configs in `/etc/nginx/sites-enabled/` and reload

## Related

- `vault-browser` skill — full setup, DNS, nginx config, systemd unit, troubleshooting
- `vault-indexing` skill — maintaining the clickable HTML index for the vault
