[BUG] /desktop on Windows spawns bare `cmd` through the where.exe PATH resolver — failed resolution cached as null for entire session; only generic 'Failed to open Claude Desktop' shown while the real error and deepLinkUrl are swallowed

Open 💬 1 comment Opened Jun 12, 2026 by omalleyandy

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet. Closest matches are different defects behind the same generic message: #59824 / #59692 / #59883 / #36079 (MSIX install detection), #65996 / #26197 (& split by cmd.exe when cmd does spawn). Nothing covers the cmd-resolution / negative-cache path. #67156 is the same resolver defect on the git surface.
  • [x] This is a single bug report (one root defect: bare-cmd PATH resolution with permanent negative caching and a swallowed error, all in one code path).
  • [x] I am using the latest version of Claude Code available to the desktop app (2.1.170 bundled; opener verified identical in bundled 2.1.146; resolver identical to standalone 2.1.150 examined in #67156).

What's Wrong?

On Windows, /desktop (alias /app) hands the session off to the desktop app by spawning:

cmd /c start "" claude://resume?session=...&cwd=...

The "cmd" is a bare name, resolved through the CLI's where.exe-based PATH resolver — the same resolver shown in #67156 to (a) depend on the desktop-spawned CLI's stale/sanitized inherited PATH and (b) cache a failed resolution as null for the entire process lifetime. If where.exe cmd exits 1 once (e.g. the inherited PATH lacks C:\Windows\System32, or any transient hiccup), nothing is ever spawned again for cmd: every /desktop for the rest of the session fabricates exit 127 instantly and the user sees only:

Failed to open Claude Desktop. Please try opening it manually.

The real error — Command 'cmd' not found or is in an unsafe location (current directory) — is sitting in the fabricated result's stderr, and the failure object even carries the exact deepLinkUrl, but neither is surfaced. This one generic string is currently shared by at least three distinct root causes (MSIX detection: #59824 family; &-splitting: #65996; this resolver path), which is why the existing reports have been so hard to triage — none of them could see which failure they had.

Root cause (from the shipped bundle, minified names as found — desktop-bundled 2.1.170)

The opener spawns bare "cmd" and keeps only the exit code, discarding stderr:

async function _G5(H) {
  N(`Opening deep link: ${H}`);
  { let { code: K } = await m6("cmd", ["/c", "start", "", H]); return K === 0 }
  return !1
}

Its caller already has both the real diagnosis and the manual fallback in hand, and shows neither:

let K = KG5(H);                       // the claude://resume?... deep link
if (!await _G5(K)) return {
  success: !1,
  error: "Failed to open Claude Desktop. Please try opening it manually.",
  deepLinkUrl: K                      // carried, never shown
};
return { success: !0, deepLinkUrl: K }

m6 delegates to xq, which — for any bare name with no shell option — consults the resolver and fabricates a result without spawning anything when resolution returns null:

function xq(H, q, { ..., shell: Y, ... } = {...}) {
  let w = H;
  if (!Y) {
    let j = wgH(H);
    if (j === null) return Promise.resolve({
      stdout: "",
      stderr: `Command '${H}' not found or is in an unsafe location (current directory)`,
      code: 127,
      error: `Command '${H}' not found or is in an unsafe location (current directory)`
    });
    ...

wgH passes through anything containing a path separator and sends bare names to the cached resolver:

function wgH(H) {
  if (!ME1()) return H;                              // non-Windows: passthrough
  if (H.includes("/") || H.includes("\\")) return H; // absolute/relative path: passthrough
  return BKq(H)                                      // bare name: cached where.exe lookup
}

and the resolver caches failure permanently (UKq is a module-level Map, never invalidated):

let K = process.env.SYSTEMROOT || "C:\\Windows",
    $ = DpK.join(K, "System32", "where.exe");        // absolute path — for where.exe itself!
try {
  let f = jpK.execFileSync($, [H], { stdio: "pipe", encoding: "utf8",
            timeout: wE1, windowsHide: !0, env: process.env })
          .trim().split(/\r?\n/).filter(Boolean), A = process.cwd();
  for (let z of f) { if (jg8(z, A)) continue; return UKq.set(H, z), z }
  return null
} catch (_) {
  if (jE1(_)) UKq.set(H, null);                      // where.exe exit 1 → null cached FOREVER
  return null
}

So the chain is:

  1. /desktop builds the deep link and calls _G5m6("cmd", ["/c","start","",url]).
  2. wgH("cmd")BKq("cmd")where.exe cmd under the CLI's inherited env. where.exe searches cwd + PATH — so this requires the desktop-spawned CLI's PATH to contain C:\Windows\System32. #67156 proved (on this same machine, via git trace2 forensics) that the desktop-app → CLI environment chain ships a stale/sanitized PATH that is missing registry-User-PATH entries.
  3. One exit 1 → UKq.set("cmd", null) → every later call fabricates {code: 127, stderr: "Command 'cmd' not found or is in an unsafe location (current directory)"} without spawning. Unhealable in-session.
  4. _G5 keeps only code; the caller replaces the specific error with the generic message and buries deepLinkUrl in a field nothing displays.

Note the irony in step 2: the resolver already builds an absolute %SYSTEMROOT%\System32\where.exe path so that where.exe itself never depends on PATH — and then uses it to do a PATH lookup for cmd, a fixed system binary sitting in the very same directory.

Suggested fixes (aligned with #67156)

  1. Don't resolve cmd through PATH at all. Spawn %COMSPEC% (or %SYSTEMROOT%\System32\cmd.exe) the same way the resolver already locates where.exe. PATH lookup for a fixed system binary adds failure modes and zero value. Better yet: open the URL via a native ShellExecute equivalent and skip cmd entirely — that would also have prevented the #65996 / #26197 class (deep link split on & by cmd.exe re-parsing).
  2. Never cache negative resolutions for the process lifetime — retry on next call, or add TTL/invalidation (same proposal as #67156).
  3. Surface the real error. The fabricated result already carries Command 'cmd' not found or is in an unsafe location in stderr, and the failure object already carries deepLinkUrl. Showing either of these instead of the generic message would have made this diagnosable from the first report — and deepLinkUrl gives users a copy-pasteable manual fallback:

``powershell
Start-Process "claude://resume?session=<id>&cwd=<urlencoded-cwd>" # works (per #65996)
``

Steps to Reproduce

Observed live on this machine — the same machine and environment state forensically documented in #67156 (desktop-spawned CLI inherits a stale/sanitized PATH missing registry User-PATH entries):

  1. Start a Claude Code session from the Windows desktop app.
  2. Run /desktop.
  3. Failed to open Claude Desktop. Please try opening it manually. — instant, deterministic, every attempt, all session. Claude Desktop is installed and running, and the claude:// protocol handler is registered and valid.
  4. Dispatching the same claude://resume?... URL via Start-Process from a terminal works (independently confirmed in #65996) — the handoff target is fine; only the cmd spawn path is broken.

Deterministic simulation for triage on any Windows machine, derived from the shipped code:

  1. Start the CLI with a PATH that lacks C:\Windows\System32 (simulating the sanitized Electron-inherited env proven in #67156):

cmd /c "set PATH=&& %APPDATA%\Claude\claude-code\2.1.170\claude.exe"

  1. Run /desktop.
  2. → the same generic failure, instantly; cmd is never spawned (the where.exe lookup fails, null is cached).
  3. Every retry in the session fails identically, even though %SYSTEMROOT%\System32\cmd.exe exists.

What Should Happen?

  • /desktop should spawn cmd by absolute path (%COMSPEC%), or avoid cmd entirely with a native shell-open.
  • A failed where.exe lookup should not poison every subsequent spawn of that name for the whole session.
  • The failure message should include the underlying error (exit-127 stderr) and the deepLinkUrl so users can open it manually.

Environment

  • Claude Desktop 1.12603.1.0 (MSIX/WindowsApps), bundled CLI 2.1.170 (%APPDATA%\Claude\claude-code\2.1.170\claude.exe)
  • Opener code identical in bundled 2.1.146; resolver/fabrication identical to standalone 2.1.150 examined in #67156
  • Windows 11 Pro 10.0.26200
  • Related: #67156 (same resolver + negative cache, git surface), #65996 / #26197 (same launcher, &-splitting when cmd does spawn), #59824 / #59692 / #59883 / #36079 (same generic message, MSIX-detection family)

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗