midudev/autoskills · error · Error
no recorded hash for
Error message
no recorded hash for ${normalizedRel} What it means
The installer enforces integrity by comparing every downloaded file's SHA-256 against a hash recorded in the registry entry (entry.sha256, keyed by relative path). If neither the original rel string nor the normalized path has a recorded hash, it refuses to download, because an unhashed file could not be verified after download.
Solutions
- Regenerate the registry manifest so every downloaded file has a sha256 entry (run the registry sync/publish script).
- If hand-editing, add the missing key to entry.sha256 with the correct sha256 of the file content.
- Re-sync your local skills-registry.json from the official source in case your copy is stale or corrupted.
- Remove the unhashable file from the registry entry's file list if it isn't required.
Example fix
// registry entry (before)
{ "files": ["skills/foo/NEW.md"], "sha256": {} }
// after
{ "files": ["skills/foo/NEW.md"], "sha256": { "skills/foo/NEW.md": "<sha256 of file>" } } Defensive patterns
Strategy: validation
Validate before calling
import { createHash } from "node:crypto";
function validateEntryHashes(entry) {
const files = entry.files ?? Object.keys(entry.sha256);
return files.every(rel => typeof entry.sha256[rel] === "string" && entry.sha256[rel].length === 64);
}
// before install: if (!validateEntryHashes(entry)) throw new Error("registry entry has files without sha256"); Type guard
const hasHash = (entry, rel) => typeof entry.sha256?.[rel] === "string" && entry.sha256[rel].length === 64;
Try / catch
try {
await downloadRegistryEntry(name, entry, dest);
} catch (e) {
if (e.message.startsWith("no recorded hash for")) {
const missing = e.message.match(/no recorded hash for (.+)$/)?.[1];
await resyncRegistryManifest(); // refresh manifest, then retry once
return downloadRegistryEntry(name, freshEntry, dest);
} else throw e;
} Prevention
- Always regenerate sha256 via the official publish script, never by hand.
- Add a CI check that every file in the registry has a 64-char sha256 entry.
- Re-sync the local registry manifest before installs in long-lived environments.
- Treat a manifest/hash mismatch as a sign of a stale cache, not something to bypass.
When it happens
Trigger: Calling downloadRegistryFile for a rel path that is absent from entry.sha256 — e.g. the skill's file list in the registry manifest and its sha256 map are out of sync, or a new file was added to the registry without regenerating hashes.
Common situations: Hand-editing skills-registry.json to add a file without updating sha256; a registry publishing bug that emitted files[] but skipped the hash map; stale local registry cache from an older release where the file didn't exist.
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
AI-assisted analysis of midudev/autoskills@0ec725320d (2026-09-15).
Data as JSON: /api/errors/7776e768c43229d7.
Report an issue: GitHub.
Appendix: source
Thrown at packages/autoskills/installer.ts:286
function isDisallowedSkillFile(rel: string): boolean {
return rel.toLowerCase().endsWith(".zip");
}
async function downloadRegistryFile(
skillName: string,
entry: RegistryEntry,
rel: string,
opts: InstallOptions,
): Promise<{ buf: Buffer; url: string }> {
const normalizedRel = normalizeRegistryRelPath(rel);
if (isDisallowedSkillFile(normalizedRel)) {
throw new Error(`refusing to download disallowed skill archive: ${normalizedRel}`);
}
const expected = entry.sha256[rel] || entry.sha256[normalizedRel];
if (!expected) {
throw new Error(`no recorded hash for ${normalizedRel}`);
}
const fetchFile = opts.fetchImpl || fetch;
const errors = [];
for (const baseUrl of getRegistryRawBaseUrls(opts)) {
const url = `${baseUrl}/${encodeRawPath(skillName, normalizedRel)}`;
opts.onTrace?.(`GET ${url}`);
const res = await fetchFile(url, {
headers: githubDownloadHeaders(url),
});
if (!res.ok) {
const resetAt = Number(res.headers.get("x-ratelimit-reset") || 0) * 1000;
const resetSuffix = resetAt ? ` (resets ${new Date(resetAt).toISOString()})` : "";
if (res.status === 403 && res.headers.get("x-ratelimit-remaining") === "0") {
throw new Error(
`GitHub rate limit exceeded${resetSuffix}. Set GITHUB_TOKEN or GH_TOKEN to increase the limit.`,
);
}View on GitHub (pinned to 0ec725320d)