Skip to content

Linux: app quits when last window closed; breaks in-app schedulers and 'minimize to tray' expectation #448

@lizthegrey

Description

@lizthegrey

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

  1. Open Claude Desktop on GNOME Wayland (or any Linux DE).
  2. Create a scheduled task via /schedule for e.g. 0 8 * * *.
  3. Close the window (click X, not minimize).
  4. Wait until the scheduled time.
  5. 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-closedapp.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:

  1. Intercept BrowserWindow close on Linux: call event.preventDefault(); win.hide() unless an app.isQuitting flag is set.
  2. 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).
  3. Wire the existing tray menu's Quit action to set app.isQuitting = true before calling app.quit().
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpriority: mediumShould be addressed when possibletriage: investigatedIssue has been triaged and investigated

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions