11 Commits
Author SHA1 Message Date
Ryan Hughes 3b0d9b5734 Merge remote-tracking branch 'origin/quattro' into harden-notification-exec-argv
# Conflicts:
#	test/shell.d/notification-send-test.sh
2026-08-23 18:31:42 -04:00
Ryan Hughes 8f245e59dc Restore notify-send option parity and prove legacy fail-closed
The direct-Notify rewrite dropped notify-send options that callers rely on, which
the momus review caught: omarchy-display-text-size uses -r/-p to refresh one
toast in place, and the acceptance suite uses the --expire-time=15000 equals
form. Re-add -r/--replace-id (replaces_id), -p/--print-id (emit the returned id),
and the --flag=value form for every long option; a dash-leading description like
"-50% off" is now kept as body text rather than erroring, and --exec "" is
rejected.

Add a fixture proving the deliberate upgrade behavior: a popup persisted by a
pre-upgrade shell with a legacy `exec` shell string restores with an inert click
(execArgv empty, the old string never run) rather than executing it.
2026-08-23 17:18:58 -04:00
Ryan Hughes e3729a385b Send notifications via the Notify D-Bus method, never notify-send
omarchy-notification-send now calls org.freedesktop.Notifications.Notify
directly with `busctl --user`, instead of shelling out to notify-send. Each
value is one typed D-Bus parameter, so there is no argv/option layer that could
reinterpret a relayed headline like `--hint=…` or `-rf` as an option or a hint:
the summary and body are strings, and omarchy-exec-argv is built only from
--exec. A leading `--` keeps busctl's own getopt from reading a dash-leading
value as a busctl option.

Map -i to app_icon, -t to expire_timeout, and urgency to the byte hint; unknown
options are now a hard error rather than a silent pass-through. Route the unused
hypr o.notify helper and the sample hooks through the wrapper too, and tighten
the bin-style test so nothing under bin/ may call notify-send. The test stubs
busctl and trips if notify-send is invoked.
2026-08-23 17:02:29 -04:00
7e469f962d Let a received Taildrop file wait to be answered (#7953)
* Let a received Taildrop file wait to be answered

A delivery can land hours after it was sent, and the toast announcing it was expiring after five seconds -- so a file that arrived while nobody was at the machine was gone from the screen before anyone could click it open. Critical urgency is what the shell reads as a popup that lives until it is clicked or dismissed, the same thing omarchy-crash-watch uses to keep its click-to-diagnose toast around.

The wrapper takes options after the headline and description as well as before, which is how this argument list is built. That path had no test, and it fails quietly rather than loudly: the wrapper appends its own default urgency last, so an urgency it stopped parsing would reach notify-send as `-u critical ... -u low` and the toast would go back to expiring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Say it in the commit message, not above the code

`-u critical` next to a line that builds a notification says what it does, and the five lines explaining why it is there were a recap of the change rather than something the code could not say. The reasoning stays where it belongs, in the commit that made the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-23 22:12:07 +02:00
Claude Opus 5 (1M context)andCodex XHigh 1b15120d27 Keep relayed text out of notify-send's option parser
The argv click command closed injection through the hint's value, but the
sender still handed the headline and description to notify-send bare. A
value beginning with a dash is parsed there as flags, and one shaped like
`--hint=string:omarchy-exec-argv:[...]` is read as a hint of its own --
libnotify keys hints in a hash table, so the later of two replaces the
earlier and a forged headline outranks the vector --exec built.

That is reachable without any --exec in sight: omarchy-tailscale-send
passes a single file's basename verbatim as the description, so a file
named like the hint gives its click action to whoever chose the name.

Put the headline and description behind a `--` so notify-send reads them
as text, and refuse any pass-through word carrying omarchy-exec-argv --
--exec is the only thing that may build a click command.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-23 21:44:06 +02:00
Ryan Hughes 21cbbf8194 Recognize --exec only after the positionals
A greedy pre-scan matched the first --exec token anywhere on the line, so an
untrusted headline or description literally equal to "--exec" could be mistaken
for the delimiter. It always failed closed (the following token became a
dash-option argv[0] that parseExecArgv rejects), but it was a latent footgun
surfaced by an adversarial review.

Detect --exec only in the trailing-option loop, after the headline/description
positionals are captured. A headline that is the string "--exec" is now kept as
text and the real trailing --exec still wins. The residual — a description
exactly equal to "--exec" losing its click action — is an inherent, harmless CLI
ambiguity for a value identical to the delimiter.
2026-08-23 14:41:56 -04:00
Ryan Hughes bf2013e6f3 Make --exec take the command as rest-of-line words
Replace --exec-arg with an ergonomic --exec that consumes the rest of the line
as the click command. The caller's shell tokenizes the words into discrete
arguments before the tool sees them, and the shell runs them as positional
parameters (never a re-parsed string), so safety is identical to the argv form
while the call sites read naturally: `--exec omarchy toggle something`.

Crucially the tool never splits a string itself — a single quoted whole-command
argument is rejected and points at the unquoted form, because whitespace-
splitting a string hands argument boundaries to whoever controls its content
(the injection we are avoiding). --exec must come last; migrate every caller.
2026-08-23 14:26:25 -04:00
Ryan Hughes eb988b42e6 Remove --exec entirely; --exec-arg is the only click-command form
A free-form shell-string --exec sitting next to the safe --exec-arg is a
standing invitation for the next caller to interpolate untrusted data and
reintroduce the RCE. Remove it: omarchy-notification-send --exec now errors and
points at --exec-arg, and the shell drops the omarchy-exec string hint and its
bash -lc execution path, leaving only the argv path.

Migrate the remaining string callers (the first-run invitation hooks, wifi and
welcome prompts) to --exec-arg, and update their notification mocks. Trim the
verbose security comments added along the way.
2026-08-23 13:35:02 -04:00
Ryan Hughes 07443f3970 Run notification click actions as argv, not shell strings
The click action of a notification was a free-form shell string run through
`bash -lc`, safe only when every sender shell-quoted every interpolated value
perfectly. One slip is RCE: a hostile yt-dlp video title forged an output
record and injected an mpv option into the click command (mehmetince.net RCE,
partially addressed by #7847).

Add a parameterized transport: omarchy-notification-send gains --exec-arg
(repeatable), encoding a JSON argv into the omarchy-exec-argv hint. The shell
runs it with Quickshell.execDetached(argv) and no shell, so data an attacker
controls is only ever one argument and can never be reparsed as a command. The
shell fails closed on a malformed argv hint.

The legacy free-form --exec string is retained but honored only from Omarchy's
own omarchy-action toasts, and deprecated. Migrate all in-repo callers
(screenshot, screen recording, taildrop receive, migrate-notify, crash-watch,
yt-dlp host) to --exec-arg. Update docs and tests.
2026-08-23 12:00:03 -04:00
5a58f79876 Keep clicking a notification working after a shell restart (#6636)
* Keep clicking a notification working after a shell restart

Notification actions lived only in the sending process: `-a` appended
`-A default=default`, so notify-send blocked on a D-Bus ActionInvoked signal and
the caller ran the command when it arrived. Nothing about that reached disk, so a
restored popup had no action to run and its sender stayed blocked forever.

Replace `-a` with `--exec <command>`, carried as an `omarchy-exec` hint into the
snapshot's `exec` role. It travels through the popup files and history, and the
shell runs it on click, so restored toasts behave exactly like live ones and the
sender exits immediately.

That drops the scaffolding whose only job was keeping a blocked sender alive: the
first-run invitations lose their `--show` re-entry and two transient units each,
omarchy-migrate-notify loses its transient service, and the screenshot,
recording, download, and taildrop toasts lose their wrapper subshells.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep a failed toast from failing the work it announces

Moving these sends out of their backgrounded subshells put a fallible command
on the foreground path, where the `&` used to swallow its exit status. A
notification outage — including the shell restart this branch targets — now
propagates:

- taildrop's receiver dies under `set -e` mid-delivery
- omarchy-capture-screenshot reports failure for a screenshot it already saved
- a completed download exits before scheduling its thumbnail cleanup, leaking
  the mktemp file

Announcing is best-effort in all three: the work is already done by the time
the toast goes out.

Also drop the first-run sleep that spaced out the welcome and Wi-Fi toasts.
It compensated for the background notify-send processes this branch removes;
each send now returns only once the server has taken the toast, so sending in
order is enough to stack them newest-on-top.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stop tying the preview cleanup to the toast's expiry

The shell loads a notification thumbnail into memory when the toast appears and
never re-reads the file, so the preview only has to outlive that load. Deriving
the cleanup delay from the expiry was false precision, and it turned -t into a
variable for no reason: -t is already the helper's expiry setting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 12:30:47 +02:00
David Heinemeier Hansson af5e918dd5 Improve actionable notification wrapper 2026-05-25 23:51:48 +02:00