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

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

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

Shell config mutations rewrite shell.json from in-memory state — concurrent/external edits silently lost, service plugins[] entries vanishrevision 1 · captured 2026-09-28 21:23 UTC via poll

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)
}
  • 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.

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