BundledSoftware DevelopmentVersion 1.0.0

Inspecting the Live Hermes Desktop DOM via CDP

Read the live Hermes desktop DOM/CSS over CDP.

Written by Neura Market from the official Hermes Agent documentation for Inspecting Hermes Desktop Dom. Commands, paths, and version numbers are reproduced from the source unchanged.

Read the official documentation

When you are developing the Hermes desktop app and a user is running that same app, you can read the live rendered DOM of the window they are looking at: computed styles, geometry, which CSS rule actually won, console output. This skill gives you that ability over the Chrome DevTools Protocol (CDP), so you can answer factual questions about the UI without guessing from the source code. Reach for it when you need to verify a change took effect, find the winning CSS rule, or grab a stable selector, and you want to do it without disturbing the user's session.

What it does

Dev-server runs of the desktop app open a CDP port on 127.0.0.1:9222 automatically. The renderer is a Chromium page, so everything DevTools can read, a script can read. This skill provides the commands and a small client library to evaluate JavaScript in the live page, read computed styles, check for elements, and inspect the renderer console. It is a factual tool: it tells you what is rendered and which CSS rule applied, not whether the result looks good. Colour balance, spacing feel, and aesthetics still need human eyes or a screenshot.

Before you start

You need a running dev-server instance of the desktop app. The CDP port opens on 127.0.0.1:9222 for any dev-server run, and it is closed in exactly two cases, both defined in apps/desktop/electron/dev-cdp.ts:

  • packaged builds: always closed, and no environment value overrides it;
  • no HERMES_DESKTOP_DEV_SERVER: an unpackaged electron . against dist/ is how the packaged app gets smoke tested, so it behaves like one.

You can move the port with HERMES_DESKTOP_CDP_PORT (for example =9333) or disable it entirely with =off. Before doing anything else, check whether the port is open:

curl -s --max-time 3 http://127.0.0.1:${HERMES_DESKTOP_CDP_PORT:-9222}/json/version

Empty output means no port. Do not guess another port silently. Never relaunch the user's app to get a port: that destroys their session and their state. Instead, launch your own isolated instance as described below.

Reading the DOM

The one-liner for quick checks is apps/desktop/scripts/eval.mjs. From the apps/desktop directory, run:

cd apps/desktop
node scripts/eval.mjs "document.querySelectorAll('[data-slot]').length"

For multi-step work, use the shared client in scripts/perf/lib/cdp.mjs. It has target discovery and promise-aware eval, so you can connect to the right window and run several expressions in sequence:

import { CDP, SELECTORS } from './scripts/perf/lib/cdp.mjs'

const cdp = await CDP.connect({ port: 9222, match: '5174' })
const out = await cdp.eval(`JSON.stringify({
  radius: getComputedStyle(document.documentElement).getPropertyValue('--radius-scalar').trim(),
  composer: !!document.querySelector('[data-slot="composer-rich-input"]')
})`)
cdp.close()

SELECTORS in scripts/perf/lib/cdp.mjs holds the stable data-slot hooks for the composer, thread viewport, assistant message, turn pair, and profile rail. Prefer them over inventing a querySelector: they are updated as a unit when components move, so they stay correct across refactors.

The question this is best at: which rule won?

Editing every call site because a style "isn't applying" is the classic waste of time. Read the real node first to see what is actually happening. This example inspects a link inside an assistant message, collecting its own classes, the computed font weight, and the classes of up to six ancestors:

const el = document.querySelector('[data-slot="aui_assistant-message-root"] a')
JSON.stringify({
  ownClasses: el.className,
  weight: getComputedStyle(el).fontWeight,
  parents: (() => {
    const out = []
    let n = el
    while ((n = n.parentElement) && out.length < 6) out.push(n.className)
    return out
  })()
})

If the node carries no class of its own, the value is inherited: sweeping call sites will not fix it, and you need the ancestor rule. A plugin stylesheet, for example @tailwindcss/typography's prose a { font-weight: 500 }, routinely beats a utility class; override on the shared class, not at each usage.

Your own isolated instance

When there is no port, or you must not disturb the user's window, launch your own instance with a separate user data directory and home. This avoids Electron's single-instance lock and keeps it away from real sessions:

cd apps/desktop
HERMES_HOME=/tmp/cdp-probe-home \
HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 \
HERMES_DESKTOP_CDP_PORT=9333 \
  npx electron . --user-data-dir=/tmp/cdp-probe-userdata

The separate --user-data-dir dodges Electron's single-instance lock, so it cannot collide with a running hgui; the separate HERMES_HOME keeps it away from real sessions. Pick a port other than 9222 for the same reason. Run it in the background and kill it when done.

If you also want the perf harness, npm run perf:serve does the same with a temp HERMES_HOME baked in.

Pitfalls

  • Never kill the user's dev server or app to "free" anything. A mid-serve kill nukes Chromium's socket pool, and the resulting ERR_NETWORK_CHANGED gets blamed on whatever you just changed.
  • A throwaway HERMES_HOME has no backend. The app logs ECONNREFUSED for hermes:api and may exit on its own. The renderer still mounts and the DOM is readable: read promptly, and don't mistake a self-exited probe for a broken port. Chromium logs DevTools listening on ws://127.0.0.1:/… when it binds; that line is the proof the port opened.
  • Poll, don't probe once. A just-launched app needs a second or two before the port answers.
  • Never dump the whole DOM. The desktop renders hundreds of nodes and outerHTML will bury your context. Project down to a small JSON object inside the evaluated expression.
  • Pass match to CDP.connect. Without it you may attach to the pet overlay, quick-entry window, or a devtools target instead of the main window.
  • cdp.eval returns the value; raw Runtime.evaluate double-nests it (.result.result.value). Use the wrapper.
  • import.meta.env.DEV is true under vite dev in this repo. The note in apps/desktop/scripts/profile-typing-lag.md claiming otherwise is stale.

When not to use it

This skill is not for performance profiling or heap work: those belong to node-inspect-debugger and debugging-hermes-desktop. It also cannot answer "does this look right?" Aesthetics need the user's eyes or a screenshot.

Limits and gotchas

The port is only open for dev-server runs, and never for packaged builds. A throwaway HERMES_HOME has no backend, so the app may log ECONNREFUSED and exit on its own; the DOM is still readable, but you must read promptly. The whole DOM is huge, so always project to a small JSON object. Always pass match to CDP.connect to avoid attaching to the wrong target.

What pairs with this

This skill sits alongside node-inspect-debugger for Node-level debugging, systematic-debugging for a structured approach to finding root causes, and dogfood for testing the app yourself. Together they cover the full debugging workflow from renderer to backend.

Skills the docs pair this with

More Software Development skills