Summary
caelestia shell -d exits 0 without starting a shell when another instance is still running, printing only An instance of this configuration is already running. to stdout. This makes the common kill; sleep; shell -d restart idiom racy, and it fails in the worst possible situation: restarting the shell while the session is locked.
Details
subcommands/shell.py builds the launch args as:
args = ["qs", "-c", "caelestia", "-n"]
...
if self.args.daemon:
args.append("-d")
subprocess.run(args)
qs -n / --no-duplicate means "Exit immediately if another instance of the given config is running." So when a previous instance has been asked to quit but has not finished tearing down, shell -d no-ops. Because it's a daemon launch there is no terminal attached, the message goes nowhere, and subprocess.run's non-zero-ness is never checked — the caller sees success.
Why this is more than cosmetic
The documented-ish way to restart the shell from a compositor keybind is:
qs -c caelestia kill; sleep .3; caelestia shell -d
Shell shutdown is not instant, and it is slowest while a session lock is up (the lock surface and its screencopy/blur have to be torn down). On my machine a full kill → restart round trip measures ~1.5s, so a 0.3s sleep loses the race.
When it loses the race while locked, the result is that Hyprland is left holding an ext-session-lock with no client: the fallback "lock error" screen, no password prompt, and no shell to restart — recoverable only via a TTY or a separate lock client. The keybind that exists specifically to rescue a broken lock is the thing that guarantees you stay locked out.
Log from the dead instance, showing a clean exit (not a crash) followed by nothing coming back:
2026-07-30 09:16:57.894 INFO: Exiting due to IPC request.
Reproduction
With a shell already running:
$ caelestia shell -d; echo "exit: $?"
An instance of this configuration is already running.
exit: 0
Suggested fixes
Any one of these would remove the footgun:
- Propagate the failure — return
qs's exit status from shell -d so kill; sleep; shell -d can be written to retry.
- Add
caelestia shell -r/--restart that kills, waits for the instance to actually disappear (qs list --all), then starts. This is the operation people actually want, and it's the one that needs to be race-free.
- Drop the hardcoded
-n (or put it behind a flag). For an emergency restart, briefly having two instances is strictly better than having zero.
Happy to send a PR for (2) if you'd like — it's the variant that fixes the lock-out case.
Versions
caelestia-cli-git 1.1.2.r5.g2311754-1
caelestia-shell-git 2.2.0.r17.gfbb3d1b-1
quickshell-git 0.3.0.r19.g43d4fa9-1
- Hyprland 0.56.1
Summary
caelestia shell -dexits 0 without starting a shell when another instance is still running, printing onlyAn instance of this configuration is already running.to stdout. This makes the commonkill; sleep; shell -drestart idiom racy, and it fails in the worst possible situation: restarting the shell while the session is locked.Details
subcommands/shell.pybuilds the launch args as:qs -n/--no-duplicatemeans "Exit immediately if another instance of the given config is running." So when a previous instance has been asked to quit but has not finished tearing down,shell -dno-ops. Because it's a daemon launch there is no terminal attached, the message goes nowhere, andsubprocess.run's non-zero-ness is never checked — the caller sees success.Why this is more than cosmetic
The documented-ish way to restart the shell from a compositor keybind is:
Shell shutdown is not instant, and it is slowest while a session lock is up (the lock surface and its screencopy/blur have to be torn down). On my machine a full kill → restart round trip measures ~1.5s, so a 0.3s sleep loses the race.
When it loses the race while locked, the result is that Hyprland is left holding an
ext-session-lockwith no client: the fallback "lock error" screen, no password prompt, and no shell to restart — recoverable only via a TTY or a separate lock client. The keybind that exists specifically to rescue a broken lock is the thing that guarantees you stay locked out.Log from the dead instance, showing a clean exit (not a crash) followed by nothing coming back:
Reproduction
With a shell already running:
Suggested fixes
Any one of these would remove the footgun:
qs's exit status fromshell -dsokill; sleep; shell -dcan be written to retry.caelestia shell -r/--restartthat kills, waits for the instance to actually disappear (qs list --all), then starts. This is the operation people actually want, and it's the one that needs to be race-free.-n(or put it behind a flag). For an emergency restart, briefly having two instances is strictly better than having zero.Happy to send a PR for (2) if you'd like — it's the variant that fixes the lock-out case.
Versions
caelestia-cli-git1.1.2.r5.g2311754-1caelestia-shell-git2.2.0.r17.gfbb3d1b-1quickshell-git0.3.0.r19.g43d4fa9-1