# Changelog All notable changes to Veil are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [1.1.1] — 2026-08-26 ### Security - Panel text defaulted to rich text, so an HTML-like window class or a helper error could make the shell fetch a resource of the app's choosing. Every `Text` item now pins `textFormat: Text.PlainText`; the engine is unchanged. ## [1.1.0] — 2026-08-26 ### Security - State values and CLI arguments could execute shell commands. All arithmetic now happens in jq, and CLI integers are validated before use. - Every predictable path Veil opens — state, rules, lock, migration temp — is checked first: a FIFO at any of them hung the helper forever, and a symlink at the rules or lock path truncated its target. Migration is now done in memory, and a symlinked state file is followed rather than replaced. - Foreign JSON at the state path or the legacy adoption path was adopted and overwritten by read-only commands, including a document holding one Veil-shaped value. It is now refused and left untouched. - `1e999` in the state file became `opacity = "+inf"` in the generated config. State is normalised once at load into integers in range. - Window classes are byte-measured as documented, refused rather than truncated over the ceiling, rejected when they contain control characters, and escaped on output. Window addresses are validated before they reach Lua. - The state file is created `0600`, and the integer guard runs under `LC_ALL=C`. - Live apply carries one 20s budget; per-call timeouts allowed 42 minutes. ### Fixed - The generated default rule matched every window and loads after Omarchy's, so it overrode Omarchy's opacity opt-outs — picture-in-picture, the webcam overlay, Steam, QEMU, RetroArch, DaVinci Resolve, browser video. It now matches the `default-opacity` tag; per-app rules still override by class. - A jq failure mid-write left an empty state file, and unreadable state produced a fully transparent desktop that survived reboot. State is validated before it is written, and unreadable state is backed up and reset. - No read-only command created `veil.lua`, so `require("hypr.veil")` aborted the rest of `hyprland.lua`. Every invocation creates it, and the rules are regenerated whenever state is rewritten. - Concurrent commands lost updates; state access is serialised with `flock`. - Setting or nudging a value merged `enabled: true` into the app entry, so a keybind or menu preset silently undid `veil app off`. Neither touches `enabled` now; a new entry still arrives enabled. - Every subcommand validates its argument count. `veil reset ` used to wipe every setting at exit 0. - v1 state was mis-migrated by partial documents, a null `defaults`, a stray v1 key on a v2 document, and a v1 file never written forward on disk. - On a large state file the live apply touched no window: the whole state went through one 128 KiB-capped argv entry, and now goes by file descriptor. - Failures are reported instead of swallowed: a failed `hyprctl reload`, an unreadable window list, one malformed window, skipped app entries, and a rules directory that is not writable — checked before state is committed. - Every keypress ran a full `hyprctl reload`, monitors included; Veil only ever regenerates window rules, and now reloads `config-only`. - The rules file is written temp-and-rename, so a truncated config is never readable at boot, and a symlinked `~/.config/hypr` or `veil.lua` works again. - The README's uninstall procedure did not restore opacity. It now runs `veil reset` first and then raises the defaults to 100%: `reset` leaves them at 98/96, and raising them alone does nothing for an app with its own entry. - `--help` lists every subcommand, and `--version` was added. - Error messages that named the wrong cause now report the real one. - Panel: engine errors are shown instead of vanishing, input during a running command is queued per control instead of dropped, sliders no longer snap back on release, and a path containing a space no longer breaks every subprocess. ### Changed - The default for a fresh state file is 98/96, matching Omarchy's own; at 50/50 the first reload after installing took the whole desktop to half opacity. `veil reset` restores 98/96, and saved settings are unaffected. - State is read once per invocation rather than once per lookup, and the rules file is generated by one jq program rather than a shell loop. - The per-app cap refuses a new entry rather than silently discarding entries. ### Removed - No keystroke wipes every setting; a full reset is the explicit button only. - The over-cap window-list warning, which cost a re-parse on every keypress. - `docs/panel.png`, a byte-identical duplicate of `preview.png`. - Dead code: an unused helper function and an unused jq argument. ## [1.0.2] — 2026-08-25 ### Security Bounded the two remaining paths that were unbounded **in time** rather than in size. Raised in the Omarchy marketplace follow-up review of commit `ae23756`. - **A FIFO at either predictable write path would hang the helper forever.** `[[ -f ]]` is false for a FIFO, so `ensure_state()` read a FIFO planted at the state path as "absent" and then redirected generated JSON into it, blocking on `open()` with no reader. The generated rules path had the same shape. Both are now checked with `require_regular()`, which refuses anything that exists but is not a regular file. > **Correction (1.1.0):** this entry originally claimed symlinked setups were > unaffected. That was wrong for the state file, which was replaced rather > than followed, silently orphaning a dotfile-repo copy. It is fixed in 1.1.0. - **`hyprctl` had no deadline** and could stall before the byte-bounded pipeline was ever reached. Every external process call now runs under `timeout`: `HYPRCTL_TIMEOUT` (5s) for each `hyprctl` invocation, `NOTIFY_TIMEOUT` (3s) for the desktop notification. Verified: a FIFO at either path is refused in under a second where it previously blocked indefinitely, and a `hyprctl` stub that never returns costs 10s across a full apply instead of hanging. The panel is covered transitively, since both commands it spawns now terminate on their own. ## [1.0.1] — 2026-08-25 ### Security Bounded every untrusted input before it reaches `jq`, a shell variable, or QML collection. Raised in the Omarchy marketplace security review of commit `9ca3d6f`. Two inputs are influenceable by something other than Veil, and neither was capped: - **The state file** (`~/.local/state/veil/opacity.json`) is an ordinary user-writable file, and every command parsed it whole through `jq`. - **`hyprctl clients -j`** carries window metadata — class, title, address — whose length is chosen by the applications that own those windows. It was fetched twice per apply and piped straight into `jq` and then into shell variables. Either could be made large enough to exhaust the helper or the shell that spawned it. Ceilings are now applied at the producer, before `jq` allocates a document and before a shell variable holds one: | Ceiling | Value | Applies to | | --- | --- | --- | | `MAX_STATE_BYTES` | 256 KiB | state file, size-checked then `head -c` capped | | `MAX_CLIENTS_BYTES` | 2 MiB | `hyprctl clients -j`, `head -c` capped pre-parse | | `MAX_WINDOWS` | 256 | windows processed per apply | | `MAX_APPS` | 512 | per-app entries honoured from state | | `MAX_CLASS_LEN` | 256 B | window class accepted, stored, or emitted | Design notes: - **Oversized input is refused, not truncated.** A clipped JSON document is not valid JSON, and rewriting someone's settings from a partial parse is worse than failing. An over-cap window list is treated as "no clients" so a partial list is never acted upon. - **The panel caps independently of the helper**, rather than trusting the helper's own bounds. - The ceilings sit far above any legitimate value — a real state file is well under 4 KiB and a busy Hyprland session reports a few tens of KiB — so normal use is unaffected. ### Changed - `hyprctl clients -j` is now fetched once per apply instead of twice. ## [1.0.0] — 2026-08-25 Initial release. - Bar widget with a flyout panel: per-app focused and unfocused opacity, plus defaults for apps you haven't customised. - `bin/veil`, the engine behind the widget. The panel, the optional Omarchy menu rows, and the optional keybinds all shell out to it, so no two surfaces can disagree about state. - State in `~/.local/state/veil/opacity.json`. Every app carries its own focused and unfocused value; an app with no entry inherits the defaults. Disabling an app applies 100% to both while remembering its numbers. - Settings persist across reboots: each change regenerates `~/.config/hypr/veil.lua`, required from `hyprland.lua`, so windows opened later inherit the current state with nothing running at login. Live windows additionally receive `set_prop` dispatches for instant effect. - Optional keybind and Omarchy menu snippets under `examples/`. [1.1.1]: https://github.com/Carasibana/omarchy-veil/releases/tag/v1.1.1 [1.1.0]: https://github.com/Carasibana/omarchy-veil/releases/tag/v1.1.0 [1.0.2]: https://github.com/Carasibana/omarchy-veil/releases/tag/v1.0.2 [1.0.1]: https://github.com/Carasibana/omarchy-veil/releases/tag/v1.0.1 [1.0.0]: https://github.com/Carasibana/omarchy-veil/releases/tag/v1.0.0