Windows: main window left permanently always-on-top after the office-addin focus_desktop bridge runs
Windows: main window is left permanently always-on-top after the office-addin focus_desktop bridge runs
Summary
On Windows, the Claude desktop window intermittently becomes permanently
always-on-top and floats over every other application. There is no user-facing
setting or shortcut to turn it off, and nothing in the UI indicates the state — so
from the user's side it looks like an unfindable setting they toggled by accident.
The cause is a raise-to-front trick in the office-addin bridge that pins the window,
focuses it, and unpins it on a 100 ms timer. If that timer does not run, theWS_EX_TOPMOST flag is left set for the lifetime of the window.
Environment
- Claude desktop app 1.49585.0
- Windows 11 Enterprise, build 10.0.26100 (x64)
- Reproduced 2026-09-09. Heavy Office automation use (the relevant trigger).
Impact
Low severity, high annoyance, and effectively undiagnosable by a normal user:
- The window covers every other application, including full-screen apps.
- There is no setting, menu item, or shortcut to clear it (see "What was ruled out").
- The state is not persisted, so it survives until the app is restarted — but users
have no way to know a restart is the fix, because nothing tells them what changed.
Root cause
resources/app.asar, office-addin bridge, focus_desktop handler. Reformatted from
the minified bundle, with the surrounding branch for context:
// t.bP is the main BrowserWindow
if (process.platform === "win32") {
t.bP.setAlwaysOnTop(true);
t.bP.focus();
t.yP?.webContents.focus();
setTimeout(() => {
if (t.bP && !t.bP.isDestroyed()) t.bP.setAlwaysOnTop(false);
}, 100);
} else {
t.bP.focus();
t.yP?.webContents.focus();
}
The pin is the durable state and the unpin is the fragile one — the flag stays set if
the timer never fires. Ways that happens:
- Re-entrancy. A second
focus_desktopinside the 100 ms window pins again; the
first timer unpins, the second still pins — but any ordering where a pin lands
after the last unpin leaves it on. There is no pin counter or in-flight guard.
- Timer starvation. A busy main process can push the callback well past 100 ms,
widening the race.
- Any throw between the two calls abandons the pending unpin.
Confirming this is the only reachable path: the main window is constructed withalwaysOnTop:!1, and this is the only site in the bundle that sets it true on the
main window. Every other alwaysOnTop:!0 in app.asar belongs to a deliberately
floating auxiliary window (quick-entry, the transparent overlays) or to session
popouts, which expose it as real persisted state ({paneTileId, view, title, in
alwaysOnTop, bounds}session-popouts.json).
Reproduction
The race is timing-dependent, so the practical repro is the state itself. To
reproduce the stuck state directly:
# Any Electron path that leaves the flag set is equivalent to:
# win.setAlwaysOnTop(true) with the paired unpin never running
To observe it after it happens in the wild — this reads the real Win32 ex-style, so
it distinguishes a genuine topmost flag from the app merely stealing focus:
Add-Type -TypeDefinition 'using System;using System.Runtime.InteropServices;
public class W{[DllImport("user32.dll")]public static extern int GetWindowLong(IntPtr h,int i);}'
Get-Process claude | Where-Object { $_.MainWindowHandle -ne 0 } | ForEach-Object {
$ex = [W]::GetWindowLong($_.MainWindowHandle, -20) # GWL_EXSTYLE
'{0} exstyle=0x{1:X8} topmost={2}' -f $_.MainWindowTitle, $ex, (($ex -band 0x8) -ne 0)
}
Observed while stuck: Claude exstyle=0x00280108 topmost=True
Expected / after restart: Claude exstyle=0x00280100 topmost=False
Bit 0x8 is WS_EX_TOPMOST.
What was ruled out
Worth stating, because these are the answers a user will be given first and all three
are wrong:
- Not a user setting. No always-on-top key is persisted in
%APPDATA%\Claude\
— not config.json, window-state.json, Preferences, or Local Storage.
- Not a mis-keyed shortcut. The app's complete accelerator list (31 entries,
extracted from app.asar) contains no on-top toggle, and there is no menu item or
UI affordance for it. en-US.json has no matching label.
- Not PowerToys. Its Always on Top module (
Win+Ctrl+T) is the usual suspect
for this symptom on Windows; it was not installed on the affected machine. No other
window on the desktop carried WS_EX_TOPMOST.
Suggested fix
Make the unpin unconditional rather than timer-dependent. Any of:
- Reference-count the pin, and unpin only when the count returns to zero — fixes
the re-entrancy case directly.
- Unpin on the window's own
focusevent instead of a fixed delay, with the
timer only as a fallback. The pin exists to win the foreground race; once focus is
actually held it has served its purpose.
try/finallyaround the pin so a throw cannot strand it.- Assert on the way in: if
win.isAlwaysOnTop()is already true at the start of
focus_desktop and no popout has legitimately requested it, clear it first. This
is a cheap self-healing backstop regardless of which fix above is chosen.
Consider also whether the pin is needed at all — Electron'swin.moveTop() / app.focus({steal:true}) may be sufficient on modern Windows, andfocus({steal:true}) is already being called on the line above.
Workaround
Restart the app, or clear the flag in place without losing the session:
Add-Type -TypeDefinition 'using System;using System.Runtime.InteropServices;
public class T{[DllImport("user32.dll")]public static extern bool SetWindowPos(
IntPtr h,IntPtr a,int x,int y,int cx,int cy,uint f);}'
Get-Process claude | Where-Object { $_.MainWindowHandle -ne 0 } | ForEach-Object {
# HWND_NOTOPMOST(-2), SWP_NOSIZE|SWP_NOMOVE|SWP_NOACTIVATE(0x13)
[T]::SetWindowPos($_.MainWindowHandle, [IntPtr](-2), 0,0,0,0, 0x13)
}
Position, size, focus and the running session are all preserved.
---
*If this belongs on the desktop-app tracker rather than this repo, happy to move it —
please redirect.*
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗