{"record":{"id":"fa3607ba2d55dcc3","repo":"agalwood/Motrix","slug":"plugin-manifest-invalid-fa3607","errorCode":"PLUGIN_MANIFEST_INVALID","errorMessage":"plugin.install.local_file_hash_mismatch","messagePattern":"plugin\\.install\\.local_file_hash_mismatch","errorType":"exception","errorClass":"AppError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/install/plugin-installer.ts","lineNumber":188,"sourceCode":"      '_staging',\n      `s_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`\n    )\n    const { bundleSha256, manifestRaw } = await extractMoext(\n      moextPath,\n      stagingDir\n    )\n    const localFileHash =\n      sourceInput.type === 'local'\n        ? createHash('sha256')\n            .update(await readFile(moextPath))\n            .digest('hex')\n        : null\n    if (\n      sourceInput.type === 'local' &&\n      sourceInput.fileHash !== localFileHash\n    ) {\n      await rm(stagingDir, { recursive: true, force: true })\n      throw new AppError(\n        ErrorCode.PluginManifestInvalid,\n        'plugin.install.local_file_hash_mismatch'\n      )\n    }\n\n    let parsedManifest: PluginManifest\n    try {\n      const result = parseManifest(manifestRaw, {\n        hostVersion: this.opts.hostVersion,\n      })\n      parsedManifest = await resolveManifestForInstall(\n        result.manifest as PluginManifest,\n        stagingDir\n      )\n    } catch (e) {\n      await rm(stagingDir, { recursive: true, force: true })\n      throw e\n    }","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/install/plugin-installer.ts#L170-L206","documentation":"During stage(), for sourceInput.type==='local' only, the installer recomputes sha256 of the .moext file at moextPath and compares it byte-for-byte to sourceInput.fileHash. A mismatch means the file on disk changed between the time the caller computed its hash and the install — the installer treats this as a tamper/integrity failure and deletes the staging dir.","triggerScenarios":"Caller passed {type:'local', absPath, fileHash} where fileHash was computed against an older version of the file; the file was rewritten/replaced between hash and stage() call; the hash string was truncated or for a different algorithm.","commonSituations":"A download manager overwrote the cached .moext after the hash was shown to the user; CI re-built the artifact between checksum and install; copy-paste of a hash from a different file.","solutions":["Recompute the sha256 of moextPath immediately before calling stage() and pass that fresh hash.","Ensure nothing (downloader, antivirus, build pipeline) mutates the file between hashing and staging.","Verify the hash is a full 64-char lowercase hex sha256 (see also error 191)."],"exampleFix":"// before — hash computed once, then file re-downloaded, now stale\nconst fileHash = sha256(oldFile)\nawait redownload(absPath)\nawait installer.stage(absPath, {type:'local',absPath,fileHash})\n// after — recompute at the boundary\nconst fileHash = createHash('sha256').update(await readFile(absPath)).digest('hex')\nawait installer.stage(absPath, {type:'local',absPath,fileHash})","handlingStrategy":"validation","validationCode":"import { createHash } from 'node:crypto'\nimport { readFile } from 'node:fs/promises'\n// compute hash AT the boundary, immediately before stage()\nconst fileHash = createHash('sha256').update(await readFile(moextPath)).digest('hex')\nawait installer.stage(moextPath, {type:'local', absPath: moextPath, fileHash})","typeGuard":null,"tryCatchPattern":"try { await installer.stage(moextPath, {type:'local',absPath,fileHash}) }\ncatch(e){ if(e instanceof AppError && e.message==='plugin.install.local_file_hash_mismatch'){ /* recompute hash, re-stage */ } else throw e }","preventionTips":["Never cache a local file hash across a re-download or filesystem write.","Compute the hash in the same tick you call stage().","Lock the source file (or copy to a temp path) between hashing and staging."],"tags":["plugin-install","integrity","sha256","local-source","tamper-detection"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}