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.
Shell config mutations rewrite shell.json from in-memory state — concurrent/external edits silently lost, service plugins[] entries vanish
Reported by taisau · filed Sep 8, 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?
- 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
System details
Intel Core i7-1260P (Framework Laptop 13), Intel Iris Xe iGPU, Omarchy 4.0.3-1, Hyprland 0.56.2, quickshell-git 0.3.0.r20.g28771c7-3
What's wrong?
~/.config/omarchy/shell.json is rewritten whole-file from in-memory state by every shell-side config mutation, so any concurrent or external edit the shell hasn't ingested is silently clobbered. In practice this deletes plugins[] entries — and a missing plugins[] entry for a service plugin means it silently never loads again.
Mechanism (quattro, shell/shell.qml:162):
function mutateShellConfig(mutator) {
var copy = JSON.parse(JSON.stringify(shellConfig || builtinShellConfig))
mutator(copy)
persistShellConfig(copy)
}shellConfigis only as fresh as the lastapplyShellConfig(). TheuserConfigFileFileView watch →reload()path is asynchronous, so an external writer that touches shell.json shortly before a shell-side mutation can lose the race: the nextenablePlugin/setPluginEnabled/ bar-move / settings mutation persists the stale in-memory copy over it.- External writers are normal in Omarchy flows:
omarchy plugin add/update/remove(driven by the plugin-manager bar widget) runomarchy-shell-config-based jq rewrites; migrations andomarchy refresh shellrewrite the file; users edit it by hand. With multiple writers, this is last-writer-wins with no merge and no warning.
Observed incident (real data loss, today):
- A third-party service plugin (
kinds: ["service"], entry pointService.qml) had aplugins[]entry in shell.json. Proven present: the shell instance started at 13:03 loaded it and mapped its layer surface (openlayerevent in the quickshell log). - At 14:12 a plugin install/enable flow rewrote shell.json; afterwards
plugins[]contained only the newly enabled plugin. The service plugin's entry was gone. - On the next shell restart the service plugin silently never loaded — service plugins require a
plugins[]entry, they have no bar widget, and nothing is logged. Restoring the entry +omarchy restart shellbrought it back.
Steps to recreate:
omarchy plugin enable <service-plugin>(or add its id toplugins[]) and restart the shell.- While the shell is running, externally edit shell.json (e.g.
jqadding a harmless key), then immediately trigger a shell-side mutation (a bar drag-reorder,omarchy-shell shell enablePlugin ..., or a plugin toggle from the plugin manager). If the mutation runs before the FileView reload lands, the persisted copy is the stale in-memory one and the external edit is gone. - Even without the race: any external edit inotify misses is silently lost on the next shell-side mutation.
Note: sequential shell-side mutations alone don't lose data (each persistShellConfig updates shellConfig); the hazard is any write the shell didn't ingest before the next mutation.
Suggested fixes (any of):
- Make
mutateShellConfigread-modify-write from the current on-disk file (re-parseuserConfigFile.text()fresh, falling back to in-memory only if unreadable) instead of the cachedshellConfig. - Before persisting, verify the on-disk text still matches the last-ingested content (cheap string compare); if it differs, re-ingest and re-apply the mutation, or fail loudly instead of persisting.
- At minimum, log a warning whenever a persist is about to overwrite content that differs from the last-known ingest — the silence is the worst part.
Related: #8357 is the upgrade-time version of the same hazard (force-overwrite instead of migrate); this report is the runtime-mutation cousin.
View raw text
### System details
Intel Core i7-1260P (Framework Laptop 13), Intel Iris Xe iGPU, Omarchy 4.0.3-1, Hyprland 0.56.2, quickshell-git 0.3.0.r20.g28771c7-3
### What's wrong?
`~/.config/omarchy/shell.json` is rewritten **whole-file from in-memory state** by every shell-side config mutation, so any concurrent or external edit the shell hasn't ingested is silently clobbered. In practice this deletes `plugins[]` entries — and a missing `plugins[]` entry for a service plugin means it silently never loads again.
Mechanism (quattro, `shell/shell.qml:162`):
```js
function mutateShellConfig(mutator) {
var copy = JSON.parse(JSON.stringify(shellConfig || builtinShellConfig))
mutator(copy)
persistShellConfig(copy)
}
```
- `shellConfig` is only as fresh as the last `applyShellConfig()`. The `userConfigFile` FileView watch → `reload()` path is asynchronous, so an external writer that touches shell.json shortly before a shell-side mutation can lose the race: the next `enablePlugin` / `setPluginEnabled` / bar-move / settings mutation persists the stale in-memory copy over it.
- External writers are normal in Omarchy flows: `omarchy plugin add/update/remove` (driven by the plugin-manager bar widget) run `omarchy-shell-config`-based jq rewrites; migrations and `omarchy refresh shell` rewrite the file; users edit it by hand. With multiple writers, this is last-writer-wins with no merge and no warning.
Observed incident (real data loss, today):
- A third-party service plugin (`kinds: ["service"]`, entry point `Service.qml`) had a `plugins[]` entry in shell.json. Proven present: the shell instance started at 13:03 loaded it and mapped its layer surface (`openlayer` event in the quickshell log).
- At 14:12 a plugin install/enable flow rewrote shell.json; afterwards `plugins[]` contained only the newly enabled plugin. The service plugin's entry was gone.
- On the next shell restart the service plugin silently never loaded — service plugins require a `plugins[]` entry, they have no bar widget, and nothing is logged. Restoring the entry + `omarchy restart shell` brought it back.
Steps to recreate:
1. `omarchy plugin enable <service-plugin>` (or add its id to `plugins[]`) and restart the shell.
2. While the shell is running, externally edit shell.json (e.g. `jq` adding a harmless key), then immediately trigger a shell-side mutation (a bar drag-reorder, `omarchy-shell shell enablePlugin ...`, or a plugin toggle from the plugin manager). If the mutation runs before the FileView reload lands, the persisted copy is the stale in-memory one and the external edit is gone.
3. Even without the race: any external edit inotify misses is silently lost on the next shell-side mutation.
Note: sequential shell-side mutations alone don't lose data (each `persistShellConfig` updates `shellConfig`); the hazard is any write the shell didn't ingest before the next mutation.
Suggested fixes (any of):
1. Make `mutateShellConfig` read-modify-write from the current on-disk file (re-parse `userConfigFile.text()` fresh, falling back to in-memory only if unreadable) instead of the cached `shellConfig`.
2. Before persisting, verify the on-disk text still matches the last-ingested content (cheap string compare); if it differs, re-ingest and re-apply the mutation, or fail loudly instead of persisting.
3. At minimum, log a warning whenever a persist is about to overwrite content that differs from the last-known ingest — the silence is the worst part.
Related: #8357 is the upgrade-time version of the same hazard (force-overwrite instead of migrate); this report is the runtime-mutation cousin.
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:13 UTC