Hmbown/CodeWhale · error · Error
The update download redirected to an unexpected host.
Error message
The update download redirected to an unexpected host.
What it means
During download, prepareUpdate() follows up to 4 HTTP redirects manually. Each redirect target must be https and on one of the allowed hosts: github.com, release-assets.githubusercontent.com, or objects.githubusercontent.com. A redirect to any other protocol or hostname is rejected to prevent the update binary from being served by an attacker-controlled server.
Solutions
- Remove any proxy, TLS-interception, or DNS override that rewrites github.com / *.githubusercontent.com redirects to a different host.
- Test the redirect chain (curl -sIL <release url>) and confirm every Location lands on github.com, release-assets.githubusercontent.com, or objects.githubusercontent.com over https.
- If you operate a mirror, host the asset at the exact expected release URL instead of redirecting to a third-party domain.
- Retry the update from an unrestricted network if a security appliance is intercepting traffic.
Defensive patterns
Strategy: try-catch
Try / catch
try { await prepareUpdate(update); } catch (e) { if (/unexpected host/.test(e.message)) { /* disable proxy/TLS interception or bypass the mirror, then retry */ } else throw e; } Prevention
- Avoid proxies or TLS-intercepting appliances on hosts that re-route github.com redirects.
- Test redirect chains with curl -sIL before rolling out updates on a controlled network.
- Do not front the release URL with a redirect to a third-party mirror domain.
When it happens
Trigger: The GitHub release URL or asset CDN responds with a 301/302/303/307/308 whose Location header points to a non-https URL or a hostname outside the allowlist (e.g. a corporate proxy, a mirror domain, or a malicious redirect injected between the client and GitHub).
Common situations: Corporate HTTPS proxies or DNS filtering appliances rewriting GitHub redirects; HTTPS interception middleboxes that re-host release assets; DNS hijacking or hosts-file overrides pointing github.com elsewhere; testing with a local mirror server that redirects to an unlisted host.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
Related errors
- bundle redirects may not change URL scheme
- failed to resolve latest stable release from
- release redirect did not resolve to a tag URL
- Release redirect refused
- Codewhale web is loopback-only and must bind to 127.0.0.1
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/57ce5b028760a6a5.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/app/updates.mjs:93
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.");
// Only GitHub's fixed release URL and its asset CDN can serve the bytes.
let url=update.url, response;
for(let redirects=0;redirects<4;redirects++) {
response=await fetch(url,{redirect:"manual",signal:AbortSignal.timeout(60_000)});
if(![301,302,303,307,308].includes(response.status)) break;
const next=new URL(response.headers.get("location"),url);
if(next.protocol!=="https:"||!["github.com","release-assets.githubusercontent.com","objects.githubusercontent.com"].includes(next.hostname)) throw new Error("The update download redirected to an unexpected host.");
url=next.href;
}
if(!response?.ok) throw new Error("The update could not be downloaded. Your current app is unchanged.");
const bytes=await responseBytes(response,update.size);
if(bytes.length!==update.size||crypto.createHash("sha256").update(bytes).digest("hex")!==update.sha256) throw new Error("The update checksum did not match. Your current app is unchanged.");
validateReleaseZip(bytes);
const stage=fs.mkdtempSync(path.join(os.tmpdir(),"codewhale-cu-release-"));
try {
const archive=path.join(stage,"release.zip"); fs.writeFileSync(archive,bytes,{mode:0o600});
const result=spawnSync("ditto",["-x","-k",archive,stage],{encoding:"utf8"});
if(result.status!==0) throw new Error("The update could not be unpacked.");
const bundle=path.join(stage,`${APP_NAME}.app`); verifyReleaseBundle(bundle);
const version=spawnSync("/usr/libexec/PlistBuddy",["-c","Print :CFBundleShortVersionString",path.join(bundle,"Contents","Info.plist")],{encoding:"utf8"});
if(version.status!==0||version.stdout.trim()!==update.version) throw new Error("The downloaded app has a different version.");
return {stage,bundle};
} catch(error) { fs.rmSync(stage,{recursive:true,force:true}); throw error; }
}
View on GitHub (pinned to 73e0f67d83)