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-796GitHub issuePublicDecided· Out of scope

4.0.3: third-party plugins lost read access to their own shell.json entry, but updateEntryInline still replaces it — plugin settings are silently destroyed

Reported by rgreboucas · filed Sep 9, 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

4.0.3: third-party plugins lost read access to their own shell.json entry, but updateEntryInline still replaces it — plugin settings are silently destroyedrevision 1 · captured 2026-09-28 21:23 UTC via poll

System details

  • Omarchy 4.0.3-1 (regressed from 4.0.2-1)
  • Kernel 7.2.3-arch1-3, x86_64
  • Intel Core i7-1260P, Intel Alder Lake-P GT2 [Iris Xe]
  • quickshell 0.3.1-1

Hardware-independent: this is a QML API regression, verifiable by reading
/usr/share/omarchy/shell from the 4.0.2 and 4.0.3 packages side by side.

What's wrong?

4.0.3 removed third-party plugins' read access to their own shell.json entry, but kept
the writer that replaces that entry wholesale. Any plugin that does read-modify-write on its
own settings — the only correct way to use that writer — now silently destroys the user's
configuration.

The read went away. 4.0.3 stopped handing plugins the host shell object:

// 4.0.2  shell.qml:306
if ("shell" in inst) inst.shell = shell

// 4.0.3  shell.qml:929
if ("shell" in inst) inst.shell = shell.pluginShellFor(manifest)

pluginShellFor() returns a PluginShellApi (services/PluginShellApi.qml, new in 4.0.3)
for every non-__isFirstParty plugin. It exposes barConfig and idleConfig, but nothing
that resolves the plugin's own entry in the top-level plugins array.

The destructive write stayed. PluginShellApi.updateEntryInline() is still reachable,
and the host implementation rebuilds the entry from scratch:

// shell.qml:1103-1107
var pnext = { id: stripped }
for (var pk in settings) if (pk !== "id") pnext[pk] = settings[pk]
copy.plugins[j] = pnext          // replace, not merge

Those replace semantics are unchanged from 4.0.2. They were safe there only because the
plugin could read the full entry first and pass it back intact.

So a plugin that used to do the correct thing:

var current = pluginEntry()       // was: full entry from shell.shellConfig
current.unlock = value            // now: {} — every other key is gone
shell.updateEntryInline(pluginId, current)

now writes {id, unlock} and drops every other setting the user had.

Reproduced with

io.github.sirjul1337.lock-explorer v1.6.1, which stores design, boot, unlock,
unlockMs, blankMs, keepDisplayOn and inputMonitor on its top-level entry. On 4.0.3
it comes up with defaults for all of them, and one design change reduces the entry to
{id, design}. There are two user reports of the visible half of this at
SirJul1337/omarchy-lock-explorer#15 and #16.

Minimal repro without that plugin: any third-party service plugin with a top-level plugins
entry that reads shell.shellConfig gets undefined on 4.0.3.

Expected

Either half would close the hole; the first seems closest to the existing design:

  1. Give the facade the read that matches its write. PluginShellApi already scopes

updateEntryInline to the plugin's own id, so exposing that same entry for reading grants
nothing new. There is precedent in the bar path: bar entries already receive their inline
settings (plugins/bar/Bar.qml:2005, if ("settings" in target) target.settings = moduleSettings).
Top-level plugins entries receive no equivalent.

  1. Or make `updateEntryInline` merge instead of replace for top-level entries, so a

partial settings object cannot delete keys it never mentioned.

Right now a plugin's only workaround is to read ~/.config/omarchy/shell.json off disk
itself, which sidesteps the scoping the facade was introduced to enforce — it hands the
plugin everyone else's config too. That seems like the opposite of the intent.

View raw text
### System details

- Omarchy 4.0.3-1 (regressed from 4.0.2-1)
- Kernel 7.2.3-arch1-3, x86_64
- Intel Core i7-1260P, Intel Alder Lake-P GT2 [Iris Xe]
- quickshell 0.3.1-1

Hardware-independent: this is a QML API regression, verifiable by reading
`/usr/share/omarchy/shell` from the 4.0.2 and 4.0.3 packages side by side.

### What's wrong?

4.0.3 removed third-party plugins' read access to their own `shell.json` entry, but kept
the writer that replaces that entry wholesale. Any plugin that does read-modify-write on its
own settings — the only correct way to use that writer — now silently destroys the user's
configuration.

**The read went away.** 4.0.3 stopped handing plugins the host shell object:

```qml
// 4.0.2  shell.qml:306
if ("shell" in inst) inst.shell = shell

// 4.0.3  shell.qml:929
if ("shell" in inst) inst.shell = shell.pluginShellFor(manifest)
```

`pluginShellFor()` returns a `PluginShellApi` (`services/PluginShellApi.qml`, new in 4.0.3)
for every non-`__isFirstParty` plugin. It exposes `barConfig` and `idleConfig`, but nothing
that resolves the plugin's own entry in the top-level `plugins` array.

**The destructive write stayed.** `PluginShellApi.updateEntryInline()` is still reachable,
and the host implementation rebuilds the entry from scratch:

```qml
// shell.qml:1103-1107
var pnext = { id: stripped }
for (var pk in settings) if (pk !== "id") pnext[pk] = settings[pk]
copy.plugins[j] = pnext          // replace, not merge
```

Those replace semantics are unchanged from 4.0.2. They were safe there only because the
plugin could read the full entry first and pass it back intact.

So a plugin that used to do the correct thing:

```qml
var current = pluginEntry()       // was: full entry from shell.shellConfig
current.unlock = value            // now: {} — every other key is gone
shell.updateEntryInline(pluginId, current)
```

now writes `{id, unlock}` and drops every other setting the user had.

### Reproduced with

`io.github.sirjul1337.lock-explorer` v1.6.1, which stores `design`, `boot`, `unlock`,
`unlockMs`, `blankMs`, `keepDisplayOn` and `inputMonitor` on its top-level entry. On 4.0.3
it comes up with defaults for all of them, and one design change reduces the entry to
`{id, design}`. There are two user reports of the visible half of this at
SirJul1337/omarchy-lock-explorer#15 and #16.

Minimal repro without that plugin: any third-party service plugin with a top-level `plugins`
entry that reads `shell.shellConfig` gets `undefined` on 4.0.3.

### Expected

Either half would close the hole; the first seems closest to the existing design:

1. **Give the facade the read that matches its write.** `PluginShellApi` already scopes
   `updateEntryInline` to the plugin's own id, so exposing that same entry for reading grants
   nothing new. There is precedent in the bar path: bar entries already receive their inline
   settings (`plugins/bar/Bar.qml:2005`, `if ("settings" in target) target.settings = moduleSettings`).
   Top-level `plugins` entries receive no equivalent.

2. **Or make `updateEntryInline` merge instead of replace** for top-level entries, so a
   partial settings object cannot delete keys it never mentioned.

Right now a plugin's only workaround is to read `~/.config/omarchy/shell.json` off disk
itself, which sidesteps the scoping the facade was introduced to enforce — it hands the
plugin everyone else's config too. That seems like the opposite of the intent.

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 11:14 UTC