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-1829GitHub issuePublicFixed

Idle cycle restarts during a pending lock and launches a second screensaver into it — new ttfx aborts (isLocked guard passes while lock-pending)

Reported by s373nZ · filed Aug 15, 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

Idle cycle restarts during a pending lock and launches a second screensaver into it — new ttfx aborts (isLocked guard passes while lock-pending)revision 1 · captured 2026-09-28 21:29 UTC via poll

What happens. When the idle lock fires, omarchy-system-lock tears the running screensaver down correctly (the #6764 ordering). Destroying the screensaver window makes the compositor report idle again about a second later, the idle service begins a brand-new idle cycle, and because screensaverDelaySeconds == 0 that cycle launches a second screensaver immediately — into a session that is already locking. Its ttfx is born inside the lock window and aborts with the panic from #6762 as soon as its terminal goes away.

This is a third race, distinct from the two already in #6762. There the screensaver exists before the lock and the question is teardown ordering (#6764) or the supervisor loop respawning a renderer (@Unayung). Here the entire screensaver — terminal and renderer — is created after omarchy-system-lock has already run its pkills. No kill-ordering or bounded-wait fix in the lock script can prevent it, because at cleanup time there is nothing to kill and nothing to wait for.

Timeline (Omarchy 4.0.0-1, ttfx 0.3.1-1, foot, idle: {screensaver: 150, lock: 300}):

14:58:43.279  idle-monitor: idle
14:58:43.279  idle-cycle-start: screensaver=150 lock=300
14:58:43.279  process-start: screensaver            <- screensaver #1
14:58:44.288  process-exit: screensaver exitCode=0     (launcher ran 1.0s: it spawned)

15:01:13.423  lock-system: lock-timeout
15:01:13.428  process-start: lock omarchy-system-lock  <- kills screensaver #1

15:01:14.406  idle-monitor: idle                     <- compositor re-reports idle
15:01:14.406  idle-cycle-start: screensaver=150 lock=300
15:01:14.406  process-start: screensaver            <- screensaver #2, into the pending lock
15:01:14.462  lock-requested
15:01:14.462  lock-pending: screen-stabilizing
15:01:15.___  foot maps; ttfx PID 7320 starts
15:01:15.561  secure=true
15:01:15.6    ttfx 7320 SIGABRT
15:01:15.622  process-exit: lock exitCode=0
15:01:15.925  process-exit: screensaver exitCode=0     (launcher ran 1.5s: it spawned a terminal)
15:01:17.409  idle-cycle-cancel: screensaver-not-running

Same panic, recovered verbatim from the core:

thread 'main' (7320) panicked at library/std/src/io/stdio.rs:1166:9:
failed printing to stderr: Input/output error (os error 5)

Adjacent to it on the heap is ttfx's own io error: Input/output error (os error 5), so the double-fault path described in omacom-io/ttfx#10 is confirmed here too. The backtrace does not symbolize (ttfx ships stripped and is not on debuginfod.archlinux.org); frame offsets match the rest of the thread (+0xcc30a, +0xa48a9, +0xccd09, ...).

Why the guard doesn't stop it. shell/plugins/services/idle/Service.qml launches the screensaver through:

runProcess(screensaverProcess, "screensaver",
  "[[ $(omarchy-shell lock isLocked 2>/dev/null) == \"true\" ]] || omarchy-launch-screensaver")

At 15:01:14.406 the lock had been requested but had not reached secure=true (15:01:15.561, 1.1s later) — it was sitting in lock-pending: screen-stabilizing. isLocked reported false, correctly, and the launcher ran. It is a check-then-act test against a state that is still settling.

Why a new cycle starts at all. With screensaver=150 lock=300:

  • firstIdleTimeoutSeconds = min(150, 300) = 150
  • screensaverDelaySeconds = 150 - 150 = 0

so in beginIdleCycle:

if (root.screensaverDelaySeconds === 0) launchScreensaver()

any new idle cycle launches the screensaver with no delay at all. Since the lock's own cleanup destroys the screensaver window, and the service already documents that screensaver window churn perturbs the idle monitor ("Starting the screensaver can make the compositor report activity", Service.qml:160), locking feeds straight back into starting a new screensaver. This holds whenever screensaver <= lock, which is the normal configuration.

Evidence this is a second screensaver, not a respawned renderer. omarchy-launch-screensaver early-exits in milliseconds when pgrep -f '[o]rg.omarchy.screensaver' matches. The 15:01:14 invocation ran for 1.5s — through xdg-terminal-exec, hyprctl monitors, spawn and wait_for_screensaver_window — so no screensaver existed when it started, and it created a new terminal. Consistent with this, the crashing ttfx is PID 7320 and a client that compiled a fresh xkb keymap at 15:01:15 is PID 7313.

Expected. Locking should not leave a screensaver running, and should not start a new one.

Suggested fix. In the idle service rather than the lock script:

  • Don't begin a screensaver-launching idle cycle while a lock is requested or pending — gate on the lock's pending state, not only on secure.
  • Or suppress launchScreensaver() for a cycle that begins inside the lock transition and let lockSystem own the screensaver's absence.

Making ttfx exit cleanly on EIO/EPIPE (omacom-io/ttfx#10) would make this variant harmless, as it would the other two.

Repro. Set screensaver at or below lock, idle until the screensaver appears, then keep idling through the lock deadline. coredumpctl list | grep ttfx. One occurrence in four screensaver launches on this machine.

System. Omarchy 4.0.0-1, ttfx 0.3.1-1, foot, Hyprland 0.56.2-1, Linux 7.1.8-arch1-3, x86_64.

Related: #6762 (closed), #6764, omacom-io/ttfx#10.

Filed by Claude Opus 5 via Claude Code.

View raw text
**What happens.** When the idle lock fires, `omarchy-system-lock` tears the running screensaver down correctly (the #6764 ordering). Destroying the screensaver window makes the compositor report idle again about a second later, the idle service begins a **brand-new idle cycle**, and because `screensaverDelaySeconds == 0` that cycle launches a **second** screensaver immediately — into a session that is already locking. Its `ttfx` is born inside the lock window and aborts with the panic from #6762 as soon as its terminal goes away.

This is a third race, distinct from the two already in #6762. There the screensaver exists *before* the lock and the question is teardown ordering (#6764) or the supervisor loop respawning a renderer (@Unayung). Here the entire screensaver — terminal *and* renderer — is created **after** `omarchy-system-lock` has already run its `pkill`s. No kill-ordering or bounded-wait fix in the lock script can prevent it, because at cleanup time there is nothing to kill and nothing to wait for.

**Timeline** (Omarchy 4.0.0-1, ttfx 0.3.1-1, foot, `idle: {screensaver: 150, lock: 300}`):

```
14:58:43.279  idle-monitor: idle
14:58:43.279  idle-cycle-start: screensaver=150 lock=300
14:58:43.279  process-start: screensaver            <- screensaver #1
14:58:44.288  process-exit: screensaver exitCode=0     (launcher ran 1.0s: it spawned)

15:01:13.423  lock-system: lock-timeout
15:01:13.428  process-start: lock omarchy-system-lock  <- kills screensaver #1

15:01:14.406  idle-monitor: idle                     <- compositor re-reports idle
15:01:14.406  idle-cycle-start: screensaver=150 lock=300
15:01:14.406  process-start: screensaver            <- screensaver #2, into the pending lock
15:01:14.462  lock-requested
15:01:14.462  lock-pending: screen-stabilizing
15:01:15.___  foot maps; ttfx PID 7320 starts
15:01:15.561  secure=true
15:01:15.6    ttfx 7320 SIGABRT
15:01:15.622  process-exit: lock exitCode=0
15:01:15.925  process-exit: screensaver exitCode=0     (launcher ran 1.5s: it spawned a terminal)
15:01:17.409  idle-cycle-cancel: screensaver-not-running
```

Same panic, recovered verbatim from the core:

```
thread 'main' (7320) panicked at library/std/src/io/stdio.rs:1166:9:
failed printing to stderr: Input/output error (os error 5)
```

Adjacent to it on the heap is ttfx's own `io error: Input/output error (os error 5)`, so the double-fault path described in omacom-io/ttfx#10 is confirmed here too. The backtrace does not symbolize (ttfx ships stripped and is not on debuginfod.archlinux.org); frame offsets match the rest of the thread (`+0xcc30a`, `+0xa48a9`, `+0xccd09`, ...).

**Why the guard doesn't stop it.** `shell/plugins/services/idle/Service.qml` launches the screensaver through:

```qml
runProcess(screensaverProcess, "screensaver",
  "[[ $(omarchy-shell lock isLocked 2>/dev/null) == \"true\" ]] || omarchy-launch-screensaver")
```

At 15:01:14.406 the lock had been requested but had not reached `secure=true` (15:01:15.561, 1.1s later) — it was sitting in `lock-pending: screen-stabilizing`. `isLocked` reported false, correctly, and the launcher ran. It is a check-then-act test against a state that is still settling.

**Why a new cycle starts at all.** With `screensaver=150 lock=300`:

- `firstIdleTimeoutSeconds = min(150, 300) = 150`
- `screensaverDelaySeconds = 150 - 150 = 0`

so in `beginIdleCycle`:

```qml
if (root.screensaverDelaySeconds === 0) launchScreensaver()
```

any new idle cycle launches the screensaver with no delay at all. Since the lock's own cleanup destroys the screensaver window, and the service already documents that screensaver window churn perturbs the idle monitor ("Starting the screensaver can make the compositor report activity", Service.qml:160), locking feeds straight back into starting a new screensaver. This holds whenever `screensaver <= lock`, which is the normal configuration.

**Evidence this is a second screensaver, not a respawned renderer.** `omarchy-launch-screensaver` early-exits in milliseconds when `pgrep -f '[o]rg.omarchy.screensaver'` matches. The 15:01:14 invocation ran for 1.5s — through `xdg-terminal-exec`, `hyprctl monitors`, spawn and `wait_for_screensaver_window` — so no screensaver existed when it started, and it created a new terminal. Consistent with this, the crashing ttfx is PID 7320 and a client that compiled a fresh xkb keymap at 15:01:15 is PID 7313.

**Expected.** Locking should not leave a screensaver running, and should not start a new one.

**Suggested fix.** In the idle service rather than the lock script:

- Don't begin a screensaver-launching idle cycle while a lock is requested or pending — gate on the lock's pending state, not only on `secure`.
- Or suppress `launchScreensaver()` for a cycle that begins inside the lock transition and let `lockSystem` own the screensaver's absence.

Making ttfx exit cleanly on EIO/EPIPE (omacom-io/ttfx#10) would make this variant harmless, as it would the other two.

**Repro.** Set `screensaver` at or below `lock`, idle until the screensaver appears, then keep idling through the lock deadline. `coredumpctl list | grep ttfx`. One occurrence in four screensaver launches on this machine.

**System.** Omarchy 4.0.0-1, ttfx 0.3.1-1, foot, Hyprland 0.56.2-1, Linux 7.1.8-arch1-3, x86_64.

Related: #6762 (closed), #6764, omacom-io/ttfx#10.

Filed by Claude Opus 5 via Claude Code.

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

  • Fixed bypr/7131· confirmed

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 10:00 UTC