Problem
On Linux, Claude Desktop exits when the last window is closed. The tray icon (created by the existing patches in scripts/patches/tray.sh) disappears with it. This surprises users who believe they've minimized to tray, and silently breaks in-app features that depend on the runtime staying alive — notably the /schedule skill's cron-style scheduler, which fires from setTimeouts inside the main Electron process.
Repro
- Open Claude Desktop on GNOME Wayland (or any Linux DE).
- Create a scheduled task via
/schedule for e.g. 0 8 * * *.
- Close the window (click X, not minimize).
- Wait until the scheduled time.
- The task does not fire;
~/.config/Claude/logs/main.log shows a clean beforeQuit from when the window was closed.
Root cause
In the upstream app.asar .vite/build/index.js, the main process registers Electron's stock handler (variable names are minified; shape is stable across the releases I've checked):
const en = process.platform === "darwin";
wA.app.on("window-all-closed", () => { en || bf() }); // bf = app.quit()
So on anything non-macOS, window-all-closed → app.quit(). The existing tray code in this repo's patch set creates a StatusNotifierItem, but it has no effect on the lifecycle: when the last window closes, the app dies and the tray goes with it.
On GNOME Wayland there is an additional UX trap: stock GNOME doesn't render StatusNotifierItem without the AppIndicator extension, so users see no tray icon at all and rely on the close/minimize buttons — but X and minimize are visually similar enough that misclicks are frequent. Once the app is dead, the next /schedule firing is missed.
Proposed fix (wrapper-based, no sed)
Extend the scripts/frame-fix-wrapper.js pattern (or add a sibling lifecycle-wrapper.js) so we don't have to track upstream variable renames:
- Intercept
BrowserWindow close on Linux: call event.preventDefault(); win.hide() unless an app.isQuitting flag is set.
- Intercept
app.on('window-all-closed', ...) on Linux so the default quit is neutralised (or register our own no-op first and let the tray menu's Quit item be the only quit path).
- Wire the existing tray menu's
Quit action to set app.isQuitting = true before calling app.quit().
- Gate via a config key (e.g.
linuxCloseToTray in ~/.config/Claude/config.json) or a CLAUDE_QUIT_ON_CLOSE=1 env var, so users who prefer X-closes-app can opt out.
This is a close cousin of the concern in #321 (orphaned processes when closing) but the intent is the reverse: keep the process alive intentionally so schedulers, MCP servers, and the tray keep working, with an explicit Quit path that actually shuts everything down cleanly.
Evidence from a real session
From my ~/.config/Claude/logs/main.log:
2026-04-19 22:15:44 [info] [SkillsPlugin] Window focused — polling now (last poll was 4645848ms ago)
2026-04-19 22:21:38 [info] beforeQuit: handler fired, going down
2026-04-19 22:21:38 [info] Starting onQuitCleanup, hiding windows
...
2026-04-19 22:21:39 [info] beforeQuit: handler fired, going down
Next startup wasn't until ~14h later. A /schedule entry timed for 0 8 * * * in that window was simply never run. No OOM event (journalctl clean, systemd-oomd active but not implicated), no suspend (last -x + continuous uptime), just the stock Electron exit on window-all-closed.
Environment
- Ubuntu 25.10 (Wayland, GNOME), x86_64
claude-desktop 1.3109.0 built from this repo's deb
- Kernel 6.17
Happy to put up a PR against the wrapper if the maintainer agrees with the approach.
Written by Claude Opus 4.7 (1M context) via Claude Code
Problem
On Linux, Claude Desktop exits when the last window is closed. The tray icon (created by the existing patches in
scripts/patches/tray.sh) disappears with it. This surprises users who believe they've minimized to tray, and silently breaks in-app features that depend on the runtime staying alive — notably the/scheduleskill's cron-style scheduler, which fires fromsetTimeouts inside the main Electron process.Repro
/schedulefor e.g.0 8 * * *.~/.config/Claude/logs/main.logshows a cleanbeforeQuitfrom when the window was closed.Root cause
In the upstream
app.asar.vite/build/index.js, the main process registers Electron's stock handler (variable names are minified; shape is stable across the releases I've checked):So on anything non-macOS,
window-all-closed→app.quit(). The existing tray code in this repo's patch set creates a StatusNotifierItem, but it has no effect on the lifecycle: when the last window closes, the app dies and the tray goes with it.On GNOME Wayland there is an additional UX trap: stock GNOME doesn't render
StatusNotifierItemwithout the AppIndicator extension, so users see no tray icon at all and rely on the close/minimize buttons — but X and minimize are visually similar enough that misclicks are frequent. Once the app is dead, the next/schedulefiring is missed.Proposed fix (wrapper-based, no sed)
Extend the
scripts/frame-fix-wrapper.jspattern (or add a siblinglifecycle-wrapper.js) so we don't have to track upstream variable renames:BrowserWindowcloseon Linux: callevent.preventDefault(); win.hide()unless anapp.isQuittingflag is set.app.on('window-all-closed', ...)on Linux so the default quit is neutralised (or register our own no-op first and let the tray menu's Quit item be the only quit path).Quitaction to setapp.isQuitting = truebefore callingapp.quit().linuxCloseToTrayin~/.config/Claude/config.json) or aCLAUDE_QUIT_ON_CLOSE=1env var, so users who prefer X-closes-app can opt out.This is a close cousin of the concern in #321 (orphaned processes when closing) but the intent is the reverse: keep the process alive intentionally so schedulers, MCP servers, and the tray keep working, with an explicit Quit path that actually shuts everything down cleanly.
Evidence from a real session
From my
~/.config/Claude/logs/main.log:Next startup wasn't until ~14h later. A
/scheduleentry timed for0 8 * * *in that window was simply never run. No OOM event (journalctl clean,systemd-oomdactive but not implicated), no suspend (last -x+ continuous uptime), just the stock Electron exit on window-all-closed.Environment
claude-desktop1.3109.0 built from this repo's debHappy to put up a PR against the wrapper if the maintainer agrees with the approach.
Written by Claude Opus 4.7 (1M context) via Claude Code