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
- Delete the symlink at destination and install into a real directory path
- Point destination at the real target directory instead of the link
- 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
- Never symlink the .app path; keep it a real directory
- Audit install scripts and version managers for symlinked app paths
- Exclude the install directory from tools that create links (cloud sync, automount)
- Resolve destination with fs.realpath before installing if links are expected upstream
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
- The installation destination contains a different…
- Refusing to replace : the update target is not a regular…
- application not found — call list_apps for exact names/pids
- audited skill path does not match owned package
- background preview capture failed
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)