{"record":{"id":"158a71416fbce9e2","repo":"NousResearch/hermes-agent","slug":"invalid-external-url","errorCode":null,"errorMessage":"Invalid external URL","messagePattern":"Invalid external URL","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"apps/desktop/electron/main.ts","lineNumber":11349,"sourceCode":"  })\n})\n\n// Primary renderer → main → quick window: gateway connection state + the\n// recent-session list for the target picker. Cached so a quick window spawned\n// AFTER the last push still boots from truth instead of \"disconnected\".\nipcMain.on('hermes:quick-entry:state', (_event, payload) => {\n  quickEntryLastState = payload ?? null\n\n  if (quickEntryWindow && !quickEntryWindow.isDestroyed()) {\n    quickEntryWindow.webContents.send('hermes:quick-entry:state', payload)\n  }\n})\n\nipcMain.on('hermes:quick-entry:dismiss', () => hideQuickEntryWindow())\n\nipcMain.handle('hermes:openExternal', (_event, url) => {\n  if (!openExternalUrl(url)) {\n    throw new Error('Invalid external URL')\n  }\n})\n\n// ── Find-in-page (Ctrl/Cmd+F) ─────────────────────────────────────────────\n// The desktop supports multiple BrowserWindows (one primary plus any\n// per-session secondary windows spawned via `hermes:window:openSession`).\n// Find must run against the requesting window, not a global — otherwise\n// Cmd+F pressed in a secondary session window would search the primary\n// and the match counter would report matches the user can't see. Resolve\n// the sender through `BrowserWindow.fromWebContents(event.sender)` and\n// forward `found-in-page` results back to that same sender.\n\n// Lazily-installed forwarder per sender webContents. We track one\n// uninstall fn per webContents id and prune entries when the sender goes\n// away — Electron does not auto-detach webContents listeners on close,\n// so the map is the cleanup path.\nconst foundInPageForwarders = new Map<number, () => void>()\n","sourceCodeStart":11331,"sourceCodeEnd":11367,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/main.ts#L11331-L11367","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst open = (href: string) => ipc.invoke('hermes:openExternal', href)\n\n// after\nconst open = (href: string, base?: string) => {\n  const u = new URL(href, base)\n  if (u.protocol !== 'https:' && u.protocol !== 'http:') throw new Error(`Refusing to open ${u.protocol}`)\n  return ipc.invoke('hermes:openExternal', u.toString())\n}","handlingStrategy":"validation","validationCode":"function safeExternalHref(href: string, base?: string): string | null {\n  try { const u = new URL(href, base); return (u.protocol === 'http:' || u.protocol === 'https:') ? u.toString() : null } catch { return null }\n}\nconst url = safeExternalHref(href, window.location.href)\nif (url) await ipc.invoke('hermes:openExternal', url)","typeGuard":"function isHttpUrl(v: string): boolean { try { const u = new URL(v); return u.protocol === 'http:' || u.protocol === 'https:' } catch { return false } }","tryCatchPattern":"catch (e) { if (e instanceof Error && e.message === 'Invalid external URL') console.warn('Blocked non-http(s) external URL:', url) else throw e }","preventionTips":["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"],"tags":["security","url-validation","ipc","desktop","validation"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}