The Case of the Vanishing Window: Diagnosing a Silent Electron App Failure on Debian

Claude Desktop stopped opening — no error, no crash, just silence. Here's how a dead process left behind a stale lock file, why that made every launch attempt quietly give up, and how a self-healing wrapper script and a user-level desktop entry override made the fix permanent.

The Symptom

Claude Desktop (the unofficial Debian build, claude-desktop-unofficial) stopped opening. Not a crash, not an error dialog — nothing. Click the icon, click again, wait. No window. No message. Just silence.

That kind of failure is the most annoying kind to troubleshoot precisely because there’s nothing to go on. No stack trace to Google, no error code to search, no obvious place to start. So the approach had to be methodical: stop guessing, start eliminating.

Ruling Things Out, One Layer at a Time

Is the process even running? First move was launching from a terminal instead of the app icon, so any output would actually be visible. Running the binary directly produced an exit code of 0 — meaning the process itself thought it succeeded. That ruled out an obvious crash.

Checking ps aux | grep -i claude told a more interesting story: the app was running. Main process, GPU process, a network service, zygote processes — a full Electron process tree, alive and consuming memory, several minutes after “launch.”

So the app wasn’t failing to start. It was starting and then failing to ever draw a window.

Is there a window that’s just hidden? Before assuming a rendering bug, it was worth ruling out something dumber — a window that existed but was off-screen, minimized, or behind another window. wmctrl -l lists every window the window manager currently knows about. Claude wasn’t in that list at all. Confirmed: no window object was ever being created, not even an invisible one.

What do the logs actually say? Electron apps log extensively, and Claude Desktop keeps its logs under ~/.config/Claude/logs/. Two files stood out by their last-modified timestamps: main-window.log hadn’t been touched in months, while unknown-window.log had just been updated. That mismatch was a clue in itself — something window-related was happening, just not through the path that was supposed to create the actual main window.

The real answer, though, was sitting in main.log, repeated on every single launch attempt:

[info] Not main instance, returning early from app ready

That line is the whole story. Electron apps use what’s called a singleton lock — a lock file that ensures only one copy of the app runs at a time. When a second instance starts, it checks for that lock, sees it’s held, and assumes a first instance is already open — so it hands off its arguments and quietly exits, rather than opening a duplicate window.

The problem: no first instance existed. The lock was stale — left over from a previous session that didn’t shut down cleanly (a force-kill, a crash, a hung session). Every new launch attempt saw that orphaned lock file, assumed another copy owned it, and dutifully stepped aside for a process that no longer existed.

The Fix

Confirming the diagnosis was straightforward:

bash

ls -la ~/.config/Claude/ | grep -i singleton

This showed three symlinked lock files — SingletonLock, SingletonSocket, SingletonCookie — with SingletonLock pointing to a PID that had already been killed. Removing them (after confirming no Claude process was actually alive) cleared the false claim, and the very next launch opened a window normally.

Making It Permanent

A one-off fix that requires remembering three rm commands every time isn’t really a fix — it’s a chore. Since this kind of stale-lock situation can recur any time the app is killed abruptly rather than quit cleanly, the better move was to make the launch process self-healing.

The solution: a small wrapper script (~/.local/bin/claude-fix) that checks whether Claude is actually running (via pgrep) before launching. If no real process is found, it clears any stale lock files first, then starts the app normally:

bash

#!/bin/bash
CONFIG_DIR="$HOME/.config/Claude"

if ! pgrep -f claude-desktop > /dev/null; then
    rm -f "$CONFIG_DIR/SingletonLock" "$CONFIG_DIR/SingletonSocket" "$CONFIG_DIR/SingletonCookie"
fi

exec claude-desktop-unofficial "$@"

The last step was making sure this actually gets used — a fix that only works when typed manually into a terminal doesn’t help when you’re clicking a taskbar icon. Linux desktop app shortcuts are defined by .desktop files, which live in /usr/share/applications/ system-wide. Rather than editing that system file directly (which requires sudo and would get overwritten on the next package update), the fix was to copy it into the user-level override location (~/.local/share/applications/), which takes priority over the system version, and repoint its Exec= line at the wrapper script instead of the raw binary. A quick update-desktop-database refresh, and the icon click now runs through the safety check automatically — no terminal required, ever again.

Why This Matters Beyond One App

This wasn’t really about Claude Desktop specifically — it was a case study in a failure pattern that shows up constantly with any long-running process management, from desktop Electron apps to servers to daemons: stale state surviving an unclean shutdown, and being trusted as if it were still accurate. PID files, lock files, socket files — anything meant to represent “this thing is currently running” is only as good as the guarantee that it gets cleaned up when that thing stops. Kill something with -9 instead of a graceful signal, or have your system crash out from under it, and that guarantee breaks.

The diagnostic path here — check if it’s running, check if a window/resource actually exists, check the logs for what the app itself thinks happened, then work backward from there — is the same shape of investigation that applies to a hung service, a stuck daemon, or a misbehaving process anywhere else. Different domain, same muscle.


Filed under Lessons Learned as part of the ongoing Debian lab documentation.