Proof Portal

Project overview

Omarchy

ProbeLabs65 findings · 95 requirements

A proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.

Inbox
ReportR-2149GitHub issuePublicDecided· Out of scope

Launch OSD can get stranded on screen indefinitely ("Launching <app>…" never dismisses)

Reported by Mike-Bay · filed Oct 1, 2026 · open at source

Decided: Out of scope — decided by agents · verified 3 days ago.

At a glance

1 · What is claimed?
Only the report text below — no validator has restated it as checkable claims yet.
2 · What does it affect?
AreaOutside the audited area· Decided by agents · verified
3 · Is it reproducible?
No validation has been run yet.
4 · What remains unclear?
  • Not validated yet: no claims, no reproducer.
5 · What happens next?
Decided by agents and verified by a second, independent agent — see the decision.

Original report

Launch OSD can get stranded on screen indefinitely ("Launching <app>…" never dismisses)revision 1 · captured 2026-10-01 17:40 UTC via poll

System details

Intel Core i7-10510U, NVIDIA MX250, Omarchy 4.0.4-1 (Quickshell 0.3.1, Hyprland 0.56.2, kernel 7.2.5-3-omarchy)

What's wrong?

The launch OSD can get stranded on screen indefinitely. The message never goes away, even after the app it names is closed and no menu is open.

Observed on this machine: a Launching Google Chrome… banner was still being displayed at the bottom of the screen with no Chrome window, no menu, and no toplevel change of any kind — the launch had long since completed or been abandoned. The only thing left on screen was the OSD itself.

Evidence that the OSD was genuinely stuck

The OSD is a layer-shell surface, so it does not appear in hyprctl clients — only in hyprctl layers. It was mapped and fully opaque:

Monitor HDMI-A-1:
	Layer level 3 (overlay):
		Layer 5cba66b17700: xywh: 0 0 1920 1080, a: 1, namespace: omarchy-osd, pid: 1086

and the OSD plugin confirmed it open, while IPC itself was healthy:

$ omarchy-shell osd state
open
$ omarchy-shell osd ping
ok

A single omarchy-shell osd close cleared it, and the surface went away immediately:

$ omarchy-shell osd close
ok
$ omarchy-shell osd state
closed
$ hyprctl layers | grep -c omarchy-osd
0

So the OSD side is completely healthy — the shell simply never asked it to close. Quickshell (pid 1086) had been up ~15 hours with no restart, so this was not a shell reload losing state.

Root cause

shell/services/AppLibrary.qml shows the launch OSD with `duration: 0`:

// AppLibrary.qml:245
Quickshell.execDetached(["omarchy-shell", "osd", "show", JSON.stringify({ icon: "󱓞", message: root.launchOsdMessage, duration: 0 })])

In the OSD plugin, a positive duration is what arms the auto-hide timer, so 0 deliberately disables it:

// Osd.qml:69-70
if (duration > 0) hideTimer.restart()
else hideTimer.stop()

That makes closeLaunchFeedback() the only thing that can ever take this OSD down — there is no server-side expiry. And that function gates the close on an in-memory boolean:

// AppLibrary.qml:168-177
function closeLaunchFeedback(serial) {
  if (serial !== root.launchSerial) return
  launchDelay.stop()
  launchTimeout.stop()
  if (root.launchOsdOpen) {
    Quickshell.execDetached(["omarchy-shell", "osd", "close"])
    root.launchOsdOpen = false
  }
}

The 15-second launchTimeout (AppLibrary.qml:249-253) is clearly intended as a safety net for exactly this, but it calls the same flag-gated function, so it inherits the same weakness.

The underlying fragility is that launchOsdOpen is a local mirror of remote state, and the two mutations that matter are fire-and-forget subprocesses that each independently shell out to omarchy-shell osd … → qs ipc:

// AppLibrary.qml:245 — show
Quickshell.execDetached([...])
// AppLibrary.qml:174 — close
Quickshell.execDetached([...])

Nothing ever reconciles the flag against the OSD's actual state, so if a close is lost, arrives before its show, or the flag is cleared without the call landing, the banner is pinned on screen with no timer and no owner left to recover it. Notably, Osd.qml:122 already exposes a state() IPC method that reports open/closed — the shell never queries it, so it has no way to notice the desync.

Steps to reproduce

Not deterministic for me — I caught it in the stuck state and confirmed the mechanism by reading the source rather than by reproducing it on demand. Worth noting for anyone triaging: the window is most likely to be hit when a launch's OSD is already open and another launch begins, or when the launched app produces a toplevel at an awkward moment relative to the 2s launchDelay / 15s launchTimeout boundaries. Watching it should show the OSD open long after both timers have expired.

Workaround

omarchy-shell osd close

Safe and idempotent.

Suggested fix

Either one of these closes the hole, in shell/services/AppLibrary.qml:

  1. Make the timeout authoritative — have launchTimeout send osd close unconditionally rather than gating on launchOsdOpen, so a lost or stale flag can never strand the OSD. This is the smaller, safer change.
  2. Give the launch OSD a real duration (e.g. 4000) so the OSD's own hideTimer backstops the flag. Note this changes behavior: the banner would auto-dismiss at 4s even if the app is still starting.

Option 1 preserves the current "OSD persists until the app's window appears" intent while making the stuck state unrepresentable.

View raw text
## System details

Intel Core i7-10510U, NVIDIA MX250, Omarchy 4.0.4-1 (Quickshell 0.3.1, Hyprland 0.56.2, kernel 7.2.5-3-omarchy)

## What's wrong?

The **launch OSD** can get stranded on screen indefinitely. The message never goes away, even after the app it names is closed and no menu is open.

Observed on this machine: a `Launching Google Chrome…` banner was still being displayed at the bottom of the screen with **no Chrome window, no menu, and no toplevel change of any kind** — the launch had long since completed or been abandoned. The only thing left on screen was the OSD itself.

### Evidence that the OSD was genuinely stuck

The OSD is a layer-shell surface, so it does not appear in `hyprctl clients` — only in `hyprctl layers`. It was mapped and fully opaque:

```
Monitor HDMI-A-1:
	Layer level 3 (overlay):
		Layer 5cba66b17700: xywh: 0 0 1920 1080, a: 1, namespace: omarchy-osd, pid: 1086
```

and the OSD plugin confirmed it open, while IPC itself was healthy:

```
$ omarchy-shell osd state
open
$ omarchy-shell osd ping
ok
```

A single `omarchy-shell osd close` cleared it, and the surface went away immediately:

```
$ omarchy-shell osd close
ok
$ omarchy-shell osd state
closed
$ hyprctl layers | grep -c omarchy-osd
0
```

So the OSD side is completely healthy — the shell simply never asked it to close. Quickshell (pid 1086) had been up ~15 hours with no restart, so this was not a shell reload losing state.

### Root cause

`shell/services/AppLibrary.qml` shows the launch OSD with **`duration: 0`**:

```qml
// AppLibrary.qml:245
Quickshell.execDetached(["omarchy-shell", "osd", "show", JSON.stringify({ icon: "󱓞", message: root.launchOsdMessage, duration: 0 })])
```

In the OSD plugin, a positive duration is what arms the auto-hide timer, so `0` deliberately disables it:

```qml
// Osd.qml:69-70
if (duration > 0) hideTimer.restart()
else hideTimer.stop()
```

That makes `closeLaunchFeedback()` the **only** thing that can ever take this OSD down — there is no server-side expiry. And that function gates the close on an in-memory boolean:

```qml
// AppLibrary.qml:168-177
function closeLaunchFeedback(serial) {
  if (serial !== root.launchSerial) return
  launchDelay.stop()
  launchTimeout.stop()
  if (root.launchOsdOpen) {
    Quickshell.execDetached(["omarchy-shell", "osd", "close"])
    root.launchOsdOpen = false
  }
}
```

The 15-second `launchTimeout` (`AppLibrary.qml:249-253`) is clearly intended as a safety net for exactly this, but it calls the same flag-gated function, so it inherits the same weakness.

The underlying fragility is that `launchOsdOpen` is a **local mirror of remote state**, and the two mutations that matter are fire-and-forget subprocesses that each independently shell out to `omarchy-shell osd …` → `qs ipc`:

```qml
// AppLibrary.qml:245 — show
Quickshell.execDetached([...])
// AppLibrary.qml:174 — close
Quickshell.execDetached([...])
```

Nothing ever reconciles the flag against the OSD's actual state, so if a `close` is lost, arrives before its `show`, or the flag is cleared without the call landing, the banner is pinned on screen with no timer and no owner left to recover it. Notably, `Osd.qml:122` already exposes a `state()` IPC method that reports `open`/`closed` — the shell never queries it, so it has no way to notice the desync.

### Steps to reproduce

Not deterministic for me — I caught it in the stuck state and confirmed the mechanism by reading the source rather than by reproducing it on demand. Worth noting for anyone triaging: the window is most likely to be hit when a launch's OSD is already open and another launch begins, or when the launched app produces a toplevel at an awkward moment relative to the 2s `launchDelay` / 15s `launchTimeout` boundaries. Watching it should show the OSD open long after both timers have expired.

### Workaround

```sh
omarchy-shell osd close
```

Safe and idempotent.

### Suggested fix

Either one of these closes the hole, in `shell/services/AppLibrary.qml`:

1. **Make the timeout authoritative** — have `launchTimeout` send `osd close` unconditionally rather than gating on `launchOsdOpen`, so a lost or stale flag can never strand the OSD. This is the smaller, safer change.
2. **Give the launch OSD a real duration** (e.g. `4000`) so the OSD's own `hideTimer` backstops the flag. Note this changes behavior: the banner would auto-dismiss at 4s even if the app is still starting.

Option 1 preserves the current "OSD persists until the app's window appears" intent while making the stuck state unrepresentable.

Validation

Not checked yet. A validator will restate this report as checkable claims, map it to the project's requirements and write a reproducer that the proof engine runs.

Linked

Not linked to any known issue, other report or change yet.

Decision

Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.

  • Out of scopeDecided by agents · verified · 2026-10-09 20:26 UTC