Hmbown/CodeWhale · error · Error
Invalid update archive.
Error message
Invalid update archive.
What it means
validateReleaseZip scans the buffer for the End of Central Directory record (0x06054b50) whose comment length exactly reaches the end of file; it throws 'Invalid update archive.' when no valid EOCD is found or the disk-number fields in the EOCD are non-zero (multi-disk archives are unsupported).
Solutions
- Re-download the release asset and confirm its SHA-256 matches the digest published on the release
- Inspect the first bytes (PK\u0003\u0004 signature) to confirm the file is actually a ZIP, not an HTML error page
- Repackage without ZIP64/spanning options if the archive was built with them
- Verify asset size against the release metadata before validation
Example fix
// before (validate whatever came back)
code.validateReleaseZip(bytes);
// after (verify digest first)
if (code.createHash("sha256").update(bytes).digest("hex") !== expectedSha) throw new Error("download digest mismatch");
code.validateReleaseZip(bytes); Defensive patterns
Strategy: validation
Validate before calling
function looksLikeZip(bytes) {
return bytes.length >= 22 && bytes.readUInt32LE(0) === 0x04034b50;
} Try / catch
try {
validateReleaseZip(bytes);
} catch (e) {
if (e.message === "Invalid update archive.") {
discardDownload(); // corrupt or hostile; do not extract
} else throw e;
} Prevention
- Verify the asset's SHA-256 digest before parsing
- Ensure downloads complete fully (check Content-Length vs received bytes)
- Avoid ZIP64/spanning archive options when building releases
- Reject non-200 responses instead of parsing their bodies as archives
When it happens
Trigger: Calling validateReleaseZip on bytes that are not a supported ZIP: truncated download, an HTML error page, multi-disk/segmented archives, an EOCD with non-zero disk numbers, or an EOCD whose comment length does not match the buffer end.
Common situations: Download corrupted or interrupted mid-transfer; CDN/proxy returning an error page instead of the asset; an archive produced with ZIP64 or spanning features; checksum not verified before validation.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- Invalid update archive index.
- append_allow_rules only accepts action = "allow"
- Codewhale credentials directory has an unsupported component
- Codewhale terminal receipt exceeded its string bound
- CodewhalePet/1
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/f7364ea829ebdc5b.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/app/updates.mjs:53
if(!version||release.draft||release.prerelease||!newerVersion(version,current)) return {available:false,message:`You have Computer Use ${current}. No newer stable installer is available.`};
const name=`Codewhale-Computer-Use-${version}-macos-universal.zip`;
const asset=release.assets?.find(asset=>asset.name===name);
const url=`${repository}/releases/download/v${version}/${name}`;
if(!asset||asset.browser_download_url!==url||!/^sha256:[a-f0-9]{64}$/.test(asset.digest)||!Number.isSafeInteger(asset.size)||asset.size<=0||asset.size>limit) return {available:false,message:`Version ${version} has no verified macOS installer yet.`};
return {available:true,version,url,sha256:asset.digest.slice(7),size:asset.size,message:`Computer Use ${version} is available. Install it to restart the helper; existing computer sessions will stop.`};
}
export async function checkForUpdate() {
const response=await fetch("https://api.github.com/repos/Hmbown/codewhale-cu-plugin/releases/latest",{redirect:"error",headers:{Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28"},signal:AbortSignal.timeout(10_000)});
if(response.status===404) return {available:false,message:"No stable installer has been published yet. Your current app is unchanged."};
if(!response.ok) throw new Error(`The update service is unavailable (${response.status}). Try again later.`);
return releaseUpdate(JSON.parse((await responseBytes(response,1024*1024)).toString("utf8")));
}
/** Inspect both ZIP headers before extraction: no links, traversal or bombs. */
export function validateReleaseZip(bytes) {
const minimum=Math.max(0,bytes.length-65557); let end=-1;
for(let i=bytes.length-22;i>=minimum;i--) if(bytes.readUInt32LE(i)===0x06054b50&&i+22+bytes.readUInt16LE(i+20)===bytes.length) { end=i; break; }
if(end<0||bytes.readUInt16LE(end+4)||bytes.readUInt16LE(end+6)) throw new Error("Invalid update archive.");
const count=bytes.readUInt16LE(end+10); let position=bytes.readUInt32LE(end+16),total=0;
if(!count||count>2000||bytes.readUInt16LE(end+8)!==count||position+bytes.readUInt32LE(end+12)!==end) throw new Error("Invalid update archive index.");
const seen=new Set();
for(let i=0;i<count;i++) {
if(position+46>end||bytes.readUInt32LE(position)!==0x02014b50) throw new Error("Invalid update entry.");
const flags=bytes.readUInt16LE(position+8),method=bytes.readUInt16LE(position+10),length=bytes.readUInt16LE(position+28),extra=bytes.readUInt16LE(position+30),comment=bytes.readUInt16LE(position+32);
const name=bytes.subarray(position+46,position+46+length).toString("utf8");
const kind=(bytes.readUInt32LE(position+38)>>>16)&0xf000,offset=bytes.readUInt32LE(position+42),compressed=bytes.readUInt32LE(position+20);
const size=bytes.readUInt32LE(position+24); total+=size;
if(flags&1||![0,8].includes(method)||![0,0x4000,0x8000].includes(kind)||total>512*1024*1024||position+46+length+extra+comment>end) throw new Error("Unsupported update entry.");
if(!name.startsWith(`${APP_NAME}.app/`)||name.includes("\\")||name.includes(":")||name.includes("\0")||name.split("/").some(part=>part===".."||part===".")||seen.has(name)) throw new Error("Unsafe update path.");
seen.add(name);
if(offset+30>position||bytes.readUInt32LE(offset)!==0x04034b50) throw new Error("Invalid update file header.");
const localLength=bytes.readUInt16LE(offset+26),localExtra=bytes.readUInt16LE(offset+28);
if(offset+30+localLength+localExtra+compressed>bytes.readUInt32LE(end+16)||bytes.subarray(offset+30,offset+30+localLength).toString("utf8")!==name) throw new Error("Inconsistent update file header.");
if(bytes.readUInt16LE(offset+8)!==method||bytes.readUInt16LE(offset+6)!==flags||(!(flags&8)&&(bytes.readUInt32LE(offset+18)!==compressed||bytes.readUInt32LE(offset+22)!==size))) throw new Error("Inconsistent update sizes or compression.");
const start=offset+30+localLength+localExtra;
// Header sizes are untrusted. Bound actual expansion before ditto writesView on GitHub (pinned to 73e0f67d83)