Hmbown/CodeWhale · error · Error
Inconsistent update file header.
Error message
Inconsistent update file header.
What it means
The validator cross-checks each entry's local header against its central-directory record: the local header's name/extra lengths plus the compressed size must stay within the archive (against the central-directory offset recorded in the EOCD), and the name stored in the local header must be byte-identical to the central-directory name. A mismatch means the two directory structures disagree, which breaks the offset math used to slice the compressed payload and indicates a malformed or malicious archive.
Solutions
- Rebuild the archive cleanly with one tool (`ditto -c -k` or Info-ZIP) so central and local headers are written consistently.
- Run `unzip -t` on the artifact; if it passes there but fails here, your build pipeline is mutating the zip after creation — find and remove that step.
- Re-download and verify the asset's SHA-256 against the release digest.
- Do not prepend stubs or strip extra fields from a finished zip; repackage instead.
Defensive patterns
Strategy: validation
Validate before calling
const names = execFileSync("unzip", ["-Z1", zipPath]).toString();
if (new Set(names.split("\n").filter(Boolean)).size !== names.split("\n").filter(Boolean).length) throw new Error("central/local name mismatch risk"); Try / catch
try { validateReleaseZip(bytes); } catch (e) { if (e.message === "Inconsistent update file header.") throw new Error("Local and central headers disagree — repackage with a single zip tool"); throw e; } Prevention
- Avoid pipelines that rewrite names/extra fields in one header only
- Don't prepend self-extractor stubs to release zips
- Round-trip `zip`/`unzip -t` in CI before upload
- Verify asset digest on download
When it happens
Trigger: A local header whose 30-byte fixed part + nameLength + extraLength + compressedSize exceeds the central-directory start (EOCD offset at end+16), or whose local name differs from the central-directory name — typical of zip files with different local vs central metadata (some writers store different extra fields) or of archives edited after writing.
Common situations: A build step rewriting entry names in one directory but not the other; zip tools that prepend data (e.g. self-extracting stubs) shifting offsets; crafted archives where central and local names diverge to smuggle paths; concatenating two zips so EOCD offsets point at the wrong file.
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 length.
- Invalid update entry.
- Invalid update file header.
- The update entry size did not match its contents.
- Inconsistent update sizes or compression.
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/8fb4b65fc08b12f0.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/app/updates.mjs:68
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 writes
// anything, including a compressed payload whose headers understate size.
const payload=bytes.subarray(start,start+compressed);
let expanded;
try { expanded=method===0?payload.length:inflateRawSync(payload,{maxOutputLength:Math.max(size,1)}).length; }
catch { throw new Error("Invalid or oversized compressed update entry."); }
if(expanded!==size) throw new Error("The update entry size did not match its contents.");
position+=46+length+extra+comment;
}
if(position!==end) throw new Error("Invalid update archive length.");
return count;
}
export async function prepareUpdate(update) {
if(!update?.available) throw new Error("Check for an available update first.");
if(!newerVersion(update.version,APP_VERSION)||update.url!==`${repository}/releases/download/v${update.version}/Codewhale-Computer-Use-${update.version}-macos-universal.zip`||!Number.isSafeInteger(update.size)||update.size<=0||update.size>limit) throw new Error("The update identity is invalid.");View on GitHub (pinned to 73e0f67d83)