# NousResearch/hermes-agent · Desktop app auto-previews links causing unwanted side effects

## Open now

- 1 issue, 0 PRs open on GitHub.
- #47500: \[Bug\]: Desktop app auto-previews external links in chat, triggering custom protocol handlers (e.g. bitbrowser://)

These issues concern the desktop app automatically previewing or loading external links in chat, which can trigger unintended behaviors such as audio autoplay and custom protocol handler activation.

## TL;DR

- 3 member(s): 1 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 0 PR(s) closed without merging.
- A fix was merged to main in PR #49515. Verify it addresses your case.
- **Recommended:** read PR #49515 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-49515`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.45`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #47500: \[Bug\]: Desktop app auto-previews external links in chat, triggering custom protocol handlers (e.g. bitbrowser://)

## Closed / duplicate issues

- #49505: \[Bug\]: Desktop app auto-plays audio from YouTube links

## Prior & competing PRs

- **#49515: fix(desktop): mute hidden link-title window so historical links don't autoplay audio**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `apps/desktop/electron/link-title-window.cjs`, `apps/desktop/electron/link-title-window.test.cjs`, `apps/desktop/electron/main.cjs`, `apps/desktop/package.json`
  - fix hunks:
  - `apps/desktop/electron/link-title-window.cjs`

    ```diff
    @@ -0,0 +1,42 @@
    +'use strict'
    +
    +// Hidden BrowserWindow used by tier-2 link-title resolution: when curl can't
    +// read a page <title> (bot walls, JS-rendered pages), we briefly load the URL
    +// in an offscreen window and read its title. That window loads arbitrary
    +// user-linked pages — including YouTube/`watch` URLs that autoplay — so it must
    +// never be allowed to emit sound.
    +
    +function linkTitleWindowOptions(partitionSession) {
    +  return {
    +    show: false,
    +    width: 1280,
    +    height: 800,
    +    webPreferences: {
    +      backgroundThrottling: false,
    +      contextIsolation: true,
    +      javascript: true,
    +      nodeIntegration: false,
    +      sandbox: true,
    +      session: partitionSession,
    +      webSecurity: true
    +    }
    +  }
    +}
    +
    +// Create the offscreen title-fetch window and immediately mute it. Without the
    +// mute, autoplaying media on the loaded page (e.g. a YouTube link) leaks ~2s of
    +// audio every time a session containing such links is re-rendered. See #49505.
    +function createLinkTitleWindow(BrowserWindow, partitionSession) {
    +  const window = new BrowserWindow(linkTitleWindowOptions(partitionSession))
    +
    +  try {
    +    window.webContents.setAudioMuted(true)
    +  } catch {
    +    // webContents may be unavailable in degraded/headless environments; muting
    +    // is best-effort and the window is destroyed within a few seconds anyway.
    +  }
    +
    +  return window
    +}
    +
    +module.exports = { createLinkTitleWindow, linkTitleWindowOptions }
    ```
  - `apps/desktop/electron/link-title-window.test.cjs`

    ```diff
    @@ -0,0 +1,56 @@
    +const assert = require('node:assert/strict')
    +const test = require('node:test')
    +
    +const { createLinkTitleWindow, linkTitleWindowOptions } = require('./link-title-window.cjs')
    +
    +function makeFakeBrowserWindow() {
    +  const calls = { audioMuted: [] }
    +  const FakeBrowserWindow = function (options) {
    +    this.options = options
    +    this.webContents = {
    +      setAudioMuted(value) {
    +        calls.audioMuted.push(value)
    +      }
    +    }
    +  }
    +
    +  return { FakeBrowserWindow, calls }
    +}
    +
    +test('linkTitleWindowOptions keeps the offscreen, hardened defaults', () => {
    +  const session = { id: 'link-titles' }
    +  const options = linkTitleWindowOptions(session)
    +
    +  assert.equal(options.show, false)
    +  assert.equal(options.webPreferences.session, session)
    +  assert.equal(options.webPreferences.contextIsolation, true)
    +  assert.equal(options.webPreferences.sandbox, true)
    +  assert.equal(options.webPreferences.nodeIntegration, false)
    +})
    +
    +test('createLinkTitleWindow mutes audio so historical links never autoplay sound', () => {
    +  // Regression for #49505: the hidden title-fetch window loaded YouTube/watch
    +  // URLs (to read their <title>) without muting, leaking ~2s of audio on every
    +  // history re-render.
    +  const { FakeBrowserWindow, calls } = makeFakeBrowserWindow()
    +
    +  const window = createLinkTitleWindow(FakeBrowserWindow, { id: 'link-titles' })
    +
    +  assert.ok(window instanceof FakeBrowserWindow)
    +  assert.deepEqual(calls.audioMuted, [true])
    +})
    +
    +test('createLinkTitleWindow still returns the window if muting throws', () => {
    +  const ThrowingBrowserWindow = function (options) {
    +    this.options = options
    +    this.webContents = {
    +      setAudioMuted() {
    +        throw new Error('webContents unavailable')
    +      }
    +    }
    +  }
    +
    +  const window = createLinkTitleWindow(ThrowingBrowserWindow, { id: 'link-titles' })
    +
    +  assert.ok(window instanceof ThrowingBrowserWindow)
    +})
    ```
  - `apps/desktop/electron/main.cjs`

    ```diff
    @@ -34,6 +34,7 @@ const {
       SESSION_WINDOW_MIN_WIDTH
     } = require('./session-windows.cjs')
     const { canImportHermesCli, verifyHermesCli } = require('./backend-probes.cjs')
    +const { createLinkTitleWindow } = require('./link-title-window.cjs')
     const { probeGatewayWebSocket } = require('./gateway-ws-probe.cjs')
     const { adoptServedDashboardToken } = require('./dashboard-token.cjs')
     const { waitForDashboardPort } = require('./backend-ready.cjs')
    ```

    ```diff
    @@ -2980,20 +2981,7 @@ function runRenderTitleJob(rawUrl) {
         }
     
         try {
    -      window = new BrowserWindow({
    -        show: false,
    -        width: 1280,
    -        height: 800,
    -        webPreferences: {
    -          backgroundThrottling: false,
    -          contextIsolation: true,
    -          javascript: true,
    -          nodeIntegration: false,
    -          sandbox: true,
    -          session: partitionSession,
    -          webSecurity: true
    -        }
    -      })
    +      window = createLinkTitleWindow(BrowserWindow, partitionSession)
         } catch {
           return finish('')
         }
    ```
  - `apps/desktop/package.json`

    ```diff
    @@ -37,7 +37,7 @@
         "test:desktop:nsis": "node scripts/test-desktop.mjs nsis",
         "test:desktop:existing": "node scripts/test-desktop.mjs existing",
         "test:desktop:fresh": "node scripts/test-desktop.mjs fresh",
    -    "test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/update-rebuild.test.cjs electron/windows-user-env.test.cjs",
    +    "test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/link-title-window.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/update-rebuild.test.cjs electron/windows-user-env.test.cjs",
         "typecheck": "tsc -p . --noEmit",
         "lint": "eslint src/ electron/",
         "lint:fix": "eslint src/ electron/ --fix",
    ```

## Fix status

- Merged to main in PR #49515 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-20`.
- Subsystem: `apps/desktop`
