Proof Portal
Omarchy
ProbeLabs65 findings · 95 requirementsA 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.
Stranded-lock recovery never completes: requestSessionLock() early-returns on sessionLock.secure, so the session stays on "lockscreen app died" forever
Reported by jfbourdeau · filed Aug 14, 2026 · open at source
At a glance
- 1 · What is claimed?
- ConfirmedViolates SW-REQ-260912-WJYM: When the stranded-lock probe exits 0, the service holds no lock, and password PAM exists, the lock service shall take the session lock once. The defect: requestSessionLock returns on sessionLock.secure even when sessionLock.locked is false, so the lock is never taken.
- ConfirmedReproducible: requestSessionLock early-returns on sessionLock.secure when sessionLock.locked is false during stranded-lock recovery
- ConfirmedAffects the lock component
- 2 · What does it affect?
- 3 · Is it reproducible?
- An engine-executed run exited 1 and showed the defect signal. See reproducer
- 4 · What remains unclear?
- Nothing recorded as open.
- 5 · What happens next?
- Our staff decide whether the report is valid. Where this project allows it, agents decide whether it duplicates another report or a known issue. So far: in the audited area (lock) — decided by agents · verified; matches no known issue — decided by agents · verified. The outcome (valid, duplicate, not a bug, …) is not decided yet. See the decision.
Draft spec
When stranded-lock recovery has begun because the stranded-lock probe exited 0, password PAM is configured, and the service does not hold a session lock, the lock service shall take the session lock once even if sessionLock.secure is already true. A secure flag while sessionLock.locked is false is not a working lock and shall not make requestSessionLock return without taking the session lock.
Why this is an open question
Draft for a human. SW-REQ-260912-WJYM requires taking the session lock once when the probe exits 0 and the service holds no lock, but it does not state that sessionLock.secure without sessionLock.locked is not a held lock. The implementation treats that pair as unreachable and returns from requestSessionLock.
Proposed on this report's branch @ 7621e4f5a3 · specs/software/requirements/SW-REQ-261009-7BPR.req.yaml · not part of the audited spec until approved and merged
Written after the validation was sealed at e6801cef4f: the seal covers the reproducer and the observed behaviour, not this proposed text.
Original report
System details
Lenovo Yoga C640 laptop, single internal display, Arch, # omarchy-dev 4.0.0.r1744.gf002044-1, quickshell-git 0.3.0.r20.g28771c7-1, # hyprland 0.56.2-1
What's wrong?
Summary. If the shell reloads its QML while the session is locked, the lock surface
is destroyed and the session is left on Hyprland's "lockscreen app died" wall. The
shell notices this and starts its own recovery — lock-stranded: recovering — but
the recovery then stalls permanently, so the user still needs a TTY. The recovery path
is one early-return away from working.
Deterministic reproducer (no display flakiness or idle timing needed):
- Lock the session (
omarchy system lock). - From another machine over SSH, or a TTY, write any file into a plugin folder the
shell has loaded — e.g. touch ~/.config/omarchy/plugins/<your-plugin>/Foo.qml.
Anything that triggers Local plugin changed, reloading: will do.
3. Return to the machine: *"Oopsie daisy, it looks like you locked your screen but the
lockscreen app died"*, and it never resolves.
Hit on 2026-08-14 by a routine install.sh that copies plugin folders — the session
happened to be locked at the time.
What the log shows. quickshell never dies here; only the lock does:
14:59:16 omarchy-shell[143971]: DEBUG qml: Local plugin changed, reloading: yoda.emojis
14:59:16 omarchy-shell[143971]: DEBUG qml: omarchy lock … lock-stranded: recovering
14:59:16 omarchy-shell[143971]: DEBUG qml: omarchy lock … lock-requested
14:59:16 omarchy-shell[143971]: DEBUG qml: omarchy lock … lock-pending: screen-stabilizing
(nothing further — the session stayed this way until a TTY login 34 s later)Where it stalls — shell/plugins/lock/Service.qml:
recoverStrandedLock() (L94) correctly calls beginLock() (L128), which setslockRequested = true, logs lock-requested, and calls queueSessionLock() (L56).pendingSessionLockTimer (L441, interval: 100, repeat: true) then retriesrequestSessionLock() (L63) ten times a second — forever, silently:
function requestSessionLock() {
if (!lockRequested || sessionLock.locked || sessionLock.secure) return // L64
if (sessionLockStabilizeTimer.running) return // L65
if (!hasRealScreen()) {
… logEvent("lock-pending: no-real-screen") // L68
return
}
…
sessionLock.locked = true // L76
}The log proves it returns at L64, not later: the last event islock-pending: screen-stabilizing, and lock-pending: no-real-screen is never emitted
even though L68's condition would have logged it (lastEvent differs). So execution
never reaches L67 — it exits on sessionLock.locked || sessionLock.secure.
That is the trap: **a stranded lock means the compositor's ext-session-lock is still
held**, so sessionLock.secure is already true. The shell therefore concludes a lock
is in place and declines to create one — while no client is drawing anything. The
retry timer spins at 10 Hz achieving nothing, and because logEvent() (L110) only
records state changes, the spin leaves no trace.
locked (L37) is defined as lockRequested || sessionLock.locked || sessionLock.secure,
so the same secure flag also makes recoverStrandedLock()'s own guard (L95,if (… || locked || …) return) treat the orphan as a healthy lock on any later attempt.
Suggested fix. Recovery has to release the orphaned lock before requesting a new
one — the internal equivalent of hl.clear_crashed_lockscreen() — rather than treatingsecure as proof that a working lock exists. Distinguishing "the compositor is secured
by a surface this shell owns" from "the compositor is secured by a dead client"
would make the existing lock-stranded: recovering path complete on its own. A bounded
retry that gives up loudly (instead of an unbounded silent 10 Hz spin) would also have
made this obvious from the log.
Aggravating detail. Hyprland's on-screen recovery text advises killall -9 hyprlock,
but hyprlock is not installed on Quattro — the lock is the omarchy.lock Quickshell
plugin, so following that advice literally leads nowhere. The actual recovery ishyprctl eval 'hl.clear_crashed_lockscreen()' from a TTY or another machine.
Related but distinct: the shell exiting on a screen with no valid Wayland output
(reported separately). Here the shell survives the whole time — only the lock is lost —
so any watchdog that keys on "is quickshell running" will not notice this one.
Workaround in use here. A 30-second user timer that, when the shell is alive,
calls hl.clear_crashed_lockscreen() and immediately re-locks if it returns ok
(it returns ok only when the lock client is genuinely gone, and refuses while a
healthy lock exists, so polling it is safe). Plus a guard in our own installer that
refuses to write plugin files while the session is locked.
View raw text
### System details
# Lenovo Yoga C640 laptop, single internal display, Arch, # omarchy-dev 4.0.0.r1744.gf002044-1, quickshell-git 0.3.0.r20.g28771c7-1, # hyprland 0.56.2-1
### What's wrong?
**Summary.** If the shell reloads its QML while the session is locked, the lock surface
is destroyed and the session is left on Hyprland's *"lockscreen app died"* wall. The
shell **notices this and starts its own recovery** — `lock-stranded: recovering` — but
the recovery then stalls permanently, so the user still needs a TTY. The recovery path
is one early-return away from working.
**Deterministic reproducer** (no display flakiness or idle timing needed):
1. Lock the session (`omarchy system lock`).
2. From another machine over SSH, or a TTY, write any file into a plugin folder the
shell has loaded — e.g. `touch ~/.config/omarchy/plugins/<your-plugin>/Foo.qml`.
Anything that triggers `Local plugin changed, reloading:` will do.
3. Return to the machine: *"Oopsie daisy, it looks like you locked your screen but the
lockscreen app died"*, and it never resolves.
Hit on 2026-08-14 by a routine `install.sh` that copies plugin folders — the session
happened to be locked at the time.
**What the log shows.** `quickshell` never dies here; only the lock does:
```
14:59:16 omarchy-shell[143971]: DEBUG qml: Local plugin changed, reloading: yoda.emojis
14:59:16 omarchy-shell[143971]: DEBUG qml: omarchy lock … lock-stranded: recovering
14:59:16 omarchy-shell[143971]: DEBUG qml: omarchy lock … lock-requested
14:59:16 omarchy-shell[143971]: DEBUG qml: omarchy lock … lock-pending: screen-stabilizing
(nothing further — the session stayed this way until a TTY login 34 s later)
```
**Where it stalls** — `shell/plugins/lock/Service.qml`:
`recoverStrandedLock()` (L94) correctly calls `beginLock()` (L128), which sets
`lockRequested = true`, logs `lock-requested`, and calls `queueSessionLock()` (L56).
`pendingSessionLockTimer` (L441, `interval: 100`, `repeat: true`) then retries
`requestSessionLock()` (L63) ten times a second — forever, silently:
```qml
function requestSessionLock() {
if (!lockRequested || sessionLock.locked || sessionLock.secure) return // L64
if (sessionLockStabilizeTimer.running) return // L65
if (!hasRealScreen()) {
… logEvent("lock-pending: no-real-screen") // L68
return
}
…
sessionLock.locked = true // L76
}
```
The log proves it returns at **L64**, not later: the last event is
`lock-pending: screen-stabilizing`, and `lock-pending: no-real-screen` is never emitted
even though L68's condition would have logged it (`lastEvent` differs). So execution
never reaches L67 — it exits on `sessionLock.locked || sessionLock.secure`.
That is the trap: **a stranded lock means the compositor's `ext-session-lock` is still
held**, so `sessionLock.secure` is already `true`. The shell therefore concludes a lock
is in place and declines to create one — while no client is drawing anything. The
retry timer spins at 10 Hz achieving nothing, and because `logEvent()` (L110) only
records state *changes*, the spin leaves no trace.
`locked` (L37) is defined as `lockRequested || sessionLock.locked || sessionLock.secure`,
so the same `secure` flag also makes `recoverStrandedLock()`'s own guard (L95,
`if (… || locked || …) return`) treat the orphan as a healthy lock on any later attempt.
**Suggested fix.** Recovery has to *release* the orphaned lock before requesting a new
one — the internal equivalent of `hl.clear_crashed_lockscreen()` — rather than treating
`secure` as proof that a working lock exists. Distinguishing "the compositor is secured
**by a surface this shell owns**" from "the compositor is secured by a dead client"
would make the existing `lock-stranded: recovering` path complete on its own. A bounded
retry that gives up loudly (instead of an unbounded silent 10 Hz spin) would also have
made this obvious from the log.
**Aggravating detail.** Hyprland's on-screen recovery text advises `killall -9 hyprlock`,
but **hyprlock is not installed on Quattro** — the lock is the `omarchy.lock` Quickshell
plugin, so following that advice literally leads nowhere. The actual recovery is
`hyprctl eval 'hl.clear_crashed_lockscreen()'` from a TTY or another machine.
**Related but distinct:** the shell *exiting* on a screen with no valid Wayland output
(reported separately). Here the shell survives the whole time — only the lock is lost —
so any watchdog that keys on "is quickshell running" will not notice this one.
**Workaround in use here.** A 30-second user timer that, when the shell is alive,
calls `hl.clear_crashed_lockscreen()` and immediately re-locks if it returns `ok`
(it returns `ok` only when the lock client is genuinely gone, and refuses while a
healthy lock exists, so polling it is safe). Plus a guard in our own installer that
refuses to write plugin files while the session is locked.Also reported as
R-1855 is the earliest report of this defect, filed Aug 14, 2026. 2 later reports describe the same defect.
- Aug 14, 2026R-1855 · this report— the original
- ·Decided by agents · verified
Open upstream.
- ·Decided by agents · verified
Open upstream.
Validation
AI validator opencode-promoter-1 (xiaomi-token-plan-sgp/mimo-v2.6-pro) · evaluated revision 1 at product commit 0e375665e323 · sealed 2026-10-10T12:39:33Z
Stranded-lock recovery stalls because requestSessionLock treats a stale sessionLock.secure as a working lock. The reproducer extracts the live functions from Service.qml and demonstrates that when secure is true but locked is false, requestSessionLock returns without taking the session lock.
- ConfirmedViolates a requirementWhen the stranded-lock probe exits 0, the service holds no… SW-REQ-260912-WJYM
Violates SW-REQ-260912-WJYM: When the stranded-lock probe exits 0, the service holds no lock, and password PAM exists, the lock service shall take the session lock once. The defect: requestSessionLock returns on sessionLock.secure even when sessionLock.locked is false, so the lock is never taken.
Engine: engine-executed run exited 1 and showed the defect signal
What “confirmed” means here: the requirement exists and the engine reproduced the defect. That the reproducer exercises THIS requirement is the validator's argument (see rationale).
Evidence: requirement SW-REQ-260912-WJYM · file pocs/reproducers/a withheld known issue/repro.sh
Validator's rationale (commentary): SW-REQ-260912-WJYM requires taking the session lock once when the probe exits 0 and the service holds no lock. The reproducer shows that a stale secure flag (true while locked is false) makes requestSessionLock return without taking the lock.
- ConfirmedReproducible
Reproducible: requestSessionLock early-returns on sessionLock.secure when sessionLock.locked is false during stranded-lock recovery
Engine: engine-executed run exited 1 and showed the defect signal
Evidence: file pocs/reproducers/a withheld known issue/repro.sh
Affects the lock component
Engine: confirmed violation of SW-REQ-260912-WJYM owned by lock
Evidence: requirement SW-REQ-260912-WJYM · file pocs/reproducers/a withheld known issue/repro.sh
Engine reference checks at e6801cef4fe4 (8/8 ok)
- okcommite6801cef4fe4aa89ed10bb69c71a9c15efd0441a
- okbase commit0e375665e3233b541d5f0a761228a3dfe62acfa5
- okbase ancestry0e375665e3233b541d5f0a761228a3dfe62acfa5
- okproduct code untouchedthis report's reproducer branch
- okbranchthis report's reproducer branch
- okfilepocs/reproducers/a withheld known issue/repro.sh
- okrequirementSW-REQ-260912-WJYM
- okcomponentlock
The engine's own check, at validation time, that the commits, the reproducer branch (touching only reproducer files), the reproducer files and every mapped requirement / component / known issue really exist.
1 earlier check — superseded by the newest one above
Each new check of the report replaces the previous one; only the newest result above counts. Earlier results are kept for the record.
- Reproduced— checked report revision 1 on Oct 9, 2026; earlier check, superseded
Validator-authored hypothesis. Each claim's verification is engine-computed: confirmed only from an engine-executed reproducer run recorded in this record, refuted when recorded evidence contradicts it. Prose is commentary. The disposition of the report is a human decision.
Reproducer
bash pocs/reproducers/a withheld known issue/repro.sh
Defect signal: output contains PROOF-REPRODUCER: defect-present a withheld known issue
PROOF-REPRODUCER: defect-present a withheld known issue
control: secure=false took the session lock after-stale-secure sessionLocked=false secure=true lastEvent=lock-pending: screen-stabilizing events=log:lock-stranded: recovering,resetAuthenticationState,armBlankTimer,log:lock-requested,log:lock-pending: screen-stabilizing,stabilize.restart,callLater PROOF-REPRODUCER: defect-present a withheld known issue
Needs: linux · amd64 · toolchain · ran on linux/amd64
Linked
- Fixed bypr/7169· confirmed
- Fixed bypr/11240· confirmed
- Fixed bypr/8930· confirmed
- Related PRpr/14974· 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.
- Decided by agents · verified: in the audited area (lock)
- Decided by agents · verified: matches no known issue
The outcome (valid, duplicate, not a bug, …) is not decided yet.