← Blog

How to let Claude Code see your Electron app

10 min readMilind Soni

You tell Claude Code the sidebar in your Electron app is overlapping the titlebar. It edits the renderer, Vite reports a clean rebuild, and the agent replies "Fixed the overlap" — while the window on your screen looks exactly as broken as before.

The agent isn't lying. It genuinely cannot see the window, and Electron apps are blind spots in a specific, fixable way that most write-ups miss. Electron itself is having a discourse moment — Why is Claude an Electron app? did the rounds precisely because so many of the apps we build and use are Electron — but for working developers the practical question isn't why your app is Electron. It's how your agent can see it.

An Electron app is invisible to accessibility-based agents by default. Chromium doesn't build its accessibility tree until assistive technology asks for it, so an agent walking the macOS AX tree finds a window with nothing inside. Setting one attribute — AXManualAccessibility — makes the entire DOM materialize as text.

This post covers the three ways to close the loop: the Chrome DevTools Protocol, the accessibility flip, and pixels — and when each one is the right tool.

Vite says compiled. The window is broken.

The failure mode is always the same. The agent has perfect knowledge of the code and zero knowledge of the render. It edits a CSS module, watches the dev server log hmr update, and treats "compiled without errors" as "looks correct." Those are different claims. A build tool verifies syntax and imports; it says nothing about a dropdown clipping behind a z-index, a flex container collapsing to zero height, or a white-on-white button.

For web apps, agents patch this gap with a browser they control. But your Electron app isn't running in the agent's browser — it's a native macOS window owned by another process. Until you hand the agent a way in, every UI claim it makes is a guess. (This is the general problem of giving Claude Code eyes; Electron is the special case where the obvious door is locked.)

Why agents are blind to Electron apps

There are three layers an agent could read, and each fails differently by default.

Pixels. Screenshots need macOS Screen Recording permission for whatever process captures them, and they capture what's literally on screen — an occluding window, a notification, or your terminal sitting on top of the app all end up in the frame. Pixels also arrive with no structure: the agent gets colors, not elements.

The accessibility tree. This is the layer that surprises people. macOS exposes every native app's UI as a tree of elements (roles, names, bounds) that any process with Accessibility permission can walk. Native apps get this for free from AppKit. Chromium — and therefore Electron — does not: for performance reasons, Chromium waits until it detects assistive technology before building full accessibility support. Mirroring every DOM mutation into the platform tree costs real work, and most users never need it, so the tree simply isn't built. An agent that queries your Electron window over the AX API gets a window element with essentially nothing inside — and may wrongly conclude the app is a canvas blob.

The DOM. It exists and it's perfect — but it lives inside Chromium, reachable only through a debug port you must open on purpose.

So the agent's three senses are: permission-gated unstructured pixels, an empty tree, and a locked door. Let's unlock things.

Path A: Chrome DevTools Protocol

The best-known approach — and the one covered in Juri Strumpflohner's visual feedback loop for Electron apps with Claude Code (March 2026) — is launching the app with a remote debugging port:

# dev
electron . --remote-debugging-port=9222

# or a packaged app
open -a "YourApp" --args --remote-debugging-port=9222

# confirm the endpoint is listening
curl -s http://127.0.0.1:9222/json/version

Any CDP-speaking MCP server (chrome-devtools-mcp, Playwright MCP) can then attach to that port, and Claude Code gets screenshots of the web contents, the live DOM, console output, and script evaluation. For renderer work in development, this is genuinely good: the agent can query getBoundingClientRect() instead of eyeballing pixels.

Honest limits:

  • CDP sees only the web contents. Native menus, dialog module dialogs, the tray icon, and OS-drawn chrome don't exist in the DOM. Your File menu is invisible to it.
  • Input is synthetic. CDP dispatches events into the page, not through the OS event path — dragging a -webkit-app-region titlebar, global shortcuts, and native drag-and-drop aren't exercised.
  • The port is an unauthenticated control channel. Anything on your machine can attach and evaluate JavaScript in a process that may have Node integration. Fine on a dev machine, never in a shipped build.

Path B: force the accessibility tree with one attribute

The path almost nobody writes about. In September 2017, Electron merged PR #10305, adding a macOS-specific attribute called AXManualAccessibility. Set it on the app's AXUIElement from any process with Accessibility permission, and Electron tells Chromium to build the full tree — no VoiceOver required. It's in the official accessibility docs to this day.

import AppKit
import ApplicationServices

// Your process needs Accessibility permission
// (System Settings → Privacy & Security → Accessibility).
let apps = NSRunningApplication.runningApplications(
  withBundleIdentifier: "com.yourcompany.yourapp")
guard let app = apps.first else { fatalError("app not running") }

let axApp = AXUIElementCreateApplication(app.processIdentifier)
let result = AXUIElementSetAttributeValue(
  axApp, "AXManualAccessibility" as CFString, kCFBooleanTrue)
print(result == .success ? "AX tree enabled" : "failed: \(result.rawValue)")

The effect is dramatic: the renderer's DOM materializes in the system tree as text. Buttons, text fields, headings, values — with names pulled from your ARIA attributes — become readable by any AX client, including generic MCP servers like Peekaboo or macos-use. The agent can now diff "what the window says" before and after a change without spending a single image token. (For the full comparison, see accessibility tree vs screenshots.)

If you control the app's code, the same switch exists from the inside:

// main process — gate it, since building the tree has a real cost
if (process.env.AGENT_AX === "1") {
  app.setAccessibilitySupportEnabled(true);
}

Two traps worth knowing:

Don't use AXEnhancedUserInterface. VoiceOver signals its presence by setting that attribute, and Chromium honors it — but it's notorious for breaking window positioning system-wide. A 2020 Apple developer forums thread shows window managers like Magnet misbehaving the moment it's set; that side effect is precisely why Electron introduced the separate AXManualAccessibility attribute.

Old Electrons reject the write. Across the Electron 19–22 era, setting AXManualAccessibility externally returned kAXErrorAttributeUnsupported (-25205) — the app handled the attribute but never advertised it (issue #37465). The fix landed in PR #38102 (April 2023, backported to 23.x–25.x). Even on current versions the attribute doesn't appear in accessibilityAttributeNames, so don't probe for it — set it and check whether children appear.

Path C: pixels and pointing, for what the tree can't say

Some things never make it into any tree: a <canvas>-rendered terminal, a WebGL viewport, custom-drawn charts, or simply "this looks wrong" judgments about spacing and color. The broader ecosystem has the same hole — the Screen2AX paper (MacPaw and Ukrainian Catholic University, 2025) found only 33% of macOS apps offer full accessibility support, and showed that vision-generated trees improved agent task performance 2.2× over what those apps natively expose. When structure is missing, pixels are the fallback of record — and the OSWorld benchmark found accessibility tree plus screenshot hits 69.2–71.8% relative task success vs 56.4% for Set-of-Mark screenshots alone. You want both layers, not either.

This is the part we work on. We build SupaMaus Lite, a macOS menu-bar app that runs a local MCP server and hands agents spatial context: you hold Control+Option and circle the broken region of your Electron window while saying "this dropdown clips under here," and Claude Code receives the annotated screenshot, the gesture trail, the voice transcript (transcribed on-device with WhisperKit), and the AX elements under your gesture — role, name, and bounds. For Electron specifically, SupaMaus performs the AXManualAccessibility flip automatically when it reads a window, so full window text comes along even from apps that were empty a second earlier. And since Electron apps are web tech, its Chrome extension pairs naturally during development: run the same UI in a browser tab and captures include DOM truth — selector, React component name, and source file:line.

What a screenshot costs in tokens

Claude's vision pricing is roughly tokens ≈ width × height ÷ 750. A default MacBook-scaled Electron window at 1512×982 costs about 1,980 tokens per look; check the window after 25 edits and you've spent ~50K tokens on mostly identical pixels. Tooling overhead makes it worse in practice — one measured Playwright MCP browser_take_screenshot call weighed in at 232K tokens, and a typical Playwright MCP session runs ~114K tokens.

The AX tree rendered as text is a few kilobytes, and — unlike an image — it diffs. "Same tree except the Save button's title changed" is a one-line observation instead of a 2,000-token re-look. Spend image tokens when the question is visual; read text when the question is structural.

The verification loop

The recipe that makes "fixed" mean fixed:

  1. Launch the app with the debug port (dev) or flip AXManualAccessibility (any build).
  2. After every UI change: rebuild, then capture one screenshot and read the AX tree (or DOM via CDP).
  3. Have the agent state what it expected to change, and verify against what it read — before it declares victory.

When the loop misbehaves:

| Symptom | Cause | Fix | | --- | --- | --- | | Window in the tree, but no children | Chromium never built the tree | Set AXManualAccessibility on the app element | | Set call returns kAXErrorAttributeUnsupported | Electron 19–22 bug (#37465) | Upgrade past the #38102 fix, or call app.setAccessibilitySupportEnabled(true) inside | | Stale or erroring elements after reload/navigation | cached AXUIElement refs died with their DOM nodes | Re-walk from the app element after every change; treat refs as ephemeral | | Agent reads the wrong window | multi-window app | Enumerate AXWindows, match on AXTitle and frame first | | Window snapping breaks for the app | you set AXEnhancedUserInterface | Unset it; use AXManualAccessibility |

FAQ

Why can't Claude Code see my Electron app? Three separate reasons: screenshots require Screen Recording permission and capture occlusions; the accessibility tree is empty because Chromium defers building it until assistive technology appears; and the DOM is only reachable through a --remote-debugging-port you haven't opened. Fix any one layer and the agent can start verifying its own work.

What is AXManualAccessibility? A macOS-specific attribute Electron added in PR #10305 (2017). Setting it to true on an Electron app's AXUIElement — from any process with Accessibility permission — forces Chromium to build its accessibility tree without VoiceOver running, exposing the whole DOM as readable elements. It exists because the alternative, AXEnhancedUserInterface, breaks window positioning.

Should I use CDP or the accessibility tree? Both, for different questions. CDP (dev builds only) gives the live DOM, console, and synthetic input — best for renderer debugging. The AX tree works on packaged apps, includes nothing you must ship, and costs a few KB per read instead of ~2K tokens per screenshot. Keep screenshots for genuinely visual judgments.

Does enabling accessibility slow the app down? That's exactly why Chromium ships with it off: every DOM mutation gets mirrored into the platform tree. The overhead is acceptable for a dev session on a typical app, but gate it — an env-var check around app.setAccessibilitySupportEnabled(true), or an external flip only while an agent is working — rather than enabling it unconditionally in production.