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

  1. Log/inspect the exact url passed to the IPC — it is almost never an absolute http(s) URL
  2. Normalize links in the renderer before invoking: new URL(href, pageUrl), require protocol http:/https:
  3. Handle mailto/tel separately if you genuinely need them, by extending openExternalUrl's allow-list rather than bypassing it
  4. 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

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.