Hmbown/CodeWhale · error

The installed version could not be read.

Error message

The installed version could not be read.

What it means

After swapping in the new bundle, the install step reads CFBundleShortVersionString from the installed app's Info.plist with PlistBuddy to confirm the installation. A non-zero PlistBuddy status means the version could not be read — the installed bundle is missing its Info.plist, malformed, or PlistBuddy could not run — so the update reports failure rather than claiming success.

Solutions

  1. Retry the update — a transient swap failure usually resolves on a second attempt.
  2. Check the destination bundle exists with a readable Contents/Info.plist (plutil -p <dest>/Contents/Info.plist) and that the volume has free space.
  3. Verify /usr/libexec/PlistBuddy exists and is executable; it ships with standard macOS.
  4. Check quarantine/EDR logs for software interfering with the newly installed bundle and re-run after adding an exception.
Defensive patterns

Strategy: retry

Validate before calling

const {execFileSync} = await import('child_process');
const out = execFileSync('/usr/libexec/PlistBuddy', ['-c','Print :CFBundleShortVersionString', `${destination}/Contents/Info.plist`], {encoding:'utf8'});
if (!out.trim()) throw new Error('installed Info.plist unreadable');

Try / catch

try { await restartWithUpdate(prepared, destination); } catch (e) { if (/installed version could not be read/.test(e.message)) { /* inspect destination Info.plist / disk / quarantine, then retry */ } else throw e; }

Prevention

When it happens

Trigger: replaceMacBundle swapped in a bundle whose Contents/Info.plist is missing or corrupt; PlistBuddy (/usr/libexec/PlistBuddy) is unavailable or blocked on the host; filesystem permissions on the destination make Info.plist unreadable; partial copy/atomic-swap failure left a truncated bundle at the destination.

Common situations: Destination volume errors or disk-full during the bundle swap; security software quarantining the newly installed bundle; a corrupted source bundle that passed earlier checks but degraded during replace; non-standard macOS images lacking PlistBuddy.

Related errors


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

Appendix: source

Thrown at crates/tui/plugins/computer-use/app/updates.mjs:133

  const child=spawn(process.execPath,[fileURLToPath(import.meta.url),"--apply",prepared.bundle,destination,String(process.pid),String(process.ppid)],{detached:true,stdio:["ignore",log,log]});
  try { await new Promise((resolve,reject)=>{child.once("spawn",resolve);child.once("error",reject);}); child.unref(); }
  finally { fs.closeSync(log); }
}

if(process.argv[1]===fileURLToPath(import.meta.url)&&process.argv[2]==="--apply") {
  const [source,destination,owner,launcher]=process.argv.slice(3);
  let result;
  try {
    verifyReleaseBundle(source);
    process.kill(Number(owner),"SIGTERM");
    for(let i=0;i<100;i++) { try { process.kill(Number(owner),0); } catch { break; } await new Promise(resolve=>setTimeout(resolve,100)); }
    try { process.kill(Number(owner),0); throw new Error("The running helper did not stop; update cancelled."); } catch(error) { if(error.code!=="ESRCH") throw error; }
    // LaunchServices must not send Reopen to the retiring menu-bar process.
    for(let i=0;i<100;i++) { try { process.kill(Number(launcher),0); } catch { break; } await new Promise(resolve=>setTimeout(resolve,100)); }
    try { process.kill(Number(launcher),0); throw new Error("The menu-bar app did not exit; update cancelled."); } catch(error) { if(error.code!=="ESRCH") throw error; }
    const receipt=replaceMacBundle(source,destination,{verify:verifyReleaseBundle});
    const installed=spawnSync("/usr/libexec/PlistBuddy",["-c","Print :CFBundleShortVersionString",path.join(destination,"Contents","Info.plist")],{encoding:"utf8"});
    if(installed.status!==0) throw new Error("The installed version could not be read.");
    console.log(JSON.stringify({version:installed.stdout.trim(),...receipt,installedAt:new Date().toISOString()}));
    result={ok:true,message:`Updated to ${installed.stdout.trim()}. Choose Allow new sessions when you are ready.`};
  } catch(error) {
    console.error(error.message); process.exitCode=1;
    result={ok:false,message:`The update could not be completed: ${String(error.message).slice(0,600)} Computer sessions remain stopped.`};
  } finally {
    // The apply process outlives the menu app. Carry its result into the next
    // launch so a failed install is visible without hunting for update.log.
    try {
      fs.mkdirSync(stateDir(),{recursive:true});
      const temporary=`${updateResultPath()}.${process.pid}.tmp`;
      fs.writeFileSync(temporary,JSON.stringify({...result,completedAt:new Date().toISOString()})+"\n",{mode:0o600});
      fs.renameSync(temporary,updateResultPath());
    } catch(error) { console.error(`Could not save the update result: ${error.message}`); }
    if(destination&&fs.existsSync(destination)) spawnSync("open",["-g","-a",destination]);
  }
}

View on GitHub (pinned to 73e0f67d83)