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

  1. Regenerate the registry manifest so every downloaded file has a sha256 entry (run the registry sync/publish script).
  2. If hand-editing, add the missing key to entry.sha256 with the correct sha256 of the file content.
  3. Re-sync your local skills-registry.json from the official source in case your copy is stale or corrupted.
  4. 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

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)