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-1057GitHub issuePublicReceived

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

Partly in the audited area (lock) — decided by agents · verified. Not validated yet.

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?
AreaPartly in the audited arealock· 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?
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

Enabling a second clone of the same plugin leaves both active; two omarchy.lock clones crash-loop the shell until it gives uprevision 1 · captured 2026-09-28 21:24 UTC via poll

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

  1. 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 = true

The 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_64

Filed 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.