How claude.ai's in-app topbar (hamburger / sidebar / search / nav / Cowork ghost) is wired up on Linux, why the upstream frameless-WCO config doesn't work on X11, and how the hybrid mode (system frame + in-app topbar shim) lands functional buttons at the cost of a stacked-bar layout.
Resolved 2026-04-29 via hybrid mode. Default
CLAUDE_TITLEBAR_STYLE is hybrid: native OS frame plus the
wco-shim that convinces claude.ai's bundle to render its in-app
topbar. Topbar buttons are clickable. The trade-off vs Windows is
a stacked layout (DE-drawn titlebar on top, in-app topbar below)
instead of Windows's combined single bar.
Modes:
| mode | frame | shim | layout | notes |
|---|---|---|---|---|
hybrid (default) |
system | active | stacked: OS bar + in-app bar | clickable ✓ |
native |
system | inactive | OS bar only | no in-app topbar |
hidden |
frameless | active | Windows-style single bar | clicks broken on X11 — kept for Wayland / future investigation |
The topbar is not bundled in app.asar. claude.ai's web app
inside the BrowserView renders it. Rendering is gated by an
independent stack — each gate must pass.
Every request to claude.ai/claude.com from the desktop shell
carries unconditional headers set in index.js:504876-504907:
anthropic-desktop-topbar: 1anthropic-client-platform: desktop_appanthropic-client-os-platform: <process.platform>(literallinux)
The topbar markup is delivered to Linux clients — this gate isn't load-bearing for our scenario.
index.js builds a feature-flag object via J0() (line 301965)
and passes it to the BrowserView via
webPreferences.additionalArguments=['--desktop-features=<JSON>'].
mainView.js parses the arg and exposes the parsed object via
contextBridge as window.desktopBootFeatures. The relevant key
desktopTopBar.status is "supported" on Linux, so this gate
also isn't load-bearing.
Load-bearing. The React bundle
(https://assets-proxy.anthropic.com/.../index-*.js) contains:
const HV = /(win32|win64|windows|wince)/i;
function WV() {
if (typeof window === "undefined") return false;
// ... HV.test(window.navigator.userAgent)
}This function and a sibling gate the topbar JSX. Linux's UA
contains X11; Linux x86_64, fails the regex, and React skips
rendering the entire <div class="draggable absolute top-0 ...">
topbar tree (note the topbar-windows-menu test ID — upstream
treats this as Windows-specific).
The shim's navigator.userAgent override appends " Windows"
page-side so the regex passes. HTTP request UA is unchanged so
analytics, anti-bot fingerprints, and the
anthropic-client-os-platform header stay honest.
On Linux X11 with frameless windows, this is what kills clicks in
hidden mode. The topbar's <div class="draggable absolute top-0 inset-x-0"> would normally trigger the CSS rule
.draggable { -webkit-app-region: drag }. On Windows, Chromium
hit-tests per pixel and child app-region: no-drag regions are
clickable; on Linux X11, Chromium pushes a drag-region map to the
WM as a region for _NET_WM_MOVERESIZE and the WM intercepts
mouse events before the page sees them. Critically: that map is
sticky — not refreshable from CSS, DOM mutations, setSize
jiggles, or hide/show cycles after first paint.
In hybrid mode (frame:true) this isn't an issue. The OS handles
window dragging via the native titlebar; Chromium doesn't push a
drag-region map for framed windows. The shim's className intercept
strips 'draggable' from any DOM class assignment as
belt-and-suspenders against the .draggable rule producing
surprise click-eaten regions inside the page.
Inlined into mainView.js by patch_wco_shim. Skipped in native
mode; active in hybrid (default) and hidden.
| component | role | load-bearing? |
|---|---|---|
| Native-state probes | Capture Chromium's WCO state for launcher.log diagnostics. Phase 1 syncs non-DOM values; Phase 2 reads env(titlebar-area-*) via custom-property indirection on DOMContentLoaded. Bypassed by CLAUDE_WCO_NATIVE=1. |
No (diagnostic) |
navigator.windowControlsOverlay shim |
Returns visible: true and synthesized rect. |
No (defensive — bundle grep shows no current use) |
matchMedia shim |
Returns matches: true for (display-mode: window-controls-overlay) queries. |
No (defensive — same) |
navigator.userAgent shim |
Appends " Windows" so Gate 3 passes. |
Yes |
| className intercept | Strips 'draggable' from any class assignment via Element.prototype.className, setAttribute, DOMTokenList.prototype.add overrides. Three vectors covered. |
Defensive (belt-and-suspenders) |
| Event nudge | Dispatches geometrychange + resize to wake any framework that rendered before the shim arrived. |
No (defensive) |
Two phases. Phase 1: render the topbar at all. Phase 2: figure out why the buttons don't fire mouse events. Phase 2 went through several false hypotheses before landing on hybrid.
Original assumption was WCO @media gating. Several wasted
attempts at activating WCO at the page level
(titleBarStyle:hidden + titleBarOverlay; explicit object form;
--enable-features=WindowControlsOverlay; native Wayland) all
failed at the time, leading to the empirical conclusion that
"Linux Electron doesn't activate WCO." Bundle probing eventually
surfaced Gate 3 (the UA regex). UA spoof made the topbar
render. The other shims stayed in as defensive forward-compat.
Six escape attempts at defeating the X11 drag-region map all failed:
- CSS override of
.draggabletono-drag !important— computed style flipped, clicks still broken MutationObserverstripping the class on attach — DOM correct, clicks broken- IPC-triggered
setSizejiggle — no effect setSize+ hide/show cycle — no effect- JS-side
programmaticClickFired: trueconfirmed — handlers wire correctly, problem is purely OS/WM-level - Preemptive global
.draggable { no-drag !important }from preload — no effect
All six targeted the .draggable class as the source. The 7th
attempt — a JS-DOM API intercept stripping 'draggable' from any
class assignment via Element.prototype overrides — also failed,
even though probes confirmed zero elements ended up with the
class. The drag region wasn't coming from .draggable at all.
With no element having computed app-region: drag yet clicks
still broken, the source had to be at the Electron/Chromium
config layer. Three diagnostic experiments narrowed it:
| experiment | result |
|---|---|
CLAUDE_TBO_HEIGHT=off (omit titleBarOverlay) |
clicks still broken |
CLAUDE_TBS_DISABLE=1 (also omit titleBarStyle:'hidden') |
clicks still broken |
frame: true (hybrid mode) |
clicks work |
So the source is frame: false itself, not anything we can
configure at the Electron API level. Chromium-Linux-X11 has a
hardcoded behavior that creates an implicit drag region for the
top of frame: false windows. The fix is to not be frameless.
Hybrid trades a stacked layout for clickability.
Two unrelated Linux-X11 / Electron 41 / Chromium 146 issues surfaced during the investigation. Worth filing if someone has time. Bug A is the most actionable.
In the main window webContents of a frame:false + titleBarStyle:'hidden' + titleBarOverlay:{...} BrowserWindow,
runtime probe 2026-04-29:
| signal | value |
|---|---|
navigator.windowControlsOverlay.visible |
true |
windowControlsOverlay.getTitlebarAreaRect() |
1131×40 (matches config) |
env(titlebar-area-width) (via custom-property indirection) |
1131px (matches) |
matchMedia('(display-mode: window-controls-overlay)').matches |
false ✗ |
Three of four WCO entry points agree; only the documented @media
detection point is broken.
Minimal repro after did-finish-load:
const wco = navigator.windowControlsOverlay;
const r = wco.getTitlebarAreaRect();
const s = document.createElement('style');
s.textContent = ':root { --w: env(titlebar-area-width) }';
document.head.appendChild(s);
({
visible: wco.visible, // true
rect: { width: r.width, height: r.height }, // populated
cssEnvWidth: getComputedStyle(document.documentElement)
.getPropertyValue('--w'), // populated
mediaQueryMatches:
matchMedia('(display-mode: window-controls-overlay)').matches, // false
});Same parent BrowserWindow, probing the BrowserView instead:
| signal | value |
|---|---|
navigator.windowControlsOverlay.visible |
false |
getTitlebarAreaRect() |
0×0 |
env(titlebar-area-width) |
empty |
matchMedia('(display-mode: window-controls-overlay)').matches |
false |
The BrowserView sees nothing. May be intentional isolation (each webContents independent) — could be working-as-designed and not worth filing. Means any WCO-aware page hosted in a BrowserView never sees WCO regardless of parent config.
The root cause of the hidden-mode click problem. Investigation
ruled out .draggable, titleBarOverlay, and titleBarStyle as
the source — what remains is some hardcoded behavior in
Chromium's ozone backend that creates a non-overridable drag
region for the top of frameless windows. Confirmed present on
both X11 and Wayland (2026-04-29): running
CLAUDE_USE_WAYLAND=1 CLAUDE_TITLEBAR_STYLE=hidden produces the
same unclickable topbar as X11, ruling out a Wayland-only
shipping path. Characterizing this as a filable bug would
require source-level inspection of ui/ozone/platform/{x11,wayland}/.
The combined impact of A + B + C is that WCO is effectively
unusable on Linux today.
- Wayland-only shipping (ruled out 2026-04-29). Wayland WCO
landed in Electron 38.2 / 41 with apparently fuller support
(Electron Wayland tech talk),
raising the possibility that hidden mode might work on native
Wayland even though X11 is broken. Tested with
CLAUDE_USE_WAYLAND=1 CLAUDE_TITLEBAR_STYLE=hidden: topbar clicks are still unresponsive. The implicit drag region (Bug C) exists on both backends. Hybrid is the answer everywhere. - Bundle rewriting via
session.protocol.handle()— was the proposed last-resort path before hybrid worked. Would intercept claude.ai's React bundle and regex-replaceclass="draggable absolute top-0to remove thedraggabletoken before Chromium parses it. Now obsolete given hybrid; documented for posterity.
scripts/wco-shim.js— shim sourcescripts/patches/wco-shim.sh— inlines shim into mainView.jsscripts/frame-fix-wrapper.js— main-process BrowserWindow patching, mode resolution, diagnostic probesscripts/launcher-common.sh— Chromium feature flags per modescripts/doctor.sh—--doctorreports the resolved titlebar style (PASSforhybrid/native,WARNforhiddenwith a pointer to the working modes,WARN+ valid-value hint for unrecognized values)tests/launcher-common.bats— covers_resolve_titlebar_style(default + each mode + case-insensitivity + invalid fallback),build_electron_argsflag selection per mode, andsetup_electron_envELECTRON_USE_SYSTEM_TITLE_BARwiring per mode. Shim runtime behavior (className intercept, UA spoof) is not unit-tested — verified empirically via the click test in this docdocs/CONFIGURATION.md— user-facing env-var docs
(async () => {
const reactBundle = [...document.scripts]
.map(s => s.src).filter(Boolean)
.find(s => /index-[A-Za-z0-9]+\.js/.test(s));
const text = await (await fetch(reactBundle)).text();
const ctx = (term, len = 200) => {
const i = text.indexOf(term);
return i < 0 ? null : text.slice(Math.max(0, i - len), i + term.length + len);
};
return {
bundleSize: text.length,
ctx_topbar_windows: ctx('topbar-windows'),
ctx_isWindows_regex: ctx('win32|win64'),
ctx_desktopTopBar: ctx('desktopTopBar'),
ctx_windowControlsOverlay: ctx('windowControlsOverlay'),
};
})();Inspect the regex pattern, gate variable names, and any new condition strings. The shim probably needs an update if any of those move.
Should return [] in hybrid mode (className intercept strips the
class). If it returns elements, the intercept missed a vector
(e.g. dangerouslySetInnerHTML, parser-set classes) — investigate
where the class came from.
[...document.querySelectorAll('*')].filter(el =>
getComputedStyle(el).webkitAppRegion === 'drag'
).map(el => ({
tag: el.tagName,
cls: (el.className || '').toString().slice(0, 100),
rect: el.getBoundingClientRect().toJSON(),
}));Confirms a click problem is OS-level rather than CSS or JS:
const hamburger = document.querySelector('[data-testid="topbar-windows-menu"]');
const topbar = document.querySelector('div.absolute.top-0.inset-x-0');
const ts = getComputedStyle(topbar);
const hs = getComputedStyle(hamburger);
let clickFired = false;
hamburger.addEventListener('click', () => { clickFired = true; }, { once: true });
hamburger.click();
const r = hamburger.getBoundingClientRect();
const elemAtCenter = document.elementFromPoint(r.x + r.width/2, r.y + r.height/2);
({
topbarAppRegion: ts.webkitAppRegion,
hamburgerAppRegion: hs.webkitAppRegion,
topbarPointerEvents: ts.pointerEvents,
hamburgerPointerEvents: hs.pointerEvents,
programmaticClickFired: clickFired,
hitIsHamburgerOrDescendant: hamburger.contains(elemAtCenter),
});When this looks correct (no-drag, auto, true, true) but
real mouse clicks don't fire, the click is being intercepted at
the WM level — same failure mode as the hidden-mode investigation.
- DOM probes that search
[class*="topbar" i]orheader[role="banner"]won't find the topbar. It identifies viadata-testid="topbar-windows-menu"and usesclass="draggable absolute top-0 ...". Search bydata-testidfirst. - A relative
require('./wco-shim.js')from the sandboxed preload aborts the entire preload because sandboxed preloads can only require an allowlist (electron,ipcRenderer,contextBridge,webFrame, ...). The shim must be inlined into mainView.js, not pulled in via require. webFrame.executeJavaScriptmay fire beforedocument.documentElementexists. Probe code that callsgetComputedStyle(document.documentElement)immediately throws "parameter 1 is not of type 'Element'". Defer toDOMContentLoadedif needed.
