Generated by Proof — the verification layer for AI-assisted development and continuous correctness audit.
Audit Cycle Report — fmggxe6eq39aplh79bgvh273.git
Executive Summary
Verdict: No new bugs introduced and none resolved in this window — posture unchanged.
| Signal |
Count |
Detail |
| ✅ Resolved this cycle |
0 |
— |
| 🔴 Introduced this release (still open) |
0 |
0 regression + 0 net-new |
| 🟥 …of which HIGH-severity |
0 |
— |
| 📋 Pre-existing backlog (gated, reproduced) |
73 |
— |
**Range:** _snapshot (no commit window)_
⚠️ no –from given: report is a point-in-time snapshot; every artifact is treated as in-range
Summary
Rows are non-overlapping: every open KnownIssue is counted exactly once — either as introduced this release (regression + net-new) or in the pre-existing still-open backlog.
| Bucket |
Count |
| KnownIssues resolved this cycle |
0 |
| Defects introduced this release (0 regression + 0 net-new) |
0 |
| Pre-existing still-open backlog |
73 |
| Historical coverage backfill (fixed before baseline; not in window) |
0 |
| Accepted risks (active) |
0 |
Corpus totals: 73 KnownIssues, 0 ProblemReports.
1. KnownIssues Resolved This Cycle
Issues we previously reported to you that this release fixes — verified by tripwire tests that now pass.
None resolved in this window.
2. Defects Introduced In This Release
The audit’s headline finding: bugs whose introducing change lands inside this window (snapshot) and that are STILL OPEN at HEAD. 0 qualify: 0 regression and 0 net-new. Each is also tracked as an open known issue and carries a committed reproducer; its introducing commit is linked below.
2a. Regression(s) introduced this release
No regressions introduced this release.
2b. Net-new defects introduced this release
No net-new defects introduced this release.
3. Pre-Existing Still-Open Backlog
Issues still present in this release, each gated to a requirement and reproduced.
73 active KnownIssues total — all listed individually below. Severity (holistic across all dimensions) and CVSS (security-only score from the source vector) render as separate columns, so a MEDIUM correctness item is not lost behind a HIGH security item.
| KI |
Status |
Severity |
CVSS |
Age |
Title |
KI-APPLY-LOCK-EXPORTED-FUNCTIONS-ROOT |
open |
🟧 HIGH |
— |
since inception |
Exported bash functions execute as root inside apply-lock despite the trusted-PATH replacement |
KI-LOCK-PAM-FLIP-ENGAGES-LOCK |
open |
🟧 HIGH |
— |
since inception |
Lock engages when password PAM disappears during the stabilize window; the stranded watchdog stands down |
KI-260930-2FBE |
reviewed |
🟨 MEDIUM |
— |
since inception |
omarchy-system-lock exits 0 when lock engagement fails, leaving the dismissed Lock row silent |
KI-APPLY-LOCK-FPRINT-GATE-FAIL-OPEN |
open |
🟨 MEDIUM |
— |
since inception |
Fingerprint gate fail-open: substring grep treats not-enrolled or failed fprintd-list probes as enrolled |
KI-APPLY-LOCK-PAM-NONATOMIC-WRITE |
reviewed |
🟨 MEDIUM |
— |
since inception |
omarchy-apply-lock rewrites the lock PAM stacks in place; an interrupted run leaves a torn stack on the live path |
KI-APPLY-LOCK-TARGET-USER-DEGRADES-ROOT |
open |
🟨 MEDIUM |
— |
since inception |
Failed PKEXEC_UID/user mapping silently degrades target_user to root, configuring PAM for the wrong account |
KI-LOCK-FPRINT-PROBE-FAIL-OPEN |
open |
🟨 MEDIUM |
— |
since inception |
Lock-service fingerprint probe fail-open: substring grep treats zero-enrollment fprintd-list output as configured |
KI-LOCK-SPONTANEOUS-UNLOCK-FAIL-OPEN |
reviewed |
🟨 MEDIUM |
— |
since inception |
Compositor-side lock loss with a request outstanding is treated as a finished unlock; no re-lock |
KI-MENU-IMAGES-VIPS-NO-TIMEOUT |
reviewed |
🟨 MEDIUM |
— |
since inception |
vipsthumbnail still-image lane has no timeout; a stalled conversion wedges its fan-out lane indefinitely |
KI-MENU-JSONC-COMMA-IN-STRING |
open |
🟨 MEDIUM |
— |
since inception |
stripJsonc comma regex rewrites strings that carry a comma before } or ] (omacom/omarchy#13250) |
KI-MENU-JSONC-UNICODE-WHITESPACE |
open |
🟨 MEDIUM |
— |
since inception |
Unicode or control whitespace between JSONC tokens empties the whole menu file |
KI-MENU-KEYBINDINGS-CODE-REWRITE-OVERREACH |
open |
🟨 MEDIUM |
— |
since inception |
parse_keycodes rewrites code:N/mouse:N tokens anywhere in a record, corrupting descriptions and commands |
KI-MENU-KEYBINDINGS-CSV-DESC-COMMA |
open |
🟨 MEDIUM |
— |
since inception |
Comma in a binding description shifts every later CSV field: truncated label, wrong dispatcher, failed dispatch |
KI-MENU-KEYBINDINGS-FUNC-BIND-NO-DISPATCH |
open |
🟨 MEDIUM |
— |
since inception |
Lua function binds (Select all, Universal copy/paste/cut, Zoom in, Reset zoom) render in the menu but dispatch nothing |
KI-MENU-KEYBINDINGS-SCAN-DOFILE-ABORT |
open |
🟨 MEDIUM |
— |
since inception |
Lua bind scan aborts on the first throwing require; every later Lua bind silently vanishes from the menu cache |
KI-MENU-KEYBINDINGS-STALE-LUA-CACHE |
open |
🟨 MEDIUM |
— |
since inception |
Warm keybindings cache serves a stale Lua command after the user edits only the command a __lua bind runs |
KI-MENU-KEYBINDINGS-STDERR-FAILS-DISPATCH |
open |
🟨 MEDIUM |
— |
since inception |
Successful hyprctl dispatch with a stderr warning is treated as a refusal and the exec command runs twice |
KI-MENU-SELECT-POLL-DEADLOCK |
reviewed |
🟨 MEDIUM |
— |
since inception |
omarchy-menu-select hangs forever when the summoned menu dies before writing done_file |
KI-MENU-SELECT-SUMMON-UNBOUNDED |
open |
🟨 MEDIUM |
— |
since inception |
omarchy-menu-select blocks forever inside the omarchy-shell summon call when the IPC never returns |
KI-SLEEP-LOCK-BUDGET-NONPOSITIVE |
open |
🟨 MEDIUM |
— |
since inception |
Non-positive sleep-lock budget for logind windows <= 1s: suspend proceeds with no lock attempt |
KI-LOCK-EMPTY-READLINK-WIPES-WALLPAPER |
reviewed |
🟩 LOW |
— |
since inception |
Failed/empty readlink result stored as backgroundPath, wiping a good lock-screen wallpaper |
KI-LOCK-FPRINT-START-FAIL-NO-RETRY |
reviewed |
🟩 LOW |
— |
since inception |
fingerprintPam.start() returning false disables fingerprint auth for the rest of the lock with no retry |
KI-LOCK-PAM-ERROR-DOUBLE-COUNT |
open |
🟩 LOW |
— |
since inception |
Abnormal PAM error increments failedAttempts twice (onError and onCompleted both run handlePasswordFailure) |
KI-LOCK-PROBE-HYPRCTL-NO-TIMEOUT |
reviewed |
🟩 LOW |
— |
since inception |
omarchy-hyprland-session-locked has no deadline on hyprctl -j monitors; a stalled compositor IPC hangs the probe |
KI-LOCK-SCREENS-CHANGE-BLANK-LOST |
reviewed |
🟩 LOW |
— |
since inception |
Screen add/remove while locked clears the blank intent and never re-arms the idle blank timer |
KI-LOCK-STALE-POSTER-INPLACE-OVERWRITE |
reviewed |
🟩 LOW |
— |
since inception |
In-place video wallpaper overwrite during a running poster job keeps the stale poster frame |
KI-MENU-APP-ACTIVATE-NULL-LIBRARY |
open |
🟩 LOW |
— |
since inception |
Activating an app row while appLibrary is null closes the menu and skips the launch |
KI-MENU-APPS-SORT-LOCALE |
open |
🟩 LOW |
— |
since inception |
Apps submenu sorts with code-unit < on lowercased labels while search sorts with localeCompare |
KI-MENU-CARET-COUNT-HIDDEN-CHILDREN |
open |
🟩 LOW |
— |
since inception |
childCount counts when-hidden children and duplicate itemOrder entries, so the submenu badge disagrees with what renders |
KI-MENU-CLI-EXIT-CODE-WRITE-RACES |
open |
🟩 LOW |
— |
since inception |
Contracted exit codes lost when the usage/diagnostic write fails: unknown verb exits 1, help exits 1 |
KI-MENU-CLI-JQ-FAIL-LOOKS-SUCCESS |
open |
🟩 LOW |
— |
since inception |
Failed jq payload build still execs the menu IPC with an empty payload and exits 0 |
KI-MENU-DETAIL-WHITESPACE-QUERY |
open |
🟩 LOW |
— |
since inception |
A whitespace-only filter is not a search for rebuildDisplay but is truthy for the detail gates |
KI-MENU-DMENU-NAN-GEOMETRY |
open |
🟩 LOW |
— |
since inception |
Non-numeric dmenu width/maxHeight become NaN and collapse the card layout |
KI-MENU-FILEVIEW-FAILURE-ASYMMETRY |
open |
🟩 LOW |
— |
since inception |
Failed menu FileView reloads are asymmetric: user handler wipes last good items, default keeps stale set |
KI-MENU-FOLD-HEIGHT-EXCEEDS-AVAILABLE |
open |
🟩 LOW |
— |
since inception |
foldedListHeight returns a height above the available cap; availableRowsHeight never clamps at zero |
KI-MENU-GOBACK-ROOT-STRAND |
open |
🟩 LOW |
— |
since inception |
goBack refuses to retrace while activeMenu is root, stranding navStack after a drill that landed on root |
KI-MENU-GUARD-BATCH-NO-DEADLINE |
reviewed |
🟩 LOW |
— |
since inception |
Menu guard batch runs as one bash child with no deadline; a blocking guard freezes guard answers for the session |
KI-MENU-GUARD-BATCH-STALE-APPLY |
open |
🟩 LOW |
— |
since inception |
Guard batches carry no generation token: a batch computed for the previous items publishes onto the new set |
KI-MENU-GUARD-ID-INJECTION-BATCH-ABORT |
open |
🟩 LOW |
— |
since inception |
Guard ids interpolated unquoted into the shared bash batch: metacharacter ids break the protocol or execute commands |
KI-MENU-INPUT-ESCAPE-CLEARS-FILTER |
open |
🟩 LOW |
— |
since inception |
Escape in input mode clears the entered text instead of cancelling the request |
KI-MENU-ISDESCENDANT-ROOT-ALWAYS |
open |
🟩 LOW |
— |
since inception |
isDescendantOf reports every non-root id as a descendant of root, including ids absent from the menu |
KI-MENU-ITEM-PROTOTYPE-LEAK |
open |
🟩 LOW |
— |
since inception |
Map reads walk Object.prototype: item(), results maps, providersLoaded treat constructor/toString as real entries |
KI-MENU-ITEMS-KEY-AMBIGUITY |
open |
🟩 LOW |
— |
since inception |
A flat menu containing an entry id ‘items’ is parsed as a wrapper and every sibling entry is silently discarded |
KI-MENU-JSONC-ARRAY-ROOT |
open |
🟩 LOW |
— |
since inception |
parseMenuJsonc reads a top-level JSON array as entries with ids 0, 1, … (omacom/omarchy#13492) |
KI-MENU-JSONC-CR-LINE-ENDINGS |
open |
🟩 LOW |
— |
since inception |
A whole-line comment in a CR-only menu file swallows the rest of the file |
KI-MENU-JSONC-INLINE-COMMENT |
open |
🟩 LOW |
— |
since inception |
stripJsonc removes only whole-line // comments; an inline comment tail empties the whole file (omacom/omarchy#13493) |
KI-MENU-JSONC-STRIP-GAPS |
open |
🟩 LOW |
— |
since inception |
stripJsonc has no block-comment pass: a /* */ comment empties the whole file |
KI-MENU-KEYBINDINGS-NO-TIMEOUT |
reviewed |
🟩 LOW |
— |
since inception |
Keybindings menu build has no deadline on hyprctl binds or xkbcli compile-keymap; a stalled callee blocks the menu |
KI-MENU-KEYBINDINGS-SENDKEY-UP-FAIL-STUCK |
open |
🟩 LOW |
— |
since inception |
Refused key-up after an accepted key-down leaves a synthetic key stuck down and skips the sendshortcut fallback |
KI-MENU-LINK-SINGLE-HOP |
open |
🟩 LOW |
— |
since inception |
openRoute follows a link one hop and never re-inspects the target kind: link-to-action opens as a menu |
KI-MENU-MERGE-OVERRIDE-WIPES-DEFAULTS |
open |
🟩 LOW |
— |
since inception |
normalize-before-merge wipes unspecified defaults: a label-only user entry erases action/aliases/guards |
KI-MENU-MERGE-PROTOTYPE-IDS |
open |
🟩 LOW |
— |
since inception |
mergeMenuSources stores prototype-named ids but drops them from itemOrder - the row is written and never rendered |
KI-MENU-MERGE-STALE-ORDER-ALIASED-ROWS |
open |
🟩 LOW |
— |
since inception |
mergeAppRows/swapProviderRows keep stale .order values and mutate caller-owned rows in place |
KI-MENU-NORMALIZE-EDGE-CASES |
open |
🟩 LOW |
— |
since inception |
normalizeItem edge cases: null parent kept, boolean guards preserved, action:0 mis-kinded, numeric labels |
KI-MENU-OPEN-LEAVES-CONFIRM |
open |
🟩 LOW |
— |
since inception |
A menu summon leaves a pending uninstall question on top of the new prompt |
KI-MENU-OPEN-NULL-PAYLOAD |
open |
🟩 LOW |
— |
since inception |
open() throws on a JSON null payload: parse succeeds, the catch does not run, the property read throws |
KI-MENU-OPEN-QUADRATIC |
open |
🟩 LOW |
— |
since inception |
Opening a select menu walks its rows quadratically (omacom/omarchy#10601) |
KI-MENU-PATH-HELPERS-COERCION |
open |
🟩 LOW |
— |
since inception |
pathFor coerces a null parent to the id ’null’/‘0’ row while parentPathFor returns empty |
KI-MENU-PENDING-INITIAL-MENU-DEAD |
open |
🟩 LOW |
— |
since inception |
pendingInitialMenu is stored and never read: a route summoned before the JSONC loads opens root and stays there |
KI-MENU-PROVIDER-PARTIAL-COMMIT |
open |
🟩 LOW |
— |
since inception |
Failed/killed providers commit their partial stdout and stay marked loaded; apps provider loads with no AppLibrary |
KI-MENU-PROVIDER-SLUG-COLLISION-DROPPED |
open |
🟩 LOW |
— |
since inception |
Provider rows colliding with a static id are silently dropped: takenIds only records batch-minted ids |
KI-MENU-REQUEST-LIFECYCLE |
open |
🟩 LOW |
— |
since inception |
dmenu request lifecycle drops waiters on re-summon, on a busy resultProc, and for doneFile-only requests |
KI-MENU-RESOLVE-ROUTE-FOLDING |
open |
🟩 LOW |
— |
since inception |
resolveRoute folds the input before the exact-id lookup and returns the normalized string as fallthrough |
KI-MENU-REVEAL-CURSOR-PEEK |
open |
🟩 LOW |
— |
since inception |
revealCursor loses the directional peek: an uninstantiated delegate skips it and the underhang write undoes it |
KI-MENU-SEARCH-SCORE-CRASH-NAN |
open |
🟩 LOW |
— |
since inception |
searchScore throws on a label-less merged row and returns a NaN rank that displayRow coerces to the best tier |
KI-MENU-SEARCH-SCORE-RANKING |
open |
🟩 LOW |
— |
since inception |
searchScore ranking anomalies: apps promoted in-tier, root exact-labels swamped, multi-term matches collapse |
KI-MENU-SEARCH-TEXT-NORMALIZATION-MISSES |
open |
🟩 LOW |
— |
since inception |
Search text misses: whole-word description matching drops punctuation-glued words and hyphenated alias forms |
KI-MENU-SELECT-GEOMETRY-INT-COERCION |
open |
🟩 LOW |
— |
since inception |
Geometry flags silently coerce non-numeric and flag-like values: –width abc summons width 0 |
KI-MENU-SELECT-HANDSHAKE-TMP-LEAK |
open |
🟩 LOW |
— |
since inception |
Failed second mktemp leaks the selection handshake temp file: cleanup trap installed only after both files exist |
KI-MENU-STICKY-FONTFAMILY |
open |
🟩 LOW |
— |
since inception |
Payload fontFamily is sticky: a later open that omits it keeps the previous caller’s font |
KI-MENU-SUMMON-ACTION-EMPTY-PAYLOAD |
open |
🟩 LOW |
— |
since inception |
summonAction rewrites an explicitly empty quoted payload to {} - the in-process summon diverges from the bash bytes |
KI-SLEEP-LOCK-BUDGET-ARG-OCTAL |
open |
🟩 LOW |
— |
since inception |
argv budget with a leading zero (08/09) bypasses the validation guard via an octal arithmetic error |
KI-SLEEP-LOCK-DEADLINE-INVARIANT-GAPS |
open |
🟩 LOW |
— |
since inception |
Header invariant ’every call bounded by the budget remainder’ unenforced on three external calls |
6. Historical Coverage Backfill — bugs fixed in prior releases, coverage added this cycle
(internal) Bugs both introduced AND fixed before the baseline — not in the audited window. Listed only to record the coverage (requirement + tripwire) this audit added so they cannot silently regress.
These bugs were both introduced and fixed before the baseline — this release did not introduce them and they are not in the audited window; listed here only to record the coverage (requirement + tripwire) this audit added so they cannot silently regress.
No historical-backfill defects in scope.
Worst-case rollup — obligation consequences
Requirements graded by their worst-case consequence per obligation class, severity-descending. Each row pairs an authored consequence (or a catalog-generic fallback, marked) with the class it grades. Consequence severity is authored — the catalog cannot silently promote or demote a project’s judgment.
| REQ |
Class |
Severity |
Rollup |
Consequence |
SYS-REQ-260922-6642 |
error_handling |
high |
high |
an action script that neither performs its side effect nor refuses loudly: the menu closes (the row activation consumed the click), nothing happens, and the user cannot tell success from failure - silent wrong-target execution is this project documented historical bug class (wrong same-named plugin enabled), which is why the loud-refusal half of the contract is graded high |
SW-REQ-260929-REJT |
error_handling |
medium |
medium |
a converter that fails for an environmental reason rather than a file reason (e.g. ffmpegthumbnailer briefly missing during a partial upgrade, or a transient ENOSPC) records a standing .failed marker, and the video row stays suppressed on every later open until the file signature (size+mtime) changes - the marker conflates file defect with environment defect |
SW-REQ-260929-THMB |
external_call_timeout_bounded |
medium |
medium |
vipsthumbnail runs with no timeout wrapper: a still image that stalls vips (corrupt/pathological file, stalled mount) wedges its fan-out lane forever; in –preload mode drain_pending_thumbnails runs before the picker opens, so enough wedged stills block the image picker open, and lazy opens leak one orphaned vipsthumbnail per open into the menu process |
SYS-REQ-260922-0M8A |
atomicity |
medium |
medium |
a provider rerun that swaps rows in place loses the write (QML var-property writes are occasionally dropped): orphan ids linger in itemOrder with no item behind them, the next merge carries the orphan forward, and the launcher lists the same app twice - the exact historical defect the fresh-object merge semantics exist to prevent |
SYS-REQ-260922-X6Z5 |
error_handling |
medium |
medium |
the summoned picker surface dies after a successful select IPC and before writing the answer file: the synchronous caller waits on a file that will never appear - the reproduced poll-deadlock class; the bound is owned at the dmenu children (9ABD/Q6ZS) as a KI-tracked deferral on KI-MENU-SELECT-POLL-DEADLOCK, so the parent records the decision rather than duplicating the debt row |
SYS-REQ-260927-WC89 |
error_handling |
medium |
medium |
the Lock row activation closes the menu and forks omarchy-system-lock, which then fails silently (binary absent after a partial sync, or the lock component refuses without a notification surface): the menu is already dismissed so no error reaches the user, and the user walks away from a session they believe locked - impact high, likelihood near zero (the lock binary is a default-set runtime invariant), graded medium on impact times the one-way visibility loss; the callees own lock engagement and its failure surfacing (SYS-REQ-260912-T0XP family, reviewed in batch A) |
SW-REQ-260929-REJT |
malformed_input |
low |
medium |
a corrupt video that crashes ffmpegthumbnailer with a non-timeout status is marker-suppressed until edited; acceptable by design, but a file whose size+mtime are restored (touch -d) after a real repair re-inherits the stale marker |
SW-REQ-260929-B8N9 |
malformed_input |
low |
low |
an id that names an Object.prototype member (constructor, toString, valueOf - all pure a-z, so slugify preserves them and a desktop app literally named Constructor gets slug id constructor) escapes the |
SW-REQ-260929-DXFJ |
edge_case |
low |
low |
the freeze capture lands while the active filter has zero visible rows: maxRowsHeight pins at 0 and cardTop pins at the centered position, so the card grows downward from a top computed for an empty card until the menu closes - a cosmetic wedge the freeze-once semantics makes sticky for the whole open |
SW-REQ-260929-T378 |
edge_case |
low |
low |
synthetic hover churn under a stationary pointer (a redraw loop or a stray device emitting move events with sub-threshold deltas) repeatedly re-enters selectFromPointer; the gate exists precisely so this cannot walk the selection, but a gate threshold crossed by accumulated sub-threshold jitter across one event burst would land the cursor on a row the user never aimed at - the disarm enumeration (keyboard nav, filtering, menu transition, delete dialog, open) must keep covering every interaction that should reset intent |
SYS-REQ-260922-J0AN |
error_handling |
low |
low |
the dispatcher exec fails (omarchy-shell not running, socket refusal): the keybind or script caller sees a one-line error on the invoking terminal and no menu - recoverable and loud, graded low because refusal is immediate rather than hung, and the verb surface is fixed (toggle/summon/close/refresh/ping) with jq –arg payload construction making payload injection structurally impossible |
SYS-REQ-260922-P708 |
boundary |
low |
low |
a drill-down or filter rebuild lands the cursor on a row that is now disabled (row set changed under the held selection index): the user presses Enter and the activation targets a row that renders dimmed - the contract forbids cursor-on-disabled and the children guard it (disabled rows skipped on cursor moves, no parked cursor when all rows are disabled), so the residual is only a rebuild racing an open dialog, graded low |
SYS-REQ-260922-PPDW |
error_handling |
low |
low |
one syntax slip in the user menu JSONC (unbalanced brace a hundred rows in) silently drops the ENTIRE user extension for that session: parseMenuJsonc catches and returns [], the menu renders defaults only, and no diagnostic anywhere tells the user their customization was discarded - fail-visible only by absence, recovery documented but undiscoverable |
SYS-REQ-260922-R8DQ |
boundary |
low |
low |
route precedence ambiguity: a user-declared alias that equals another item exact id (alias power on the item whose id is also power, or a slugified app claiming a menu id) flips which entry an alias route opens - resolution iterates itemOrder, so merged order (default-then-user, provider swaps) decides the winner; the exact-id-first partition and single-claim merge keep the wrong-target window at config-authoring time, graded low |
Appendix: Entity Detail
One record per entity shown in this profile. Headings are the verbatim entity id, so the in-table links above resolve here.
KI-APPLY-LOCK-EXPORTED-FUNCTIONS-ROOT
- Title: Exported bash functions execute as root inside apply-lock despite the trusted-PATH replacement
- Severity: high (basis: reproducer)
- CVE surface: none
- Status: open
- Description: The EUID==0 control (bin/omarchy-apply-lock:15) replaces PATH but leaves the environment intact. Bash imports the exported functions (BASH_FUNC_grep%%, BASH_FUNC_command_not_found_handle%%, …) into the function table, and function lookup precedes PATH in bash. A caller-supplied grep runs instead of /usr/bin/grep inside the fingerprint gate (line 48). An imported command_not_found_handle returning 0 makes ‘if omarchy-shell lock status’ (line 62) TRUE with the binary absent (chroot install). Caller-supplied CODE executes with root privileges - the hostile-environment case the PATH pin exists to refuse (SW-REQ-260912-EKJP trusted_path_only) is reachable anyway.
- Root cause / remediation: Unset imported functions before any decision point: eval ‘unset -f $(compgen -A function)’ under EUID 0. Alternatively run the body via env -i bash –noprofile –norc. Use ‘command’ for the pinned lookups
- Affected API: bin/omarchy-apply-lock (root lock-screen PAM provisioning)
- Linked requirement(s): SW-REQ-260912-EKJP
- Reproducer / evidence tests:
KI-LOCK-PAM-FLIP-ENGAGES-LOCK
- Title: Lock engages when password PAM disappears during the stabilize window; the stranded watchdog stands down
- Severity: high (basis: reproducer)
- CVE surface: none
- Status: open
- Description: The missing-pam gate (lockIpc.lock, shell/plugins/lock/Service.qml:741-746) checks passwordPamConfigured once. BeginLock then schedules requestSessionLock through the 500ms stabilize delay, and requestSessionLock (87-99) re-checks only lockRequested/sessionLock state - never PAM. A FileView reload whose onLoadFailed lands inside that window (config removed / TTY-cleared failsafe) flips the flag after the gate. That happens before line 99 sets sessionLock.locked = true. onPasswordPamConfiguredChanged early-returns on the false transition, and checkStrandedLock explicitly stands down once the code sets lockRequested (‘a lock this shell took is nobody’s orphan’). So the session locks with no password PAM behind it and nothing releases it.
- Root cause / remediation: Re-check passwordPamConfigured in requestSessionLock before sessionLock.locked = true and stand the lock back down (with the missing-pam notification) when it went false. Treat a PAM flip while lockRequested as a teardown trigger
- Affected API: shell/plugins/lock/Service.qml (session lock lifecycle, missing-pam gate)
- Linked requirement(s): SW-REQ-260912-J8SX, SYS-REQ-260912-T0XP
- Reproducer / evidence tests:
KI-260930-2FBE
- Title: omarchy-system-lock exits 0 when lock engagement fails, leaving the dismissed Lock row silent
- Severity: medium (basis: reproducer)
- CVE surface: none
- Status: reviewed
- Description: bin/omarchy-system-lock line 10 runs
omarchy-shell lock lock >/dev/null and never checks the exit status. A refused engagement (omarchy-shell exits non-zero) or an absent binary (exit 127) therefore passes silently. The script runs its side-effect steps and exits 0 on every path. The menu Lock row (default/omarchy/omarchy-menu.jsonc:38) dismisses the menu on activation, and no surface reports the failure. The user walks away from a session they believe locked; the WC89 hazard_review (batch D) grades this medium on high impact times near-zero likelihood. pocs/wc89-silent-lock-exit0.sh reproduces it: the real script exits 0 on both failure arms while the control arm confirms the invocation wiring.
- Root cause / remediation: bin/omarchy-system-lock must propagate the omarchy-shell lock exit instead of exiting 0. The lock component (SYS-REQ-260912-T0XP family) owns engagement and failure surfacing per the WC89 obligation_delegations record. Raise a visible failure surface (desktop notification with a non-zero CLI exit); the menu side stays fire-and-forget by design. Close criterion: pocs/wc89-silent-lock-exit0.sh flips red, with both failure arms exiting non-zero.
- Affected API: bin/omarchy-system-lock (menu Lock row callee, WC89 interface)
- Linked requirement(s): SYS-REQ-260927-WC89
- Reproducer / evidence tests:
KI-APPLY-LOCK-FPRINT-GATE-FAIL-OPEN
- Title: Fingerprint gate fail-open: substring grep treats not-enrolled or failed fprintd-list probes as enrolled
- Severity: medium (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: The fingerprint gate (bin/omarchy-apply-lock:47-48) greps fprintd-list output for the substring ‘finger’ with stderr discarded and no pipefail. Real fprintd empty-enrollment text (‘User alice has no fingers enrolled …’) contains ‘fingers’. The gate therefore takes the ENROLLED branch and installs pam_fprintd for a user with zero prints. A daemon error banner (‘Fingerprints’ then non-zero exit) also passes because the pipeline status is grep’s. The script then configures fingerprint PAM for an account that can never match a print.
- Root cause / remediation: Match the enrollment table (line-anchored ‘Fingerprints’ header plus per-finger rows) and require fprintd-list exit 0 with pipefail; treat any probe failure as not-enrolled
- Linked requirement(s): SW-REQ-260912-S154, SW-REQ-260912-Y0WT
- Reproducer / evidence tests:
KI-APPLY-LOCK-PAM-NONATOMIC-WRITE
- Title: omarchy-apply-lock rewrites the lock PAM stacks in place; an interrupted run leaves a torn stack on the live path
- Severity: medium (basis: reproducer)
- CVE surface: none
- Status: reviewed
- Description: bin/omarchy-apply-lock writes the password PAM stack with
as_root tee /etc/pam.d/omarchy-lock-password. With an enrolled finger, it also writes /etc/pam.d/omarchy-lock-fingerprint the same way. A heredoc feeds each write. tee opens the live file with O_TRUNC and rewrites it in place. The helper writes no temp file and does no rename.
While tee writes, the live path holds only a prefix of the new stack, and the previous stack is already gone. An interrupted run can stop in that window: a killed installer or update, a lost session, or a power cut. The lock screen then authenticates against a torn stack. A torn password stack can lack the pam_unix line, so password unlock fails until the helper runs again. A torn fingerprint stack can cut the pam_fprintd line short.
The window is short, because one process writes a few hundred bytes. The likelihood is low, but the impact on a lock screen is high. pocs/apply-lock-pam-torn-write.sh reproduces the defect on the real helper. It pauses each write after 60 bytes and kills the run. The live file then holds the torn 60-byte prefix of 549 and 122 bytes.
The seeded previous stack is gone, and the inode does not change. The control arm shows that an uninterrupted run writes both stacks byte-identical to the heredocs.
- Root cause / remediation: Write each stack to a sibling temp file in /etc/pam.d. Then rename the temp file over the live path. The rename stays on one filesystem, so it is atomic. For example,
as_root tee /etc/pam.d/omarchy-lock-password.tmp followed by as_root mv -f /etc/pam.d/omarchy-lock-password.tmp /etc/pam.d/omarchy-lock-password.
Do the same for omarchy-lock-fingerprint, and optionally fsync before the rename. Then remove the atomic_write deferrals on SYS-REQ-260912-JW2J and SW-REQ-260912-Y0WT and add witnesses. Close criterion: pocs/apply-lock-pam-torn-write.sh flips red, because the interrupted write leaves the previous complete stack on the live path.
- Affected API: bin/omarchy-apply-lock (writes /etc/pam.d/omarchy-lock-password and omarchy-lock-fingerprint)
- Linked requirement(s): SYS-REQ-260912-JW2J, SW-REQ-260912-Y0WT
- Reproducer / evidence tests:
KI-APPLY-LOCK-TARGET-USER-DEGRADES-ROOT
- Title: Failed PKEXEC_UID/user mapping silently degrades target_user to root, configuring PAM for the wrong account
- Severity: medium (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: target_user resolution (bin/omarchy-apply-lock:18-22) has no pipefail and no abort on a failed getent. With OMARCHY_INSTALL_USER and SUDO_USER unset and PKEXEC_UID unmappable, cut yields the empty string and line 22 falls back to $USER - root under pkexec/env -i. The script then configures the fprintd probe and both PAM files for the wrong account (root unless $USER maps elsewhere). The installing user’s lock authentication then stays misconfigured.
- Root cause / remediation: Abort with a diagnostic when the mapping yields an empty target_user instead of falling back to $USER; never configure PAM for uid 0
- Linked requirement(s): SW-REQ-260912-EKJP
- Reproducer / evidence tests:
KI-LOCK-FPRINT-PROBE-FAIL-OPEN
- Title: Lock-service fingerprint probe fail-open: substring grep treats zero-enrollment fprintd-list output as configured
- Severity: medium (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: fingerprintCheckProc’s embedded probe (shell/plugins/lock/Service.qml:534) is ‘fprintd-list “$USER” 2>/dev/null | grep -qi finger’. fprintd-list’s stdout for an empty store is ‘User NAME has no fingers enrolled for … Fingerprint Sensor’; grep -qi finger matches the substring, the probe echoes yes, and fingerprintConfigured = true (line 538). The lock screen then offers fingerprint auth to a zero-enrollment user where every attempt fails. Same substring-oracle class as KI-APPLY-LOCK-FPRINT-GATE-FAIL-OPEN but a disjoint file and code site (executed per lock via refreshFingerprintStatus).
- Root cause / remediation: Count enrolled fingers (line-anchored ‘Fingerprints’ table with >= 1 per-finger row) and require fprintd-list exit 0 before echoing yes
- Linked requirement(s): SYS-REQ-260912-T0XP
- Reproducer / evidence tests:
KI-LOCK-SPONTANEOUS-UNLOCK-FAIL-OPEN
- Title: Compositor-side lock loss with a request outstanding is treated as a finished unlock; no re-lock
- Severity: medium (basis: risk · correctness)
- Status: reviewed
- Description: WlSessionLock.onLockStateChanged (shell/plugins/lock/Service.qml:339-345): when locked flips false while lockRequested still holds, the branch clears lockRequested and pendingSessionLock, resets auth state and calls runWake() with no queueSessionLock/beginLock. That locked flip is an ext-session-lock finish/acquire failure delivered by the compositor rather than finishUnlock. finishUnlock sets lockRequested=false BEFORE locked=false, so this branch only runs for an external drop. A spontaneous protocol-level unlock then fails open (session visible, no re-lock attempted), unlike the no-real-screen path which keeps retrying.
- Root cause / remediation: Re-queue the lock (queueSessionLock) when locked flips false while lockRequested is outstanding, instead of treating it as an unlock
- Linked requirement(s): SYS-REQ-260912-T0XP
- Reproducer / evidence tests:
- Title: vipsthumbnail still-image lane has no timeout; a stalled conversion wedges its fan-out lane indefinitely
- Severity: medium (basis: risk · availability)
- CVE surface: none
- Status: reviewed
- Description: generate_thumbnail dispatches still images to env VIPS_CONCURRENCY=1 vipsthumbnail with no timeout wrapper (bin/omarchy-menu-images:219), while the video lane wraps ffmpegthumbnailer in timeout -k 5 10. A still image that stalls vips - corrupt or pathological file, or a path on a stalled mount - holds its fan-out lane forever. In –preload mode drain_pending_thumbnails runs synchronously before the picker opens, so enough wedged stills block the image picker open entirely. Lazy opens spawn generate_thumbnail inside the menu process, where each wedged still leaks an orphaned vipsthumbnail child per open. The converter’s content-hash lock fd stays closed, so a hung still pins no lock; damage is lane loss and unbounded process accumulation, not lock deadlock. Batch C flagged the mechanism; batch D confirms it against THMB and files the tracked debt.
- Root cause / remediation: Wrap the vipsthumbnail invocation in timeout (matching the video lane) or add an async kill to the generator. Then close this KI and flip the THMB deferral
- Affected API: bin/omarchy-menu-images still-image thumbnail lane
- Linked requirement(s): SW-REQ-260929-THMB
- Reproducer / evidence tests:
- Title: stripJsonc comma regex rewrites strings that carry a comma before } or ] (omacom/omarchy#13250)
- Severity: medium (basis: risk · data_integrity)
- CVE surface: none
- Customer impact: A menu label or action whose text has a comma directly before } or ] is silently changed: the comma is removed. The label “x, ]y” shows as “x ]y”, and the action mv f{.bak,} runs as mv f{.bak}, a different command. No error is shown. The shipped menu has no such text; only entries you write yourself can hit this.
- Status: open
- Description: stripJsonc (shell/plugins/menu/MenuModel.js:2-6) drops trailing commas with the regex /,(\s*[}]])/g. The regex does not track string literals. It also deletes a comma inside a label or an action when optional whitespace and } or ] follow it.
JSON.parse accepts the rewritten text, so nothing fails. The label “x, ]y” renders as “x ]y”. The action “mv f{.bak,}” runs as “mv f{.bak}” with the user privileges. Upstream issue: omacom/omarchy#13250. Report intake validated it (report cmulr8l6h0i461gw40vqqv64c).
- Root cause / remediation: Replace the comma regex with a string-aware pass that copies string literals verbatim, as omacom/omarchy#13968 does (requirement SW-REQ-260927-66FW in the proof layer). The pass must still drop a trailing comma when comment lines and the closer come after it. The preservation tests pin that shape.
- Affected API: MenuModel.parseMenuJsonc / stripJsonc trailing-comma pass (default and user menu JSONC)
- Linked requirement(s): SW-REQ-260922-E4J2
- Reproducer / evidence tests:
sh test/reports/report-cmulr8l6h0i461gw40vqqv64c.sh
- Title: Unicode or control whitespace between JSONC tokens empties the whole menu file
- Severity: medium (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: parseMenuJsonc (shell/plugins/menu/MenuModel.js) hands the stripped text to JSON.parse. JSON.parse accepts only space, tab, LF and CR as whitespace. The JavaScript \s class in the comment pass is wider: VT, FF, U+00A0, U+1680, U+2000 to U+200A, U+2028, U+2029, U+202F, U+205F, U+3000 and U+FEFF. The comment pass removes such a character together with a whole-line // comment that it indents.
Anywhere else between tokens, JSON.parse throws. The catch then returns an empty item set for the whole file. A user extension pasted from a web page or a word processor often carries no-break spaces. That file silently loses every entry, with no diagnostic.
We checked this live under Quickshell 0.3.1. FileView keeps these characters as read and drops only a leading byte-order mark. The real MenuModel.js parses a file with one U+00A0 or U+000B between tokens to 0 rows. pocs/menu-jsonc-unicode-whitespace.sh reproduces it for all 21 characters against an ASCII-space control.
test/shell.d/menu-test.sh pins it as a green tripwire.
- Root cause / remediation: Read every JS whitespace character outside a string literal as plain whitespace before JSON.parse, as omacom/omarchy#13968 does (characters inside strings stay data). Close criterion: pocs/menu-jsonc-unicode-whitespace.sh exits 0 and the KI-MENU-JSONC-UNICODE-WHITESPACE tripwires in test/shell.d/menu-test.sh flip red.
- Affected API: MenuModel.parseMenuJsonc (default and user menu JSONC)
- Linked requirement(s): SW-REQ-260922-E4J2, SW-REQ-260922-3T3F
- Reproducer / evidence tests:
sh pocs/menu-jsonc-unicode-whitespace.sh
- Title: parse_keycodes rewrites code:N/mouse:N tokens anywhere in a record, corrupting descriptions and commands
- Severity: medium (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: parse_keycodes’ awk matches /code:([0-9]+)/ and /mouse:([0-9]+)/ against the whole comma-joined record and sub() rewrites the first occurrence (bin/omarchy-menu-keybindings:54-63) - not just the key field. A description like ‘Count code:10 items’ renders as ‘Count 1 items’; a command like ’echo code:20’ becomes ’echo MINUS’. ’notify-send mouse:272’ becomes ’notify-send LEFT MOUSE BUTTON’. Real code: keys only survive because they happen to be the first match.
- Root cause / remediation: Confine the substitution to the key field: apply match/sub per CSV field instead of on $0, rewriting only field 2
- Linked requirement(s): SW-REQ-260922-0W96
- Reproducer / evidence tests:
- Title: Comma in a binding description shifts every later CSV field: truncated label, wrong dispatcher, failed dispatch
- Severity: medium (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: dynamic_bindings prints cache rows as raw comma-joined CSV with no quoting (printf ‘%s,%s,%s,%s,%s’, bin/omarchy-menu-keybindings:333). parse_binding_records sets FS=’,’ and takes action=$3/dispatcher=$4, so a description containing a comma (‘Save, quit’) shifts every later field. The menu shows the truncated label, and dispatch receives the literal string ’ quit’ as dispatcher and ’exec,alacritty’ as arg. dispatch_binding treats that pair as an unknown dispatcher.
- Root cause / remediation: Quote or escape the description field, or switch the internal record format to a separator descriptions cannot contain (TAB). Then parse with that separator end to end
- Linked requirement(s): SW-REQ-260922-0W96
- Reproducer / evidence tests:
- Title: Lua function binds (Select all, Universal copy/paste/cut, Zoom in, Reset zoom) render in the menu but dispatch nothing
- Severity: medium (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: Binds whose dispatcher is a raw Lua function never register in o.bind_commands, so build_lua_bind_cache caches them with kind=""/arg="" (bin/omarchy-menu-keybindings:217-219). dynamic_bindings then overwrites the __lua dispatcher with the empty map value (lines 309-312) and the row still prints. dispatch_binding’s empty-dispatcher arm returns 1: the six shipped function binds appear selectable but perform no action.
- Root cause / remediation: Cache a dispatchable expression for function binds (emit the Lua body or register bind_commands entries for the shipped function binds). Then refuse to render rows whose recovered dispatcher is empty
- Linked requirement(s): SW-REQ-260922-0W96
- Reproducer / evidence tests:
- Title: Lua bind scan aborts on the first throwing require; every later Lua bind silently vanishes from the menu cache
- Severity: medium (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: build_lua_bind_cache dofiles the user hyprland.lua under a single pcall (bin/omarchy-menu-keybindings:241). config/hypr/hyprland.lua:14 requires default.hypr.omarchy BEFORE hypr.bindings (line 21). Omarchy loads qconsole whose console_monitor() evaluates mon.scale > 0 against the scanner’s noop mock - a table-vs-number comparison that throws and kills the whole scan. The scan then never processes hypr.bindings, so every user Lua bind (rebinds and new binds alike) goes missing from the keybindings menu. Only a DEBUG=1 stderr line records why.
- Root cause / remediation: Isolate each bind registration from unrelated config code: run the dofile with a permissive mock (never-throwing comparisons), or scan binds via a per-require pcall boundary. Then one throwing module cannot drop unrelated binds
- Linked requirement(s): SW-REQ-260922-0W96
- Reproducer / evidence tests:
- Title: Warm keybindings cache serves a stale Lua command after the user edits only the command a __lua bind runs
- Severity: medium (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: keybindings_cache_key hashes a version literal, keymap lines and hyprctl binds only (bin/omarchy-menu-keybindings:541-546) - never the Lua source the scanner recovered the __lua rows from. A __lua row’s arg is empty in hyprctl output, so a command-only edit leaves the hash unchanged. output_binding_records cats the warm cache (line 575), and the menu keeps dispatching the previous command until some other hash input changes.
- Root cause / remediation: Fold the Lua config content (or its hash) into keybindings_cache_key so source edits invalidate the cache
- Linked requirement(s): SW-REQ-260922-0W96
- Reproducer / evidence tests:
- Title: Successful hyprctl dispatch with a stderr warning is treated as a refusal and the exec command runs twice
- Severity: medium (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: dispatch_lua_expression captures ‘hyprctl dispatch … 2>&1’ and accepts only status 0 with an empty or exactly-‘ok’ body (bin/omarchy-menu-keybindings:605-608). A compositor that succeeds but warns on stderr fails the body test and the function returns 1. dispatch_exec_binding’s ‘|| hyprctl dispatch exec’ (line 620) then runs the command a second time - double execution for any warned-but-accepted dispatch.
- Root cause / remediation: Decide success on the exit status alone (or capture stderr separately and ignore it). Then a warning can never flip an accepted dispatch into a fallback re-execution
- Linked requirement(s): SW-REQ-260922-0W96
- Reproducer / evidence tests:
- Title: omarchy-menu-select hangs forever when the summoned menu dies before writing done_file
- Severity: medium (basis: reproducer)
- CVE surface: none
- Status: reviewed
- Description: The done_file poll in bin/omarchy-menu-select (lines 93-95) has no deadline and no peer-liveness probe. If the menu process dies after a successful summon IPC, nothing writes the done file. The script loops forever and hangs every synchronous caller. The protocol covers user cancel but not peer death. Reproduced live on 2026-09-23: an external 3s timeout killed the waiting script. No requirement pins a bounded wait, so the defect had no contract to violate.
- Affected API: bin/omarchy-menu-select (dmenu select protocol)
- Linked requirement(s): SW-REQ-260922-Q6ZS, SW-REQ-260922-9ABD, SW-REQ-260922-HR29, SW-REQ-260922-KRBH, SW-REQ-260922-MP00, SW-REQ-260922-MH9B, SW-REQ-260922-SWFT
- Reproducer / evidence tests:
/bin/bash test/shell.d/menu-select-poll-deadlock-repro.sh
- Title: omarchy-menu-select blocks forever inside the omarchy-shell summon call when the IPC never returns
- Severity: medium (basis: risk · availability)
- CVE surface: none
- Status: open
- Description: bin/omarchy-menu-select:91 executes omarchy-shell shell summon with no timeout wrapper. A hung IPC (summon never returns) blocks the synchronous caller indefinitely - the external-call leg of the bounded-wait obligation. Distinct from KI-MENU-SELECT-POLL-DEADLOCK, which covers the post-summon done_file poll; here the process never even reaches the poll.
- Root cause / remediation: Bound the summon: timeout omarchy-shell shell summon … with a non-zero exit and a diagnostic on expiry
- Linked requirement(s): SW-REQ-260922-Q6ZS
- Reproducer / evidence tests:
KI-SLEEP-LOCK-BUDGET-NONPOSITIVE
- Title: Non-positive sleep-lock budget for logind windows <= 1s: suspend proceeds with no lock attempt
- Severity: medium (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: derive_budget_ms reserves max(window/5, 1000ms) with no floor (bin/omarchy-system-sleep-lock:37-40): a 1s InhibitDelayMaxUSec yields budget 0 and any sub-second window yields a NEGATIVE budget. The line-45 heal guard re-derives instead of flooring - a second identical busctl read that reinstalls the same illegal value. With budget <= 0 the deadline is already past, so every lock_ipc hits the remaining<=0 guard and returns 1 with empty stdout. request_lock’s case then silently falls through. The machine suspends with the lock NEVER attempted - only a ‘within 0ms’ notification marks it.
- Root cause / remediation: Floor the derived budget at 1ms (or clamp the reserve to window-1ms). Make the heal guard substitute a safe constant instead of re-deriving. Surface lock_ipc’s refusal instead of swallowing it
- Linked requirement(s): SW-REQ-260912-FAWV
- Reproducer / evidence tests:
KI-LOCK-EMPTY-READLINK-WIPES-WALLPAPER
- Title: Failed/empty readlink result stored as backgroundPath, wiping a good lock-screen wallpaper
- Severity: low (basis: risk · correctness)
- Status: reviewed
- Description: readlinkProc’s handler (shell/plugins/lock/Service.qml:499-507) takes lines[0] of the collector output with no non-empty guard. The embedded bash prints only after a successful readlink, so a missing currentBackgroundLink or a readlink error yields next = ‘’. The next !== backgroundPath branch then clears videoPosterPath, stores backgroundPath = ‘’, and bumps backgroundVersion. A transient wallpaper race (theme switch) then blanks the lock background until a later refresh succeeds.
- Root cause / remediation: Keep the previous backgroundPath (and signature) when the readlink output is empty, and retry via a short timer instead of committing the empty result
- Linked requirement(s): SYS-REQ-260912-T0XP
- Reproducer / evidence tests:
KI-LOCK-FPRINT-START-FAIL-NO-RETRY
- Title: fingerprintPam.start() returning false disables fingerprint auth for the rest of the lock with no retry
- Severity: low (basis: risk · correctness)
- Status: reviewed
- Description: startFingerprint (shell/plugins/lock/Service.qml:294-295) clears fingerprintAuthenticating when fingerprintPam.start() returns false but arms no timer. Only handleFingerprintFinished and the PAM onError path restart fingerprintRetryTimer; neither runs when a conversation never began. The per-event callers (onSecureStateChanged, fingerprintCheckProc.onExited) fire once - so a transient start failure leaves fingerprint dead until some unrelated event re-runs startFingerprint.
- Root cause / remediation: Arm fingerprintRetryTimer in the start()-false arm so the offer comes back on the same lock
- Linked requirement(s): SYS-REQ-260912-T0XP
- Reproducer / evidence tests:
KI-LOCK-PAM-ERROR-DOUBLE-COUNT
- Title: Abnormal PAM error increments failedAttempts twice (onError and onCompleted both run handlePasswordFailure)
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: Quickshell’s PamContext documents error as followed by completed(PamResult.Error). Service.qml wires onError -> handlePasswordFailure (436-437) AND onCompleted’s else branch -> handlePasswordFailure (432). The failedAttempts += 1 at 283 has no per-transaction latch, so one abnormal PAM failure counts as two. The failure message then rewrites itself (Authentication failed (n) then (n+1)). Normal wrong passwords take only the completed path and count once.
- Root cause / remediation: Handle the failure in onCompleted only (it receives the Error result), or latch the transaction so the error/completed pair increments once
- Linked requirement(s): SYS-REQ-260912-T0XP
- Reproducer / evidence tests:
KI-LOCK-PROBE-HYPRCTL-NO-TIMEOUT
- Title: omarchy-hyprland-session-locked has no deadline on hyprctl -j monitors; a stalled compositor IPC hangs the probe
- Severity: low (basis: reproducer)
- CVE surface: none
- Status: reviewed
- Description: bin/omarchy-hyprland-session-locked asks the compositor for the lock state with
monitors=$(hyprctl -j monitors 2>/dev/null) || exit 2. Nothing puts a deadline on that call. Sometimes the Hyprland IPC socket accepts the request but never answers. This happens in practice around lid and monitor transitions. The probe then blocks for as long as hyprctl blocks, instead of returning exit 2 (undetermined).
The stranded-lock recovery path calls this probe. That path then waits on a probe that never reports, so recovery makes no progress for the duration of the stall. The Hyprland failsafe lock screen is the backstop, so the session stays locked. pocs/eh0k-hyprctl-monitors-no-timeout.sh reproduces the defect. With hyprctl stalled, an external 5s watchdog kills the real probe (rc=124). The control arms answer 0 (locked) and 1 (unlocked) immediately.
- Root cause / remediation: Bound the IPC call in bin/omarchy-hyprland-session-locked, for example
monitors=$(timeout 2 hyprctl -j monitors 2>/dev/null) || exit 2. A stalled compositor then maps to the existing exit 2 (undetermined), which callers already handle. Then remove the SW-REQ-260912-EH0K external_call_timeout_bounded deferral and add a witness for the bounded path. Close criterion: pocs/eh0k-hyprctl-monitors-no-timeout.sh flips red, because the stall arm returns exit 2 inside the watchdog.
- Affected API: bin/omarchy-hyprland-session-locked (lock-state probe used by stranded-lock recovery)
- Linked requirement(s): SW-REQ-260912-EH0K
- Reproducer / evidence tests:
KI-LOCK-SCREENS-CHANGE-BLANK-LOST
- Title: Screen add/remove while locked clears the blank intent and never re-arms the idle blank timer
- Severity: low (basis: risk · correctness)
- Status: reviewed
- Description: onScreensChanged (shell/plugins/lock/Service.qml:666-674) sets displaysBlank = false (so a returning panel shows the wallpaper) but calls neither armBlankTimer nor runWake. After the one-shot idleBlankTimer has fired, nothing restarts the 5s countdown, and a newly added output falls back to displaysBlank in the DPMS map. So after any hotplug/unplug while locked, all displays stay awake until the next keypress re-runs the idle path.
- Root cause / remediation: After clearing displaysBlank in onScreensChanged, re-arm the idle blank timer (or runWake-then-arm) so the power path resumes
- Linked requirement(s): SW-REQ-260912-ND55
- Reproducer / evidence tests:
KI-LOCK-STALE-POSTER-INPLACE-OVERWRITE
- Title: In-place video wallpaper overwrite during a running poster job keeps the stale poster frame
- Severity: low (basis: risk · correctness)
- Status: reviewed
- Description: refreshPoster drops the new request while posterProc is running (running guard, Service.qml:138) without queueing, and posterProc.onExited (523-527) commits whenever sourcePath === backgroundPath. An in-place overwrite (same path, new bytes) therefore keeps the poster hashed from the pre-overwrite stat. BackgroundVersion changes the URL but the jpeg bytes are stale, and the code schedules no second refresh.
- Root cause / remediation: Queue (or re-arm) the poster request when the running guard fires. Then compare the stat signature - not just the path - in onExited before committing
- Linked requirement(s): SYS-REQ-260912-T0XP
- Reproducer / evidence tests:
- Title: Activating an app row while appLibrary is null closes the menu and skips the launch
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: activateIndex sets opened=false before the launch guard. With appLibrary null at activation time (the code built the rows while it existed), the click only dismisses the menu. No launch, no reopen (SX26/C29).
- Root cause / remediation: check the library before closing, or keep the menu open when the launch cannot run
- Linked requirement(s): SW-REQ-260922-Z680
- Reproducer / evidence tests:
- Title: Apps submenu sorts with code-unit < on lowercased labels while search sorts with localeCompare
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: The apps comparator uses aLabel < bLabel (UTF-16 code units), so labels above U+007A sort after ‘z’ - accented app names land at the end. The search path instead uses localeCompare on the same rows (anchor SRT1).
- Root cause / remediation: use localeCompare in the apps sort
- Linked requirement(s): SW-REQ-260922-Z680
- Reproducer / evidence tests:
- Title: childCount counts when-hidden children and duplicate itemOrder entries, so the submenu badge disagrees with what renders
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: childCount has no visibility check and no id dedup. Hidden (when:false) children and duplicated order entries each count once (F20 measured 3 where 1 row renders). The caret badge therefore shows counts the menu will not display.
- Root cause / remediation: count only children passing isVisible and dedupe ids (or trust the merges to dedupe itemOrder)
- Linked requirement(s): SW-REQ-260922-SJ7P
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: Contracted exit codes lost when the usage/diagnostic write fails: unknown verb exits 1, help exits 1
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: Under set -e, the unknown-verb diagnostic (bin/omarchy-menu:50-52) and the help heredoc (34-35) abort the script when their write fails. With fd 2 closed an unknown verb exits 1 without reaching the canonical exit 2. With fd 1 closed help exits 1 and never reaches exit 0. Callers that distinguish usage errors (2) from generic failure (1) misclassify, and help loses its promised success.
- Root cause / remediation: Guard the writes (echo … >&2 || true; exit 2; and cat >&1 || true; exit 0) so the contracted exit status survives a failed write
- Linked requirement(s): SW-REQ-260922-T257
- Reproducer / evidence tests:
- Title: Failed jq payload build still execs the menu IPC with an empty payload and exits 0
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: toggle and summon (bin/omarchy-menu:23,26) embed $(menu_payload …) inside the exec with no status check. When jq fails, the substitution is empty, the script still execs omarchy-shell shell toggle/summon omarchy.menu ’’ and exits 0. A broken payload builder presents as a successful menu open that silently does nothing (the plugin receives an empty payload).
- Root cause / remediation: Build the payload first with a status check (payload=$(menu_payload …) || exit 1) and refuse to exec with an empty payload
- Linked requirement(s): SW-REQ-260922-T257, SW-REQ-260922-SNZG
- Reproducer / evidence tests:
- Title: A whitespace-only filter is not a search for rebuildDisplay but is truthy for the detail gates
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: rebuildDisplay branches on filterText.trim() while rowHeightForDetail, the detail Text and the header test the raw truthy filterText (anchor DET1). setFilter(’ ‘) keeps the unfiltered rows yet renders detail-height rows and a whitespace title.
- Root cause / remediation: gate the detail surfaces on the trimmed query (share one ‘searchActive’ computed property)
- Linked requirement(s): SW-REQ-260922-B757
- Reproducer / evidence tests:
- Title: Non-numeric dmenu width/maxHeight become NaN and collapse the card layout
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: Math.max(1, Number(payload.width || 300)) is NaN for a truthy non-numeric width, and QML int properties coerce NaN to 0. The card lays out at zero width. The same conversion drops the maxHeight cap silently (anchor DME1).
- Root cause / remediation: Number.isFinite guard with the default fallback
- Linked requirement(s): SW-REQ-260922-FGZQ
- Reproducer / evidence tests:
- Title: Failed menu FileView reloads are asymmetric: user handler wipes last good items, default keeps stale set
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: Only the user menu FileView has onLoadFailed, and it clears userMenuItems to [] before rebuilding (anchors FV1/FV2). A transient read failure of the user extension erases every user entry until the next successful load. A failed default-file reload meanwhile silently keeps the old set. Neither behavior matches the documented behavior for a failed reload.
- Root cause / remediation: keep the last good items on failure for both files (drop the wipe), or surface the failure in the menu
- Linked requirement(s): SW-REQ-260922-50RE
- Reproducer / evidence tests:
- Title: foldedListHeight returns a height above the available cap; availableRowsHeight never clamps at zero
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: The full==1 path returns totals[0]+rowSpacing+peek with no clamp to available/maxHeight (anchor FOLD1; measured 82 against a cap of 50). availableRowsHeight can also return a negative budget (chrome larger than panel.height) that sends every branch past the cap (SX26/C36). The card clamp then cuts the visible list exactly at a row boundary, removing the fold peek the function exists to provide.
- Root cause / remediation: clamp available at >= 1px, and min the return against available (accepting the lost peek on tiny budgets)
- Linked requirement(s): SW-REQ-260922-B757
- Reproducer / evidence tests:
KI-MENU-GOBACK-ROOT-STRAND
- Title: goBack refuses to retrace while activeMenu is root, stranding navStack after a drill that landed on root
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: The route code rewrites a missing link target to root before pushing its source (Menu.qml:786-787). An explicit root link or a reload-forced root does the same. Then goBack’s root early-return (801) never pops the stack and Back is a permanent no-op (SX26/C04).
- Root cause / remediation: pop navStack in goBack even when activeMenu is root, or clear navStack when rewriting the target to root
- Linked requirement(s): SW-REQ-260922-N3RM
- Reproducer / evidence tests:
- Title: Menu guard batch runs as one bash child with no deadline; a blocking guard freezes guard answers for the session
- Severity: low (basis: reproducer)
- CVE surface: none
- Status: reviewed
- Description: Menu.qml evaluateGuards() runs every when:, checked: and disabled: guard of the menu in one child process,
guardProc.command = ["bash", "-lc", script]. Nothing puts a deadline on that process. While guardProc.running is true, each later evaluateGuards() call only sets guardsPending and returns. The batch exit then starts the deferred evaluation.
One blocking guard expression keeps the whole batch alive indefinitely. Examples are a wait on a lock, a slow probe or a hung device query.
No later evaluation then starts. For the rest of the session the menu shows the last complete answer set. Rows stay checked, visible or dimmed for a state that no longer holds. A killed batch keeps the last complete set and does not adopt a half-read one. A when: hides a row only on an explicit false, so the failure shows stale rows, not hidden ones.
pocs/47t8-guard-batch-no-deadline.sh reproduces the defect. It runs the real evaluateGuards() and guardProc handlers from Menu.qml with the real MenuModel.guardScript(). The test adds a guard that waits on a held lock. At 4s the batch still runs with zero exits, and a re-evaluation spawns nothing (guardsPending=true).
A row’s checked answer stays false after its state turns true. The control arm shows a healthy batch that lands its answers and refreshes after a state flip.
- Root cause / remediation: Give the guard batch a deadline. One option wraps the child, for example
guardProc.command = ["timeout", "-k", "1", "2", "bash", "-lc", script]. Another option is a watchdog Timer that stops guardProc after a bound and re-arms the pending evaluation. A third option is a per-guard timeout in MenuModel.guardLine, so that one slow guard cannot stall the rest of the batch.
Pair the fix with an explicit staleness policy for the answers of a killed batch. Then remove the SYS-REQ-260922-47T8 external_call_timeout_bounded deferral and add a witness. Close criterion: pocs/47t8-guard-batch-no-deadline.sh flips red, because the blocked batch exits inside the observation window.
- Affected API: shell/plugins/menu/Menu.qml evaluateGuards / guardProc (when:, checked:, disabled: guard batch)
- Linked requirement(s): SYS-REQ-260922-47T8
- Reproducer / evidence tests:
- Title: Guard batches carry no generation token: a batch computed for the previous items publishes onto the new set
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: guardProc.onExited publishes collected output with no comparison against the items generation (providerProc has a revision check, guards do not). Overlapping ids keep the previous when:false on screen. When the newer script is EMPTY the deferred path clears the maps and returns without rebuildDisplay. The open menu then stays stuck on stale visibility (SX26/C09).
- Root cause / remediation: tag each batch with an items serial and drop stale commits; rebuildDisplay after clearing the maps
- Linked requirement(s): SW-REQ-260922-MQ37
- Reproducer / evidence tests:
- Title: Guard ids interpolated unquoted into the shared bash batch: metacharacter ids break the protocol or execute commands
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: guardLine splices the id unquoted: guardScript({‘my;id’: {when:’true’}}) emits ’echo my;id:w:1’ - a second command and a broken record format (F16a2). The result parser then attributes answers to the wrong ids. Because all guards share ONE script, a single unbalanced expression makes the entire batch fail bash -n. Every row then silently keeps its last answers (F16b2). A boolean when/checked/disabled (normalizeItem preserves it) throws inside generation and aborts the script build entirely (F16c2).
- Root cause / remediation: single-quote the id into the echo (or base64 it) and sanitize/validate expressions at parse time; fail one guard without failing the batch
- Linked requirement(s): SW-REQ-260922-MQ37, SW-REQ-260922-Y58B
- Reproducer / evidence tests:
node pocs/menu-probes3.js ../shell/plugins/menu/MenuModel.js
- Title: Escape in input mode clears the entered text instead of cancelling the request
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: The Escape branch takes setFilter(’’) whenever filterText is non-empty, with no mode===‘input’ exception. In input mode the filter IS the submission, so the first Escape neither writes the done file nor cancels; the caller keeps waiting (SX26/C20).
- Root cause / remediation: route Escape straight to cancel()/finishRequest(null) in input mode
- Linked requirement(s): SW-REQ-260922-FGZQ
- Reproducer / evidence tests:
- Title: isDescendantOf reports every non-root id as a descendant of root, including ids absent from the menu
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: isDescendantOf walks parents and treats an unknown/missing parent chain as reaching root (F19: item ‘missing-id’ with parent ’not-in-tree’ reports true). The search current-vs-drilldown partition therefore groups unknown routes under the root menu, and reveal logic inherits the wrong grouping.
- Root cause / remediation: return false when the walk cannot find the claimed ancestor through existing rows
- Linked requirement(s): SW-REQ-260922-DQ9P
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: Map reads walk Object.prototype: item(), results maps, providersLoaded treat constructor/toString as real entries
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: item() is a raw truthy read (items[id] ? … : null), so inherited prototype properties look up as menu rows (probe F7). They beat aliases in resolveRoute, defeat the root fallback in openExistingMenu/setActiveMenu, and mark provider menus already-loaded (loadProviderForMenu’s providersLoaded truthiness, SX26/C21). The same class hits results maps: isDisabled/labelFor read disabledResults[id] off a fresh {} and get the inherited function. They dim and check-mark a row whose id collides (HFY2/C16).
- Root cause / remediation: hasOwnProperty checks (or null-prototype maps / Object.hasOwn) at every dynamic map read: item(), results lookups, providersLoaded
- Linked requirement(s): SW-REQ-260922-PRNV, SW-REQ-260922-50RE
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: A flat menu containing an entry id ‘items’ is parsed as a wrapper and every sibling entry is silently discarded
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: parseMenuJsonc (MenuModel.js:96-98) treats an object-valued top-level ‘items’ key as the wrapper form: {‘items’: {…}, ‘b’: {…}} parses to only the inner rows (F14). A user menu that legitimately names an entry ‘items’ loses all siblings with no diagnostic.
- Root cause / remediation: reserve the wrapper form to documents that carry ONLY an items key (or require an explicit marker), else treat ‘items’ as an entry id
- Linked requirement(s): SW-REQ-260922-E4J2
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: parseMenuJsonc reads a top-level JSON array as entries with ids 0, 1, … (omacom/omarchy#13492)
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Customer impact: If a menu file’s top level is a list instead of an object, the menu silently reads the list as entries. Items with an action appear under their own labels, items without one stay hidden, and no error says the file has the wrong shape. The shipped menu is not affected; only a hand-edited file written as a list hits this.
- Status: open
- Description: parseMenuJsonc (shell/plugins/menu/MenuModel.js:44-66) rejects scalar and null roots only. typeof [] is “object”, so an array root reaches the for..in loop. The loop iterates the array indices. Each object element becomes a row with id “0”, “1”, … under root, with the label and action of the element.
The default file and the user extension file use the same parser, and mergeMenuSources merges both. Upstream issue: omacom/omarchy#13492. Report intake validated it (report cmulr8j7z0hy31gw4pne8v6u4).
- Root cause / remediation: Reject an array root like the scalar roots, as omacom/omarchy#13968 does with an Array.isArray guard (requirement SW-REQ-260928-BMFE in the proof layer).
- Affected API: MenuModel.parseMenuJsonc root-type guard
- Linked requirement(s): SW-REQ-260922-3T3F
- Reproducer / evidence tests:
sh test/reports/report-cmulr8j7z0hy31gw4pne8v6u4.sh
- Title: A whole-line comment in a CR-only menu file swallows the rest of the file
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: The comment pass in stripJsonc (shell/plugins/menu/MenuModel.js) is /^\s*//[^\n](\n|$)/gm. With the m flag, ^ also matches after a lone CR. But [^\n] stops only at LF. Some editors save a file with CR-only line endings (classic Mac style).
In such a file the first whole-line // comment runs to the end of the file. The pass deletes the rest of the document, JSON.parse fails, and parseMenuJsonc returns an empty item set. We checked this live under Quickshell 0.3.1. FileView keeps CR as read.
The real MenuModel.js parses a CR-only file with one comment line to 0 rows. LF and CRLF files parse correctly, and so do CR-only files without comments. pocs/menu-jsonc-cr-line-endings.sh reproduces it with LF, CRLF and comment-free controls. test/shell.d/menu-test.sh pins it as a green tripwire.
- Root cause / remediation: End a // comment at any line terminator that the m flag uses: CR, LF, U+2028 and U+2029. Or normalise CR and CRLF to LF before the comment pass. omacom/omarchy#13968 does not change this, because its comment drop also stops only before LF. Close criterion: pocs/menu-jsonc-cr-line-endings.sh exits 0 and the KI-MENU-JSONC-CR-LINE-ENDINGS tripwire in test/shell.d/menu-test.sh flips red.
- Affected API: MenuModel.stripJsonc comment pass (default and user menu JSONC)
- Linked requirement(s): SW-REQ-260922-E4J2
- Reproducer / evidence tests:
sh pocs/menu-jsonc-cr-line-endings.sh
- Title: stripJsonc removes only whole-line // comments; an inline comment tail empties the whole file (omacom/omarchy#13493)
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Customer impact: One // comment after content on a line, for example after an entry, makes the whole menu file unreadable, so every entry in it disappears: all of your own additions in the extension file, or the whole menu in the default file. Nothing in the menu or the journal says why. Comments on their own line work, and no data is changed.
- Status: open
- Description: The stripJsonc comment pass (shell/plugins/menu/MenuModel.js:4) matches only at the start of a line. A // comment after content on the same line stays in the text. JSON.parse then rejects the file, and parseMenuJsonc returns no rows.
One inline note in the user extension drops every user entry; in the default file it drops every row. docs/menu.md documents this limitation. Upstream issue: omacom/omarchy#13493. Report intake validated it (report cmulr8j7w0hy01gw4menggvbx).
- Root cause / remediation: Strip a // comment wherever its opener sits outside a string, without breaking the trailing-comma-then-comment-line shape, as omacom/omarchy#13968 does (requirement SW-REQ-260928-C8W1 in the proof layer). The earlier single-pass attempt (omacom/omarchy#13512, closed) broke that shape; #13968 supersedes it.
- Affected API: MenuModel.parseMenuJsonc / stripJsonc whole-line comment pass
- Linked requirement(s): SW-REQ-260922-E4J2
- Reproducer / evidence tests:
sh test/reports/report-cmulr8j7w0hy01gw4menggvbx.sh
- Title: stripJsonc has no block-comment pass: a /* */ comment empties the whole file
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: Upstream stripJsonc (shell/plugins/menu/MenuModel.js:2-6) removes whole-line // comments and trailing commas only. A /* */ block comment stays in the text. JSON.parse rejects it, and parseMenuJsonc returns an empty item set for an otherwise valid menu file. docs/menu.md documents whole-line // comments only, so block comments are outside the documented grammar, but nothing tells the user why their entries disappeared. omacom/omarchy#13968 keeps block comments unsupported and documents that, so this issue stays open after it lands. CR-only line endings and non-ASCII whitespace are separate issues (KI-MENU-JSONC-CR-LINE-ENDINGS, KI-MENU-JSONC-UNICODE-WHITESPACE).
- Root cause / remediation: Add a string-aware block-comment pass ahead of the comma pass, or document block comments as unsupported
- Linked requirement(s): SW-REQ-260922-E4J2
- Reproducer / evidence tests:
sh pocs/menu-jsonc-block-comment.sh
- Title: Keybindings menu build has no deadline on hyprctl binds or xkbcli compile-keymap; a stalled callee blocks the menu
- Severity: low (basis: reproducer)
- CVE surface: none
- Status: reviewed
- Description: bin/omarchy-menu-keybindings builds the keybindings menu from two external calls without a deadline. The first is
hyprctl binds, which the script reads for the cache key and again in dynamic_bindings. The second is xkbcli compile-keymap </dev/null, the keymap_cmd that parse_keycodes reads through gawk getline. A stalled Hyprland IPC or a wedged keymap compile blocks the menu build for as long as the callee blocks. The keybindings menu then never opens, and the script never exits.
On the stalled-hyprctl path the script calls hyprctl binds three times in sequence: cache key, cache refresh, and the uncached fallback after the refresh fails. A per-call bound therefore costs about three times its value. pocs/9dms-keybindings-no-timeout.sh reproduces both arms on the real script in –print mode. With hyprctl stalled, the build is still blocked at a 10s watchdog (rc=124). With xkbcli stalled, the build is still blocked at a 5s watchdog (rc=124). The control arm builds the stubbed bind row in about a second, with its keycode resolved through the stubbed keymap.
- Root cause / remediation: Wrap both external calls in bin/omarchy-menu-keybindings with a timeout. For example, use
timeout 2 hyprctl binds at both call sites and keymap_cmd = "timeout 2 xkbcli compile-keymap </dev/null". The existing fallback keycode table already covers a missing keymap. Consider one hyprctl binds read per run, so that a stall costs one timeout and not three.
Then remove the SW-REQ-260922-9DMS external_call_timeout_bounded deferral and add a witness for the bounded paths. Close criterion: both stall arms of pocs/9dms-keybindings-no-timeout.sh finish inside their watchdogs, and the tripwire flips red.
- Affected API: bin/omarchy-menu-keybindings (keybindings menu build: hyprctl binds, xkbcli compile-keymap)
- Linked requirement(s): SW-REQ-260922-9DMS
- Reproducer / evidence tests:
- Title: Refused key-up after an accepted key-down leaves a synthetic key stuck down and skips the sendshortcut fallback
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: dispatch_sendshortcut_binding commits on the down call (bin/omarchy-menu-keybindings:635), ignores the up call’s failure (line 637) and returns unconditionally (line 638). When the compositor accepts the down but refuses the up, the synthetic key stays in the down state. That is the exact stuck-state the clipboard helper’s comment warns about. The code then never tries the sendshortcut fallback at line 641.
- Root cause / remediation: Treat the up failure as a failed dispatch: return its status so the fallback chord runs, or send a compensating key-up before falling back
- Linked requirement(s): SW-REQ-260922-0W96
- Reproducer / evidence tests:
- Title: openRoute follows a link one hop and never re-inspects the target kind: link-to-action opens as a menu
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: After id = entry.target there is no second kind test and no loop (SX26/C18). A link whose target is an action falls into openExistingMenu instead of runAction, and a chained link stops at the intermediate id.
- Root cause / remediation: loop the link resolution (with a hop cap) and run the action branch on the final kind
- Linked requirement(s): SW-REQ-260922-N3RM, SW-REQ-260922-XW52
- Reproducer / evidence tests:
- Title: normalize-before-merge wipes unspecified defaults: a label-only user entry erases action/aliases/guards
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: normalizeItem materializes every omitted field as an empty default BEFORE mergeMenuSources’ per-key overlay. The user’s normalized row therefore carries explicit empty action/aliases/when/checked/disabled/icon keys that overwrite the default row’s values (F10: merged action becomes ‘’). The normalization step in the rebuild pipeline defeats the per-key override contract (‘a later source overrides only the keys the author declared’).
- Root cause / remediation: merge the RAW user entry over the default before normalizing (or make the merge skip empty-string/empty-array values from the overlay source)
- Linked requirement(s): SW-REQ-260922-7NPE
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: mergeMenuSources stores prototype-named ids but drops them from itemOrder - the row is written and never rendered
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: mergeMenuSources’ presence test !nextItems[entry.id] is truthy for inherited properties. An id like ‘constructor’ therefore skips the nextOrder push while the assignment still stores the row (F9). proto additionally invokes the prototype setter. Later order-walks iterate itemOrder and never see the entry.
- Root cause / remediation: use Object.hasOwn / hasOwnProperty for the presence test and a non-prototype map (Object.create(null)) for storage
- Linked requirement(s): SW-REQ-260922-7NPE
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: mergeAppRows/swapProviderRows keep stale .order values and mutate caller-owned rows in place
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: Both merges copy kept rows by reference without restamping .order. After any row leaves the set, the survivors’ order values no longer match their itemOrder indexes (F11a/F12b) - searchScore’s declaration-order tiebreak then compares stale numbers. Both merges also write row.order/providerMenu into the CALLER’s row objects (F11b). That is exactly the in-place write the module header forbids (‘an in-place write into such an object is occasionally dropped by the engine’).
- Root cause / remediation: shallow-copy kept rows and restamp order for every survivor; never write into the row objects handed in
- Linked requirement(s): SW-REQ-260922-Z680
- Reproducer / evidence tests:
node pocs/menu-probes2.js ../shell/plugins/menu/MenuModel.js
- Title: normalizeItem edge cases: null parent kept, boolean guards preserved, action:0 mis-kinded, numeric labels
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: normalizeItem infers the parent only when the parent field equals undefined (F17a). An explicit null parent therefore stays null, and the path helpers then coerce it. A boolean when/checked/disabled passes the ||-default and later throws inside guard generation (F17b). Kind derives from truthiness so action:0 yields a menu row (F17c). The code stores a numeric label as a number. A non-string alias stringifies to ‘[object Object]’, which the search index then matches for the term ‘object’ (F23).
- Root cause / remediation: treat null like undefined for parent, coerce when/checked/disabled to strings or reject non-strings, String() the label, and filter aliases to strings
- Linked requirement(s): SW-REQ-260922-46HY
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: A menu summon leaves a pending uninstall question on top of the new prompt
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Customer impact: If something summons the menu while an uninstall question is open, the question stays on top of the new prompt. The dialog preselects Uninstall, so the Enter you meant for the prompt uninstalls the app. Omarchy deletes your own, web-app and TUI launchers with no further prompt; for a package, it opens the uninstall terminal. The prompt’s caller gets a cancel, so the action behind it never runs.
- Status: open
- Description: Every summon enters through open(), which calls openDmenu(), or openRoute() and then openExistingMenu(). These replace the mode, rows and request files while opened stays true, and none of them clears deleteConfirmOpen or deleteTarget (anchor CNF1). Only onOpenedChanged clears them, when the menu closes. The keyCatcher still sends every key to the dialog, which preselects Uninstall. Enter then runs confirmDelete(): cancel() finishes the new request with no selection, and appLibrary.remove() uninstalls the app.
- Root cause / remediation: Call cancelDelete() in open(), where every summon enters, when a question is pending. PR #14054 does this.
- Linked requirement(s): SW-REQ-260922-8CQ4
- Reproducer / evidence tests:
- Title: open() throws on a JSON null payload: parse succeeds, the catch does not run, the property read throws
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: JSON.parse(’null’) returns null without throwing, so the try/catch in open() does not cover the dereference. payload.fontFamily throws TypeError and no mode dispatch runs, so nothing answers the summon lifecycle (anchors; SX26/C05 reproduced the null hole).
- Root cause / remediation: guard the parse result with an object type check before dereferencing
- Linked requirement(s): SW-REQ-260922-50RE
- Reproducer / evidence tests:
- Title: Opening a select menu walks its rows quadratically (omacom/omarchy#10601)
- Severity: low (basis: risk · performance)
- CVE surface: none
- Customer impact: Opening a long select menu does work that grows with the square of its length, because the menu measures the whole list again after each row is added. Menus of tens of rows open without a visible delay, but the 231-row keybindings menu (Super+K) costs 27,027 row visits each time it opens. Nothing breaks; the menu only opens more slowly.
- Status: open
- Description: rebuildDmenuDisplay (shell/plugins/menu/Menu.qml) appends rows to displayModel one at a time. The visibleRowsHeight binding evaluates again after each append. Each pass calls foldedListHeight over the full row list.
A menu of N rows thus costs about N(N+1)/2 row visits. 231 rows take 232 height passes and 27,027 row visits. 462 rows take 463 passes and 107,415 visits.
The Super+K keybindings menu is the realistic large case. Upstream issue: omacom/omarchy#10601. Report intake validated it (report cmulr8ysi0jwr1gw4fjmt9khq). Upstream PR 10631 does not change the counts.
- Root cause / remediation: Append the rows in one model update, or defer the height binding until the rebuild ends. Also add a requirement that bounds the menu-open cost; none exists today
- Affected API: Menu.qml rebuildDmenuDisplay + visibleRowsHeight binding (omarchy-menu-select summon)
- Linked requirement(s): SW-REQ-260922-B757
- Reproducer / evidence tests:
sh test/reports/report-cmulr8ysi0jwr1gw4fjmt9khq.sh
- Title: pathFor coerces a null parent to the id ’null’/‘0’ row while parentPathFor returns empty
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: pathFor walks through item(items, null), whose key coercion can land on a row literally keyed ’null’/‘0’ and appends its label (F21). parentPathFor returns [] for the same entry - one breadcrumb shows a phantom parent, the other shows none.
- Root cause / remediation: guard the walk on non-empty string parents and make both helpers share one walk
- Linked requirement(s): SW-REQ-260922-SJ7P
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: pendingInitialMenu is stored and never read: a route summoned before the JSONC loads opens root and stays there
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Customer impact: A menu key pressed while the shell is starting opens the main menu instead of the menu you asked for. This happens right after login, or after omarchy-restart-shell, which omarchy update runs. An action shortcut such as Super+Ctrl+R also opens the main menu, and its action never runs. Pressing it again a second later works.
- Status: open
- Description: openRoute stores the resolved id in pendingInitialMenu (Menu.qml:975) and nothing reads it (anchor PIM1: exactly two occurrences). With items still empty at summon time, resolveRoute returns the literal, item() misses, openExistingMenu rewrites to root, and after onLoaded nothing applies the stored route. A misspelled route also silently becomes root, undoing resolveRoute’s documented literal fallthrough in the same turn.
- Root cause / remediation: Apply pendingInitialMenu once both menu files have loaded, or block the first open until they load. PR #14055 does the first.
- Linked requirement(s): SW-REQ-260922-N3RM, SW-REQ-260922-50RE
- Reproducer / evidence tests:
- Title: Failed/killed providers commit their partial stdout and stay marked loaded; apps provider loads with no AppLibrary
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: providerProc.onExited merges collected stdout whenever the revision matches, with no exitCode/exitStatus check. The guardProc beside it does check (asymmetry in the same file, anchors PRV1). A failed script therefore replaces a good row set with a partial read. The code commits providersLoaded BEFORE the process proves itself (PRV2) and never clears it on failure. The submenu therefore stays stuck until a full source rebuild. The apps branch marks providersLoaded unconditionally even when appLibrary is null and mergeAppRows returns empty (PRV3), leaving an empty Apps menu with no retry.
- Root cause / remediation: check exitCode/exitStatus before merging (keep last good rows otherwise), set providersLoaded only on success, and re-run mergeAppRows when appLibrary appears
- Linked requirement(s): SW-REQ-260922-EFNR, SW-REQ-260922-Z680
- Reproducer / evidence tests:
- Title: Provider rows colliding with a static id are silently dropped: takenIds only records batch-minted ids
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: mergeProviderRows nudges slug collisions only within the current batch (takenIds is batch-local, QML anchor PRV4). A generated id that matches a STATIC item already in the menu reaches swapProviderRows unchanged. Its nextItems guard drops it (F13: the provider version of a colliding id vanishes without a trace).
- Root cause / remediation: seed takenIds with the ids already in root.items (or make swapProviderRows nudge incoming collisions)
- Linked requirement(s): SW-REQ-260922-Z680, SW-REQ-260922-EFNR
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: dmenu request lifecycle drops waiters on re-summon, on a busy resultProc, and for doneFile-only requests
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Customer impact: Summoning a menu again before answering the previous one, for example by pressing Super+K several times, leaves the earlier caller waiting forever. Each one stays behind as a background process, and the action that opened it never completes. The same happens when the menu process dies while a caller waits. Later menus still open and work.
- Status: open
- Description: A second open without close() replaces selectionFile/doneFile/requestActive with no write to the previous done file - the first caller waits forever (SX26/C06/C07; openExistingMenu/openDmenu bodies). This is omacom/omarchy#9057. The behavioural reproducer test/reports/report-cmulr95nw0kqr1gw46i7f50xj.sh drives two real omarchy-menu-select callers through the real Menu.qml open/openDmenu/cancel bodies. The superseded caller never gets its done file and polls forever. Upstream e332dc97 has no cancel-prior guard; PR 9056 adds one.
finishRequest wipes requestActive and both paths BEFORE resultProc.running = true. Quickshell therefore ignores a command change while the previous write runs, and the new selection disappears (SX26/C14; the empty-selectionFile redirect variant is SX26/C15 and HFY2/C36).
- Root cause / remediation: Finish (or sentinel) the outstanding request before replacing its files; queue the write when resultProc is running; treat a doneFile-only request as cancel-only. #9056 (head b7bd59c4) answers a superseded request and fixes the busy-writer race; a deadline in omarchy-menu-select is still needed for a dead menu process.
- Linked requirement(s): SW-REQ-260922-FGZQ, SW-REQ-260922-C8HX, SW-REQ-260922-3VTN, SYS-REQ-260922-X6Z5
- Reproducer / evidence tests:
sh test/reports/report-cmulr95nw0kqr1gw46i7f50xj.sh
- Title: resolveRoute folds the input before the exact-id lookup and returns the normalized string as fallthrough
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: resolveRoute (MenuModel.js:220-234) lowercases and underscore-folds the input BEFORE the exact-id test, so a ‘setup’ route misses the stored id ‘Setup’ (F8a). The misspelling fallthrough returns the NORMALIZED string instead of the literal input the contract states (F8b). The code returns an exact app id despite the header comment ‘app rows are never routable’ (F8c). And an alias named like a prototype property can never win because item() already reports a hit.
- Root cause / remediation: test the exact id before folding (or fold ids at normalize time), return the original input on fallthrough, and skip kind===‘app’ on the exact-id hit
- Linked requirement(s): SW-REQ-260922-PRNV, SW-REQ-260922-CYB9, SW-REQ-260922-74BZ
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: revealCursor loses the directional peek: an uninstantiated delegate skips it and the underhang write undoes it
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: itemAtIndex returning null (delegate not built) aborts before any adjustment (anchor REV1). With a delegate, the overhang (down) and underhang (up) writes both run unconditionally. On a short viewport the second therefore undoes the first, and the cursor row parks flush with the viewport edge. That is the exact state the Contain comment forbids. positionViewAtIndex can also apply deferred contentY over the manual tweak.
- Root cause / remediation: carry travel direction into select()/revealCursor and apply only the matching side; force delegate instantiation (or compute from geometry) before adjusting
- Linked requirement(s): SW-REQ-260922-Z48F
- Reproducer / evidence tests:
- Title: searchScore throws on a label-less merged row and returns a NaN rank that displayRow coerces to the best tier
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: searchScore calls entry.label.toLowerCase() and throws for a row without a label - mergeAppRows accepts rows carrying only an id (F15a). A missing entry.order makes the rank NaN (F15b), and displayRow’s NaN handling sorts the row into the top band. Both row shapes are producible through the real merge pipeline.
- Root cause / remediation: default label to the id and order to a large sentinel (or Number.isFinite guard) in searchScore; have mergeAppRows require a label
- Linked requirement(s): SW-REQ-260922-SJ7P
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: searchScore ranking anomalies: apps promoted in-tier, root exact-labels swamped, multi-term matches collapse
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: Executed probes: an app and a menu row with equal scores rank app-FIRST (the -5 within-tier nudge). That violates the contract in the code comment ‘App rows sort after all menu items’ (F15d). An exact label at root scores base 2 while the identical exact label nested deeper scores base 0 (F15f). The root row therefore ranks below its nested twin, and the depth tie-break loses effect. A multi-term query that matchesQuery accepts across fields (one term on the label, one on the description) scores in the unmatched baseline band. That ranks it below a single-field pure match (F15e2 - measured 78000 for the accepted query).
- Root cause / remediation: give multi-term queries an aggregation across per-term scores; make the root/nested exact-label bands consistent; reconcile the app nudge with the documented ordering
- Linked requirement(s): SW-REQ-260922-SJ7P
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: Search text misses: whole-word description matching drops punctuation-glued words and hyphenated alias forms
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: termInSearchWords requires whole-word equality, so a description word with trailing punctuation (‘system.’) never matches the term (F24a). A mixed-case term likewise never matches the lowercased haystack (F24b). matchesQuery’s name path only sees the separator-folded rewrite of aliases and leaf ids. Querying the exact hyphenated form (‘power-menu’) therefore misses even though the author declared the alias that way.
- Root cause / remediation: strip edge punctuation before the whole-word compare and keep the untokenized alias/id text as an additional nameText component
- Linked requirement(s): SW-REQ-260922-DQ9P
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
- Title: Geometry flags silently coerce non-numeric and flag-like values: –width abc summons width 0
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: parse_arguments accepts any following word as a geometry value (arity check only, bin/omarchy-menu-select:42) and encode_payload’s Perl int() silently coerces non-integers (int(abc)=0, int(10.9)=10, line 86). parse_arguments consumes a value that names a known flag (–width –height 400) as the width and drops the 400 as an unknown token. The summon proceeds with corrupted geometry and no error.
- Root cause / remediation: Validate geometry values at parse time: reject non-numeric tokens and a value that matches a known flag, with a usage error
- Linked requirement(s): SW-REQ-260922-Q6ZS
- Reproducer / evidence tests:
KI-MENU-SELECT-HANDSHAKE-TMP-LEAK
- Title: Failed second mktemp leaks the selection handshake temp file: cleanup trap installed only after both files exist
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: prepare_handshake_files creates selection_file (line 72) then done_file (line 73) but installs the EXIT cleanup trap only at line 75. If the second mktemp fails, set -e exits before the trap exists. The first temp file - the private selection file - stays behind in TMPDIR.
- Root cause / remediation: Install the EXIT trap before the first mktemp, wiring the variables as the code assigns them (trap ‘rm -f “${selection_file:-}” “${done_file:-}”’ EXIT)
- Linked requirement(s): SW-REQ-260922-Q6ZS
- Reproducer / evidence tests:
- Title: Payload fontFamily is sticky: a later open that omits it keeps the previous caller’s font
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: The code writes fontFamily only when the payload field is truthy, and no path restores the style default (SX26/C44). Two different summons therefore inherit the first caller’s face.
- Root cause / remediation: reset fontFamily to the Style default in cancel()/openRoute when the payload omits it
- Linked requirement(s): SW-REQ-260922-50RE
- Reproducer / evidence tests:
- Title: summonAction rewrites an explicitly empty quoted payload to {} - the in-process summon diverges from the bash bytes
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: summonAction’s regex captures ’’ for an explicit empty payload and the ‘match[2] || “{}”’ fallback rewrites it (F22). The bash path would instead pass the empty argument. The mcdc note itself names argv divergence as the forbidden defect.
- Root cause / remediation: distinguish ‘absent group’ from ’empty string’ via match[2] === undefined
- Linked requirement(s): SW-REQ-260928-8VJQ
- Reproducer / evidence tests:
node pocs/menu-probes.js ../shell/plugins/menu/MenuModel.js
KI-SLEEP-LOCK-BUDGET-ARG-OCTAL
- Title: argv budget with a leading zero (08/09) bypasses the validation guard via an octal arithmetic error
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: The argv budget guard (bin/omarchy-system-sleep-lock:45) matches ^[0-9]+$ then evaluates (( budget_ms < 1 )) in bash arithmetic, which reads a leading zero as octal. ‘08’/‘09’ raise ‘value too great for base’, the guard compound goes false, and the script keeps the literal token as its budget. The deadline assignment at line 52 errors the same way, and every lock_ipc fails its remaining guard. The run ends in report_unsecured claiming ‘within 08ms’ with no lock request issued. ‘010’ is silently accepted as octal 8.
- Root cause / remediation: Force decimal (10#) in the guard and deadline arithmetic, or reject any budget token bash cannot evaluate in base 10 with a usage error
- Linked requirement(s): SW-REQ-260912-FAWV
- Reproducer / evidence tests:
KI-SLEEP-LOCK-DEADLINE-INVARIANT-GAPS
- Title: Header invariant ’every call bounded by the budget remainder’ unenforced on three external calls
- Severity: low (basis: risk · correctness)
- CVE surface: none
- Status: open
- Description: The script header promises the deadline enforces itself on every call, but three sites break it. (1) sync_clamshell uses a fixed timeout 0.4s+0.1s and never consults remaining_ms - with a 100ms budget it runs the script 4x past the deadline. (2) derive’s busctl read gets a full 1s timeout but the deadline clock starts only at line 52. A slow reply therefore consumes exactly the 1000ms reserve held back for logind (lifetime measured at 5102ms against a 5000ms window). (3) report_unsecured’s omarchy-notification-send has no timeout wrapper and ‘|| true’ does not bound the sd-bus default (~25s). The unlocked-suspend warning can therefore stall for the whole remaining window and never land.
- Root cause / remediation: Clamp the clamshell timeout to remaining_ms. Start the deadline clock before the derive read (or charge the read to the budget). Wrap the notification send in a short timeout
- Linked requirement(s): SW-REQ-260912-FAWV, SW-REQ-260912-H2YF
- Reproducer / evidence tests:
Appendix: Data-Gap Analysis
Fields this report had to infer or reconstruct because the model does not carry
them as first-class data. Occurrences = records hitting the gap in this run.
| Field |
Severity |
Occurrences |
Came from |
Proposed model addition |
KnownIssue.resolved_at |
lossy |
0 |
inferred from history[].at of the entry whose detail matches a status->fixed transition |
add resolved_at string (RFC3339), set when proof known-issue edit --set-status fixed records the transition |
KnownIssue.resolved_in |
blocking |
0 |
reconstructed from non-model fixing_reference: key, else scavenged #PR/SHA from history detail or remediation prose |
add resolved_in string (fixing commit-ish); promote the de-facto fixing_reference YAML key into the model so it stops being dropped on load |
KnownIssue.created_at |
lossy |
0 |
inferred from the first history[] entry (action=created) |
add created_at string; age/SLA math currently depends on a history convention |
ProblemReport.detected_at |
lossy |
0 |
source.date, else regression.detected_at (when a date), else first history entry |
add a report-level detected_at; regression.detected_at is overloaded (sometimes a SHA, not a date) |
ProblemReport.regression.dwell |
cosmetic |
0 |
read directly when present; frequently empty — not derivable without bisect |
auto-derive dwell from introduced_in..fixed_in via git, or require it on regression closure |