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.
Enabling a second clone of the same plugin leaves both active; two omarchy.lock clones crash-loop the shell until it gives up
Reported by samara-hub-ro · filed Sep 2, 2026 · open at source
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?
- 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?
- 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: partly in the audited area (lock) — decided by agents · verified. The outcome (valid, duplicate, not a bug, …) is not decided yet. See the decision.
Original report
What happened
PluginRegistry disables a clone's source when the clone is enabled, but never a
sibling clone of that same source. Two third-party clones of omarchy.lock can
therefore both be enabled at once, each mounting its own WlSessionLock. When the
session is locked and the shell hot-reloads, both run stranded-lock recovery and both
write sessionLock.locked = true. One wins the ext-session-lock; the other aborts.
Because every replacement instance also starts two competing lock services, this never
converges. It is not a single recoverable abort — it is a crash loop that ends with the
shell giving up:
13:03:18 FATAL: Tried to show lockscreen surfaces without active lock
13:03:20 FATAL: Tried to show lockscreen surfaces without active lock
13:03:24 FATAL: Tried to show lockscreen surfaces without active lock
...
13:03:35 ERROR: Quickshell crashed within 10 seconds of launching. Not restarting to avoid a crash loop.
13:03:35 Giving up on the Omarchy shell after 6 relaunches in under a minute.Seven cores in twenty seconds (coredumpctl, PIDs 197304 → 208685). Hyprland's failsafe
correctly kept the session locked — but with no locker client left to draw a password
prompt. The result is a black, unresponsive locked screen and a forced reboot. No lock
bypass, and no user-data loss.
Expected
Enabling a clone should make it the only active implementation of its source. Enabling a
second clone of the same source should either disable the first or be refused, the way
enabling a clone already disables its source.
Reproduction
- Install two third-party plugins whose manifests both declare
omarchy.clonedFrom: "omarchy.lock" and kinds including service.
Observed with `erijl.lock`
("FIDO2 Security Key Suite") and
`shmall.lock` ("Media Lock Screen").
2. Enable both — one via plugins[], one as a bar widget. omarchy.lock lands in
disabledPlugins[]; neither clone disables the other.
3. Confirm both services are live. Each start logs lock-stranded: recovering twice,
and the loser's IPC handler is rejected:
`QML IpcHandler at .../shmall.lock/Service.qml[500:3]: Handler was registered but will
not be used because another handler is registered for target lock`
4. Lock the session, then trigger a plugin hot-reload (save any file under
~/.config/omarchy/plugins/). Here the lock came from a FIDO2 key being unplugged with
lockOnUnplug: true, followed by a shell reload 16 seconds later.
Diagnosis
The registry only ever disables the source:
// services/PluginRegistry.qml:523
if (clonedFrom && hasNonWidgetKind && !isDisabled(config, clonedFrom)) {
addDisabled(config, clonedFrom)
setCloneShouldRestoreSource(config, key, true)
}and activeCloneFor (line 401) returns the first matching active clone, assuming at
most one exists. Nothing enforces that assumption at enable time.
Each clone's re-lock guard only inspects its own object, so it cannot see that a sibling
already holds the session lock:
if (!lockRequested || sessionLock.locked || sessionLock.secure) return
sessionLock.locked = trueThe abort itself is the known one, symbolized against Arch debuginfod:
qAbort()
qt_message_output(QtFatalMsg, ...)
QDebug::~QDebug()
WlSessionLock::updateSurfaces(show=true, old=0x0)
at quickshell/src/wayland/session_lock.cpp:106
WlSessionLock::qt_metacall(... QMetaObject::WriteProperty ...)
QQmlPropertyPrivate::write(...)
QQmlTimer::event(...)Resource exhaustion ruled out: no OOM-killer or systemd-oomd entries, 24 GiB available.
Relation to existing issues
This is not a duplicate of #9654 or #9441, which report the same qFatal from a
single lock service.
- #9441 already identifies the mechanism —
reloadPlugins()→unloadPluginServices()
destroys a live omarchy.lock, and recovery then writes sessionLock.locked = true.
In that single-service case the replacement instance recovers.
- #9654 reports the same abort during a normal idle lock and notes it could not be
reproduced a second time.
What is new here is the registry gap that allows two lock services to coexist, which
converts those recoverable aborts into an unrecoverable crash loop. Fixing #9441 would
stop this particular trigger; the registry gap would remain, and any future path that
re-runs stranded-lock recovery would hit it again. It also gives #9654 a deterministic
reproduction.
System details
Omarchy: 4.0.2-1
Kernel: 7.1.9-arch1-2
Hyprland: 0.56.2 (efb50993780079460b0cbed1363e2166a2de1d9f)
Quickshell: 0.3.1-1
qt6-base: 6.11.2-2
qt6-declarative: 6.11.2-1
qt6-wayland: 6.11.2-1
Architecture: x86_64Filed by Claude Opus 5 via Claude Code.
View raw text
## What happened
`PluginRegistry` disables a clone's **source** when the clone is enabled, but never a
**sibling clone** of that same source. Two third-party clones of `omarchy.lock` can
therefore both be enabled at once, each mounting its own `WlSessionLock`. When the
session is locked and the shell hot-reloads, both run stranded-lock recovery and both
write `sessionLock.locked = true`. One wins the `ext-session-lock`; the other aborts.
Because *every* replacement instance also starts two competing lock services, this never
converges. It is not a single recoverable abort — it is a crash loop that ends with the
shell giving up:
```text
13:03:18 FATAL: Tried to show lockscreen surfaces without active lock
13:03:20 FATAL: Tried to show lockscreen surfaces without active lock
13:03:24 FATAL: Tried to show lockscreen surfaces without active lock
...
13:03:35 ERROR: Quickshell crashed within 10 seconds of launching. Not restarting to avoid a crash loop.
13:03:35 Giving up on the Omarchy shell after 6 relaunches in under a minute.
```
Seven cores in twenty seconds (`coredumpctl`, PIDs 197304 → 208685). Hyprland's failsafe
correctly kept the session locked — but with no locker client left to draw a password
prompt. The result is a black, unresponsive locked screen and a forced reboot. No lock
bypass, and no user-data loss.
## Expected
Enabling a clone should make it the only active implementation of its source. Enabling a
second clone of the same source should either disable the first or be refused, the way
enabling a clone already disables its source.
## Reproduction
1. Install two third-party plugins whose manifests both declare
`omarchy.clonedFrom: "omarchy.lock"` and `kinds` including `service`.
Observed with [`erijl.lock`](https://github.com/Erijl/omarchy-fido2-key-suite)
("FIDO2 Security Key Suite") and
[`shmall.lock`](https://github.com/shmall03/omarchy-shmall.lock-plugin) ("Media Lock Screen").
2. Enable both — one via `plugins[]`, one as a bar widget. `omarchy.lock` lands in
`disabledPlugins[]`; neither clone disables the other.
3. Confirm both services are live. Each start logs `lock-stranded: recovering` **twice**,
and the loser's IPC handler is rejected:
`QML IpcHandler at .../shmall.lock/Service.qml[500:3]: Handler was registered but will
not be used because another handler is registered for target lock`
4. Lock the session, then trigger a plugin hot-reload (save any file under
`~/.config/omarchy/plugins/`). Here the lock came from a FIDO2 key being unplugged with
`lockOnUnplug: true`, followed by a shell reload 16 seconds later.
## Diagnosis
The registry only ever disables the source:
```qml
// services/PluginRegistry.qml:523
if (clonedFrom && hasNonWidgetKind && !isDisabled(config, clonedFrom)) {
addDisabled(config, clonedFrom)
setCloneShouldRestoreSource(config, key, true)
}
```
and `activeCloneFor` (line 401) returns the *first* matching active clone, assuming at
most one exists. Nothing enforces that assumption at enable time.
Each clone's re-lock guard only inspects its own object, so it cannot see that a sibling
already holds the session lock:
```qml
if (!lockRequested || sessionLock.locked || sessionLock.secure) return
sessionLock.locked = true
```
The abort itself is the known one, symbolized against Arch debuginfod:
```text
qAbort()
qt_message_output(QtFatalMsg, ...)
QDebug::~QDebug()
WlSessionLock::updateSurfaces(show=true, old=0x0)
at quickshell/src/wayland/session_lock.cpp:106
WlSessionLock::qt_metacall(... QMetaObject::WriteProperty ...)
QQmlPropertyPrivate::write(...)
QQmlTimer::event(...)
```
Resource exhaustion ruled out: no OOM-killer or systemd-oomd entries, 24 GiB available.
## Relation to existing issues
This is **not** a duplicate of #9654 or #9441, which report the same `qFatal` from a
single lock service.
- #9441 already identifies the mechanism — `reloadPlugins()` → `unloadPluginServices()`
destroys a live `omarchy.lock`, and recovery then writes `sessionLock.locked = true`.
In that single-service case the replacement instance recovers.
- #9654 reports the same abort during a normal idle lock and notes it could not be
reproduced a second time.
What is new here is the **registry gap that allows two lock services to coexist**, which
converts those recoverable aborts into an unrecoverable crash loop. Fixing #9441 would
stop this particular trigger; the registry gap would remain, and any future path that
re-runs stranded-lock recovery would hit it again. It also gives #9654 a deterministic
reproduction.
## System details
```text
Omarchy: 4.0.2-1
Kernel: 7.1.9-arch1-2
Hyprland: 0.56.2 (efb50993780079460b0cbed1363e2166a2de1d9f)
Quickshell: 0.3.1-1
qt6-base: 6.11.2-2
qt6-declarative: 6.11.2-1
qt6-wayland: 6.11.2-1
Architecture: x86_64
```
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
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 08:54 UTC · withdrawn
- Decided by agents · verified: partly in the audited area (lock)
The outcome (valid, duplicate, not a bug, …) is not decided yet.