NousResearch/hermes-agent · error

integrity check failed for ${origin}

Error message

integrity check failed for ${origin}

What it means

Thrown by the desktop runtime-plugin loader (apps/desktop/src/contrib/runtime-loader.ts:117) when a caller passes an `integrity` option (an SRI-style `sha256-<base64>` hash) and the SHA-256 of the plugin source bytes does not match. The loader evaluates plugins as raw ESM code in the renderer with full app authority, so the hash is the only transport-level guarantee that the bytes are the ones you intended to ship. Note verifyIntegrity() returns false both for a genuine byte mismatch and for a malformed integrity string (wrong algorithm prefix or empty hash).

Source

Thrown at apps/desktop/src/contrib/runtime-loader.ts:117

  return actual === expected
}

export function unloadRuntimePlugin(id: string): void {
  loaded.get(id)?.forEach(dispose => dispose())
  loaded.delete(id)
}

/** Evaluate + register one runtime plugin. Returns its id, or null on failure. */
export async function loadRuntimePlugin(
  source: string,
  origin: string,
  options: LoadOptions = {}
): Promise<null | string> {
  installPluginSdk()

  try {
    if (options.integrity && !(await verifyIntegrity(source, options.integrity))) {
      throw new Error(`integrity check failed for ${origin}`)
    }

    const unsupported = unsupportedImports(source)

    if (unsupported.length > 0) {
      throw new Error(
        `unsupported import${unsupported.length > 1 ? 's' : ''}: ${unsupported.join(', ')} — ` +
          `runtime plugins may only import @hermes/plugin-sdk and react`
      )
    }

    const url = URL.createObjectURL(new Blob([rewriteSpecifiers(source)], { type: 'text/javascript' }))

    let mod: { default?: HermesPlugin }

    try {
      mod = await import(/* @vite-ignore */ url)
    } finally {

View on GitHub (pinned to c896c09c42)

Solutions

  1. Recompute the digest over the exact string you pass as `source`: sha256 of its UTF-8 bytes, then standard base64 (not base64url, not hex), prefixed 'sha256-'.
  2. If the source is legitimately different from what was hashed (file edited on disk), regenerate or remove the integrity option for local trusted disk plugins.
  3. Check for invisible mutations between hashing and loading: BOM, trailing newline, CRLF conversion, template-literal indirection.
  4. If you control a remote/allowlist pipeline, keep the hash and the payload fetched from the same request so they cannot drift.

Example fix

// before — hash computed over the file on disk, source read earlier
const src = await fs.readFile(pluginPath, 'utf8')
await loadRuntimePlugin(src, name, { integrity: recordedHash }) // may fail if file changed

// after — hash the exact bytes you load, standard base64 SRI
const src = await fs.readFile(pluginPath, 'utf8')
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(src))
const integrity = 'sha256-' + btoa(String.fromCharCode(...new Uint8Array(digest)))
await loadRuntimePlugin(src, name, { integrity })
Defensive patterns

Strategy: validation

Validate before calling

async function sriSha256(source: string): Promise<string> {
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(source))
  return 'sha256-' + btoa(String.fromCharCode(...new Uint8Array(digest)))
}
// before loading: compute from the SAME string you pass
const integrity = await sriSha256(source)
if (integrity !== expectedHash) throw new Error('plugin bytes drifted from manifest')

Type guard

function isValidSri(v: string): v is string {
  return /^sha256-[A-Za-z0-9+/]{43}$/.test(v) // 32 bytes -> 43 base64 chars, no padding
}

Try / catch

try { const id = await loadRuntimePlugin(src, origin, { integrity }) } catch (e) { if (e instanceof Error && e.message.startsWith('integrity check failed')) { /* regenerate hash from current bytes, or refuse to load remote code */ } throw e }

Prevention

When it happens

Trigger: Calling loadRuntimePlugin(source, origin, { integrity }) where integrity is 'sha256-<base64>' computed over DIFFERENT bytes than `source` (e.g. hash of the file on disk while passing a cached/edited string, or CRLF vs LF line endings differences); passing a base64url-encoded hash instead of standard base64; passing 'sha384-...' or a hash without the 'sha256-' prefix; trailing newline appended to or stripped from the source after hashing.

Common situations: Agent rewrites a plugin.js file and the watcher reloads it with a stale integrity recorded from the previous version; a build step normalizes line endings between hash time and load time; copy-pasting an SRI hash from a manifest computed over the minified artifact while loading the unminified source; hash generated with a different encoding (hex or base64url) than the standard SRI base64 the loader expects.

Related errors


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/bebe4974962cc14b. Report an issue: GitHub.