NousResearch/hermes-agent · warning · Error
Invalid external URL
Error message
Invalid external URL
What it means
The 'hermes:openExternal' IPC delegates to openExternalUrl(url), which validates the URL before handing it to the OS (shell.openExternal). Only approved schemes/targets (typically http/https) are allowed; anything else — javascript:, file://, malformed strings, non-URLs — makes the guard return false and this error is thrown. This is a security boundary: it prevents a compromised renderer from launching arbitrary schemes or executables.
Source
Thrown at apps/desktop/electron/main.ts:11349
})
})
// Primary renderer → main → quick window: gateway connection state + the
// recent-session list for the target picker. Cached so a quick window spawned
// AFTER the last push still boots from truth instead of "disconnected".
ipcMain.on('hermes:quick-entry:state', (_event, payload) => {
quickEntryLastState = payload ?? null
if (quickEntryWindow && !quickEntryWindow.isDestroyed()) {
quickEntryWindow.webContents.send('hermes:quick-entry:state', payload)
}
})
ipcMain.on('hermes:quick-entry:dismiss', () => hideQuickEntryWindow())
ipcMain.handle('hermes:openExternal', (_event, url) => {
if (!openExternalUrl(url)) {
throw new Error('Invalid external URL')
}
})
// ── Find-in-page (Ctrl/Cmd+F) ─────────────────────────────────────────────
// The desktop supports multiple BrowserWindows (one primary plus any
// per-session secondary windows spawned via `hermes:window:openSession`).
// Find must run against the requesting window, not a global — otherwise
// Cmd+F pressed in a secondary session window would search the primary
// and the match counter would report matches the user can't see. Resolve
// the sender through `BrowserWindow.fromWebContents(event.sender)` and
// forward `found-in-page` results back to that same sender.
// Lazily-installed forwarder per sender webContents. We track one
// uninstall fn per webContents id and prune entries when the sender goes
// away — Electron does not auto-detach webContents listeners on close,
// so the map is the cleanup path.
const foundInPageForwarders = new Map<number, () => void>()
View on GitHub (pinned to c896c09c42)
Solutions
- Log/inspect the exact url passed to the IPC — it is almost never an absolute http(s) URL
- Normalize links in the renderer before invoking: new URL(href, pageUrl), require protocol http:/https:
- Handle mailto/tel separately if you genuinely need them, by extending openExternalUrl's allow-list rather than bypassing it
- Do not catch-and-ignore: a false return means the URL was rejected by the security guard
Example fix
// before
const open = (href: string) => ipc.invoke('hermes:openExternal', href)
// after
const open = (href: string, base?: string) => {
const u = new URL(href, base)
if (u.protocol !== 'https:' && u.protocol !== 'http:') throw new Error(`Refusing to open ${u.protocol}`)
return ipc.invoke('hermes:openExternal', u.toString())
} Defensive patterns
Strategy: validation
Validate before calling
function safeExternalHref(href: string, base?: string): string | null {
try { const u = new URL(href, base); return (u.protocol === 'http:' || u.protocol === 'https:') ? u.toString() : null } catch { return null }
}
const url = safeExternalHref(href, window.location.href)
if (url) await ipc.invoke('hermes:openExternal', url) Type guard
function isHttpUrl(v: string): boolean { try { const u = new URL(v); return u.protocol === 'http:' || u.protocol === 'https:' } catch { return false } } Try / catch
catch (e) { if (e instanceof Error && e.message === 'Invalid external URL') console.warn('Blocked non-http(s) external URL:', url) else throw e } Prevention
- Normalize every link from untrusted content through new URL() before opening
- Reject javascript:, file:, and protocol-relative URLs at render time
- Never pass raw markdown/model output hrefs straight to openExternal
When it happens
Trigger: Renderer invokes hermes:openExternal with a non-http(s) URL (mailto:, file:///, javascript:), a relative path, undefined/null, or a malformed string; deep links built from untrusted content (markdown links, model output) that were not normalized first.
Common situations: Clicking links rendered from chat/model output that contain anchor-only (#...) or protocol-relative URLs; drag-pasted text being treated as a URL; renderer passing an event object instead of the URL string.
Related errors
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/158a71416fbce9e2.
Report an issue: GitHub.