Hmbown/CodeWhale · error · Error

The installation destination must not be a symlink.

Error message

The installation destination must not be a symlink.

What it means

replaceMacBundle rejects a destination that is a symbolic link. Atomic publication relies on rename replacing a real directory; renaming over a symlink would silently write through to whatever the link targets, so a defensive lstat check throws before any staging work.

Solutions

  1. Delete the symlink at destination and install into a real directory path
  2. Point destination at the real target directory instead of the link
  3. Investigate what created the symlink (version manager, sync tool) and reconfigure it before reinstalling

Example fix

// before
code.replaceMacBundle(src, "/Applications/ComputerUse.app"); // path is a symlink
// after
code.fs.unlinkSync("/Applications/ComputerUse.app"); // remove symlink first
code.replaceMacBundle(src, "/Applications/ComputerUse.app");
Defensive patterns

Strategy: validation

Validate before calling

if (fs.existsSync(destination) && fs.lstatSync(destination).isSymbolicLink()) {
  throw new Error("destination is a symlink");
}

Try / catch

try {
  replaceMacBundle(src, dest);
} catch (e) {
  if (e.message.includes("must not be a symlink")) {
    fs.unlinkSync(dest); // or resolve and install at the real target
    replaceMacBundle(src, dest);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling replaceMacBundle(source, destination) where the existing destination path is a symlink (fs.lstatSync(destination).isSymbolicLink() is true). Note the symlink check runs only after the different-application check, so a symlink pointing at a foreign app reports error 91 instead.

Common situations: User or an earlier tool replaced the .app with a symlink (e.g. linking to a versioned directory); automounter or cloud-sync directories exposed as symlinks; malicious symlink planted to redirect an install.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/e5e02e8a6c92c85b. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/plugins/computer-use/app/install-macos.mjs:11

import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { spawnSync } from "node:child_process";

/** Publish a verified bundle in one rename, retaining the previous install. */
export function replaceMacBundle(source, destination, { prepare = () => {}, verify = verifySignature } = {}) {
  const parent = path.dirname(destination);
  fs.mkdirSync(parent, { recursive: true });
  if (fs.existsSync(destination) && !fs.existsSync(path.join(destination, "Contents", "Resources", "plugin", "app", "daemon.mjs"))) throw new Error("The installation destination contains a different application.");
  if (fs.existsSync(destination) && fs.lstatSync(destination).isSymbolicLink()) throw new Error("The installation destination must not be a symlink.");
  const staging = fs.mkdtempSync(path.join(parent, ".codewhale-cu-update-"));
  const next = path.join(staging, path.basename(destination));
  let backup = null;
  try {
    fs.cpSync(source, next, { recursive: true });
    prepare(next);
    verify(next);
    if (fs.existsSync(destination)) {
      const backups = path.join(parent, ".codewhale-cu-backups");
      fs.mkdirSync(backups, { recursive: true, mode: 0o700 });
      backup = path.join(backups, `${Date.now()}-${crypto.randomUUID()}.app`);
      fs.renameSync(destination, backup);
    }
    try { fs.renameSync(next, destination); }
    catch (error) { if (backup) fs.renameSync(backup, destination); throw error; }
    return { backup };
  } finally { fs.rmSync(staging, { recursive: true, force: true }); }
}

View on GitHub (pinned to 73e0f67d83)