agalwood/Motrix · error · AppError
PLUGIN_MANIFEST_INVALID
PLUGIN_MANIFEST_INVALID
Error message
plugin.install.local_file_hash_mismatch
What it means
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.
Source
Thrown at src/core/plugin/install/plugin-installer.ts:188
'_staging',
`s_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
)
const { bundleSha256, manifestRaw } = await extractMoext(
moextPath,
stagingDir
)
const localFileHash =
sourceInput.type === 'local'
? createHash('sha256')
.update(await readFile(moextPath))
.digest('hex')
: null
if (
sourceInput.type === 'local' &&
sourceInput.fileHash !== localFileHash
) {
await rm(stagingDir, { recursive: true, force: true })
throw new AppError(
ErrorCode.PluginManifestInvalid,
'plugin.install.local_file_hash_mismatch'
)
}
let parsedManifest: PluginManifest
try {
const result = parseManifest(manifestRaw, {
hostVersion: this.opts.hostVersion,
})
parsedManifest = await resolveManifestForInstall(
result.manifest as PluginManifest,
stagingDir
)
} catch (e) {
await rm(stagingDir, { recursive: true, force: true })
throw e
}View on GitHub (pinned to 1a708ee577)
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).
Example fix
// before — hash computed once, then file re-downloaded, now stale
const fileHash = sha256(oldFile)
await redownload(absPath)
await installer.stage(absPath, {type:'local',absPath,fileHash})
// after — recompute at the boundary
const fileHash = createHash('sha256').update(await readFile(absPath)).digest('hex')
await installer.stage(absPath, {type:'local',absPath,fileHash}) Defensive patterns
Strategy: validation
Validate before calling
import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
// compute hash AT the boundary, immediately before stage()
const fileHash = createHash('sha256').update(await readFile(moextPath)).digest('hex')
await installer.stage(moextPath, {type:'local', absPath: moextPath, fileHash}) Try / catch
try { await installer.stage(moextPath, {type:'local',absPath,fileHash}) }
catch(e){ if(e instanceof AppError && e.message==='plugin.install.local_file_hash_mismatch'){ /* recompute hash, re-stage */ } else throw e } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/fa3607ba2d55dcc3.
Report an issue: GitHub.