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
- 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-'.
- 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.
- Check for invisible mutations between hashing and loading: BOM, trailing newline, CRLF conversion, template-literal indirection.
- 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
- Always hash the exact runtime string, never the file on disk separately from the read
- Use standard base64 (not base64url/hex) with the 'sha256-' prefix
- Fetch payload and hash from the same source request so they cannot drift
- Beware line-ending and BOM normalization between hashing and loading
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
- ${caller}: illegal path traversal in "${path}"
- Not a directory: ${watchDir}
- Invalid external URL
- Invalid preview URL
- Loopback callback state mismatch (possible CSRF)
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/bebe4974962cc14b.
Report an issue: GitHub.