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

  1. Recompute the sha256 of moextPath immediately before calling stage() and pass that fresh hash.
  2. Ensure nothing (downloader, antivirus, build pipeline) mutates the file between hashing and staging.
  3. 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

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.