musistudio/claude-code-router · error · Error

Failed to open Claude App: ${spawnError}

Error message

Failed to open Claude App: ${spawnError}

What it means

Thrown by installUpdate when there is no downloaded update ready to install — status.canInstall is false, meaning the download step never completed (or was not run) on this staging path.

Source

Thrown at packages/cli/src/cli.ts:193

      const runtimeResult = applyProfileRuntimeConfig(launchConfig, profile, launchConfig.APIKEY);
      if (!runtimeResult.ok) {
        throw new Error(runtimeResult.message);
      }
    }
    if (resolvedSurface === "cli" && process.env[prepareProfileOnlyEnv] === "1") {
    return;
  }
  if (profile.agent === "claude-code" && resolvedSurface === "app") {
      applyClaudeAppGatewayConfig(launchConfig);
      applyClaudeAppGatewayConfig(launchConfig, {
        backup: false,
        dataDir: resolveClaudeAppProfileUserDataDir(configDir, profile),
        refreshModelDiscoveryCache: true
      });
      const launch = await launchClaudeAppProfile(configDir, profile, launchConfig);
      const spawnError = await waitForImmediateSpawnError(launch.child, 500);
      if (spawnError) {
        throw new Error(`Failed to open Claude App: ${spawnError}`);
      }
      process.stdout.write(`Opened Claude App with ${profile.name || profile.id}.\n`);
      return;
    }
    if (profile.agent === "zcode" && resolvedSurface === "app" && profileOptions.agentArgs.length === 0) {
      const launch = launchZcodeAppProfile(configDir, profile, launchConfig);
      const spawnError = await waitForImmediateSpawnError(launch.child, 500);
      if (spawnError) {
        throw new Error(`Failed to open ZCode App: ${spawnError}`);
      }
      process.stdout.write(`Opened ZCode App with ${profile.name || profile.id}.\n`);
      return;
    }

    const plan = buildProfileLaunchPlan(configDir, profile, resolvedSurface, profileOptions.agentArgs);

    if (path.isAbsolute(plan.command) && !existsSync(plan.command)) {
      throw new Error(`Profile launcher was not found: ${plan.command}. Open CCR once or re-save the profile.`);

View on GitHub (pinned to 99f24806c6)

Solutions

  1. Ensure downloadUpdate() awaited successfully before enabling/calling installUpdate()
  2. Subscribe to update status and only offer install when canInstall is true
  3. If download previously failed, re-run downloadUpdate() and handle its error first
  4. After app restart, re-check for updates and re-download before installing

Example fix

// before
await updates.installUpdate(); // no staged download
// after
await updates.downloadUpdate();
if ((await updates.getStatus()).canInstall) {
  await updates.installUpdate();
}
Defensive patterns

Strategy: validation

Validate before calling

const status = await updateService.getStatus();
if (!status.canInstall) {
  await updateService.downloadUpdate();
}
await updateService.installUpdate();

Type guard

const canInstallUpdate = (s: UpdateStatus): boolean =>
  s.canInstall === true;

Try / catch

try {
  await updateService.installUpdate();
} catch (error) {
  if (error instanceof Error && /No downloaded update/.test(error.message)) {
    await updateService.downloadUpdate(); // then retry install
  } else { throw error; }
}

Prevention

When it happens

Trigger: Calling installUpdate() before downloadUpdate() has succeeded, after a failed download, or after the staged update was cleared/restarted.

Common situations: UI flow bug that skips or races the download step; user clicks 'install' while download is still in progress; app restarted between download and install so no staged update exists; a prior download failed silently.

Related errors


AI-assisted analysis of musistudio/claude-code-router@99f24806c6 (2026-08-27). Data as JSON: /api/errors/b2d387cd9190c155. Report an issue: GitHub.