Commit Graph
124 Commits
Author SHA1 Message Date
Afonso OliveiraandClaude Fable 5.1 13a4306a8e Run the refresh hook before its transaction and keep the inhibitor through user work
Three review findings on the update-hook boundary:

The pre-refresh-pacman hook had been moved after the refresh transaction
and, during a channel switch, deferred to the very end. That defeated the
hook's purpose: custom repositories and IgnorePkg entries were not in
place when the downgrade-capable -Syyuu ran. Run the hook where it used
to run, after the package config is re-synced and before the transaction,
but cold: revoke the timestamp, run it behind the no-update wrapper with
the caller's original PATH, and revoke again before continuing. Every
later privileged command authenticates with --no-update, so a detached
child left by the hook has no reusable timestamp to wait for. Channel
switching hands the caller's PATH to the refresh the same way the updater
receives it, and no longer defers or re-runs the hook.

Stay Awake was released before AUR builds, hooks and mise, so the machine
could sleep during the longest part of an update. Releasing the inhibitor
needs no privilege because the held command already dropped to the user,
so stop it after mise and before the reboot prompt, as before.

A packaged channel destination cannot be inspected before its package is
installed, and a transaction can replace the running tree with a release
that predates the command-scoped wrapper; from then on a bare sudo would
resolve to /usr/bin/sudo and publish a timestamp, and the destination's
own updater authenticates the same way. The switch used to abort only
after the packages had changed, with generic rerun advice. Now it checks
for the wrapper after each transaction before any further privileged
step, completes what it safely can, and stops cold with instructions to
run that release's update from a fresh session instead of launching it.

Boundary tests pin the hook between the config copies and the transaction
with a cold timestamp on both sides, the older-destination stop with its
guidance and no launched updater, the new inhibitor position, and the
post-update hook staying unreached on failures and signals. Docs, the
manual and the sample hook describe the restored timing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 11:11:08 +01:00
Afonso OliveiraandClaude Fable 5.1 99ddf35138 Merge the current passwordless sudo expiry head
Bring in the legacy-grant classifier fix and the reserved-prefix
quarantine from #9457 so this branch no longer carries a stale copy of
that command.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-17 10:44:30 +01:00
Afonso OliveiraandClaude Fable 5.1 ea617b9126 Quarantine unrecognized policy under the generated sudoers prefix
Matching a legacy grant by its filename and rule relationship is not a
complete fingerprint: the legacy writer took the filename from $USER but
produced the rule with echo, and under BASH_ENV with xpg_echo a name such
as ali\0143e yields an alice rule in a mismatched file. Preserving that
as administrator policy let the migration certify success with an
unrestricted grant still live until the next boot.

The prefix is reserved anyway: boot cleanup and the package hook remove
everything under it. Move any file the classifier does not recognize
into a fresh root-only directory under /var/lib/omarchy/sudoers-quarantine/
as `policy`, with the original name stored beside it, so nothing there
stays live, the administrator keeps the content, and a legacy filename
already close to NAME_MAX still fits. An untrusted quarantine directory
keeps the migration pending. Cover the mismatched and maximum-length
legacy files in the unit cleanup and through the real migration runner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 23:32:17 +01:00
Afonso OliveiraandClaude Fable 5.1 3eb3142313 Recognize legacy sudo grants for any account name
The legacy command wrote the caller's unvalidated name into both the
sudoers filename and the rule. Cleanup applied the current lower-case
account pattern to that suffix, so an exact legacy grant for an account
such as Alice was classified as administrator policy, left active, and
the machine-wide migration marker was written anyway.

Match a legacy grant by its exact filename and rule relationship instead
of the account policy, and cover it in the lifecycle suite through both
the unit cleanup and the real migration runner.

The account pattern itself stays lower-case: sudoers reads an upper-case
word such as ALICE as a User_Alias reference, and ALL as every user, so
such names must never reach the generated rule. Pin that with a test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 23:01:13 +01:00
Afonso OliveiraandClaude Fable 5.1 4bd593f4ed Merge quattro and route refreshes through omarchy-update-pacman
Upstream now runs every Omarchy-owned pacman transaction through the
hidden omarchy-update-pacman helper so a mid-transaction systemd reexec
cannot kill it. Keep the deferred pre-refresh-pacman hook and the
command-scoped sudo wrapper, and call the helper from the refresh and
channel commands; the wrapper still applies to the helper's own sudo.

The sudo boundary fixture copies the helper into its root and runs a
systemd-run stand-in that execs the wrapped pacman step in place.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:54:06 +01:00
David Heinemeier HanssonandClaude Fable 5 f5194e3ff5 Shield Omarchy pacman transactions from desktop session teardown
Upgrading systemd runs its post_upgrade scriptlet mid-transaction, which
reexecs both the system manager and every user manager. When pacman runs
inside a user-session scope (the floating update terminal), that reexec
can SIGKILL it and abandon the transaction halfway, with packages
upgraded but none of the post-transaction hooks run.

Route every Omarchy-owned system mutation through a new hidden
omarchy-update-pacman helper that registers the transaction as a PID 1
scope via systemd-run, keeping it out of the user manager's cgroups.
System scopes survive the system manager's own reexec, and as a bonus the
transaction now also survives its terminal window closing. On unbooted
systems (the installer chroot) the helper runs pacman directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-13 10:21:38 +02:00
Afonso Oliveira c46f321680 Simplify passwordless sudo grant lifecycle 2026-09-10 22:01:09 +01:00
Erik MeltonandCopilot Autofix powered by AI 5bfc8b5cc3 Update packaging companion instructions for clarity
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-08 19:47:11 +02:00
Afonso Oliveira 75e58f034f Integrate the shared sudo lifecycle foundation 2026-09-07 23:10:29 +01:00
Afonso Oliveira a6385e60b8 Bound sudo policy natively and guard expiry package transactions 2026-09-07 22:17:32 +01:00
Afonso Oliveira 4ae25cd4d2 Keep temporary sudo grants bounded through lifecycle failures 2026-09-07 21:50:47 +01:00
Afonso Oliveira c8697407cb Keep channel transitions inside command-scoped sudo 2026-09-07 17:33:33 +01:00
Afonso Oliveira f37c73fa20 Bind protected update commands to their source root 2026-09-07 17:27:04 +01:00
Afonso Oliveira aa422fcd4e Merge current Quattro while preserving command checks 2026-09-07 11:50:18 +01:00
Ryan Hughes e78d89ee2a Merge pull request #9618 from acrogenesis/security/plugin-auth-boundary
Restrict third-party plugin access to authentication services
2026-09-07 03:14:48 -04:00
Ryan Hughes 4cb9c75a96 Move Kitty defaults into the system config 2026-09-07 01:24:12 -04:00
Ryan HughesandGPT-5.6-Sol 3292c19fef Preserve built-in clone integrations
Co-Authored-By: GPT-5.6-Sol <noreply@openai.com>
2026-09-06 22:04:48 -04:00
Ryan Hughes 04b0a47c9b Configure locate through the packaged service
Reported-by: uiop / @wasdhjklxyz <uiop@wasdhjkl.xyz>
2026-09-06 21:42:31 -04:00
Afonso Oliveira a9e9954e3e Clarify unattended updates still require sudo authorization 2026-09-06 22:59:08 +01:00
Afonso Oliveira 1f8f5bacac Integrate current quattro before security review 2026-09-06 22:56:04 +01:00
Afonso Oliveira 35b318ed09 Complete command-scoped authentication across update phases 2026-09-06 22:26:06 +01:00
Afonso Oliveira 7f9a401bb1 Merge commit 'b71dcad96e9d0b2962b7d225828a5cb6000ad720' into codex/review-9469-20260906 2026-09-06 22:11:06 +01:00
acrogenesis feb0557e1d Harden ownership of installed sleep hooks
Publish privileged sleep-hook and hybrid-GPU files through root-owned replacement inodes, repair unsafe existing copies while preserving administrator customizations, and keep partial hibernation setup retryable.

Reported-by: Roger Piñol <rogerpicar@gmail.com>
2026-09-06 14:36:27 -06:00
David Heinemeier Hansson 36e56f4fb4 Drop tests for shipped one-shot migrations (#10318)
Keep the migrations themselves for late-updaters. Drop the tests that only
exercised frozen file rewrites from 4.0.0, and keep live invariants,
privileged repairs, and the migrator.
2026-09-05 20:05:04 +02:00
acrogenesis 094c913065 Tighten replacement bar plugin boundaries
Reported-by: Roger Piñol <rogerpicar@gmail.com>
2026-09-02 11:03:03 -06:00
acrogenesis 19985314c2 Merge quattro into plugin auth boundary 2026-09-02 10:12:15 -06:00
d3d23fddde Honor keepLoaded for services during plugin hot-reload (#9485)
* Honor keepLoaded for services during plugin hot-reload

Plugin reload destroyed every service, including omarchy.lock, which drops the ext-session-lock client while Hyprland still holds the lock and surfaces the crashed-lockscreen fallback.

* Prove keepLoaded service survival with a fixture service

A fresh lock service also reports an empty lastEventAt, so comparing it
across the rescan passed whether or not the instance survived. A fixture
keepLoaded service whose in-memory marker is set before the rescan and
read back after can only pass when the same instance is still mounted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop kept services whose plugin no longer declares a service

The _syncServices cleanup only asked whether the plugin was still
installed and enabled, so a kept service whose plugin dropped its
service kind or entry point kept running as a zombie until shell
restart. Apply the same eligibility checks used at creation, and hand
kept instances the refreshed manifest after a rescan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Cover omarchy.media in keepLoaded expectations; note kept services reload on restart

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 12:33:44 +02:00
acrogenesis 1702cf0bee Restrict third-party shell plugin capabilities
Reported-by: Roger Piñol <rogerpicar@gmail.com>
2026-09-01 10:55:35 -06:00
Afonso Oliveira 1136a715c0 OM-SEC-14: Run update hooks without reusable sudo authority 2026-08-31 21:32:42 +01:00
Michael Gannotti e482977f09 Add a Hermes skills migration test and list Antigravity in file-layout
The provision-user suite never ran the one-shot migration. Cover default-home
links, a pre-existing profile, idempotency, and a missing skill source.
Document ~/.gemini/config/skills and stop wrapping that bullet.
2026-08-29 15:31:29 -04:00
James (SMF Works) c64e03d9c5 Link Omarchy agent skills into Hermes skill directories
Hermes was missing from the provision-user symlink list that already
covers Claude, Codex, Pi, Antigravity, and ~/.agents. Add ~/.hermes/skills
plus existing ~/.hermes/profiles/*/skills. Migration for current installs.
2026-08-29 15:26:33 -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
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 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 d2fd2e11c6 Run argv click actions through a login shell as positional params
Quickshell.execDetached(argv) ran the click target with only the shell
process's stripped environment, so GUI actions like the screenshot editor
(tensaku-edit) — resolved on the login-shell PATH the old `bash -lc` string
exec provided — stopped launching on click.

Run the argv through `bash -lc 'exec "$@"'` instead: the script text is a
constant and the arguments are passed as positional parameters, which bash
expands without re-tokenizing or re-evaluating, so injection safety is intact
while PATH and session env match the old behavior exactly.
2026-08-23 12:25:48 -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
OmarchybotandCodex XHigh ef6d9e6605 Stop an installed theme from running code (#7884)
* Stop an installed theme from shipping code

`omarchy theme install <url>` clones a stranger's git repository into ~/.config/omarchy/themes, and omarchy-theme-set then copied that whole directory into the staged theme. Most of the files in a staged theme are code rather than colour: Hyprland requires hyprland.lua and gum_env.lua from it at login, Neovim loads neovim.lua at startup, and alacritty.toml, kitty.conf, foot.ini and ghostty.conf each name the program the terminal launches. Installing a theme was the same act as running its author's code, and nothing on disk distinguishes an installed theme from one the user wrote.

Stage only what a theme needs in order to be a theme: colors.toml, light.mode, the preview and unlock images, and image files under backgrounds/. Everything else is ignored, named on stderr, and generated from default/themed/*.tpl instead. Symlinks are never followed, because in an untrusted theme they point wherever the author chose. A theme older than colors.toml keeps its palette: its alacritty.toml is read for colours in a scratch directory and only the resulting colors.toml is staged, so the terminal config never lands.

The filter belongs in omarchy-theme-set rather than in omarchy-theme-install because staging is the choke point. It also covers themes installed before this change, themes copied in by hand, and files a theme gains later through `omarchy theme update`.

First-party themes under $OMARCHY_PATH/themes are unaffected. Per-theme overrides of a generated file are no longer available to user themes; the template at ~/.config/omarchy/themed/<file>.tpl replaces that, and icons.theme is the one setting with no replacement.

🤖 Generated by Opus 5 in Claude Code.

* Stop a theme URL or name being read as an option or a path

Three paths in the theme commands took an attacker-shaped string straight into git, into basename, or into rm.

`git clone "$REPO_URL"` passes the URL as the first positional argument, so a URL beginning with a dash is parsed as an option instead and the destination path becomes what git tries to clone. Pass `--` before the URL so a URL is always a URL. git also treats `<helper>::<address>` as a remote helper to run; git's own protocol.allow default already refuses `ext::`, so rejecting that shape here is a second line rather than the fix, and it keeps holding if that default ever moves. The helper name is a bare word at the very start of the URL, which is what the guard matches: an scp-style IPv6 host such as git@[2001:db8::1]:org/repo.git carries `::` of its own and still clones.

`basename "$REPO_PATH" .git` has the same problem one step later, after the scp-style prefix has been stripped: `host:-s/foo.git` leaves basename reading `-s` as an option and returning `.git` as the theme name. Take the name with `--`.

That name is then joined into a path that is about to be `rm -rf`'d, so a repo whose basename came out as `..` would take ~/.config/omarchy with it. omarchy-theme-remove had the same shape from its own argument, and omarchy-theme-set's sed/tr normalization does not stop a name containing a slash. Reject empty, anything starting with a dot, and anything containing `/` in all three, before the name reaches a path.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Codex XHigh <codex@openai.com>

* Re-stage the current theme for installs that already applied one

Dropping a theme's code at staging time only takes effect the next time a theme is staged. An install that already applied an extra theme keeps that theme's hyprland.lua, gum_env.lua, neovim.lua and terminal configs in ~/.local/state/omarchy/current/theme, which Hyprland requires at login and the terminals include at launch, and nothing forces a theme change — so for those installs the fix would arrive whenever the user next happened to switch themes, which may be never.

Re-stage once through omarchy-theme-refresh. First-party themes stage identically, so the cost for everyone else is a single retint during an update they are already running.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Codex XHigh <codex@openai.com>

* Stop a theme's unlock image republishing a file it points at

omarchy-plymouth-set-by-theme reads unlock.png straight out of ~/.config/omarchy/themes, which is an installed theme's own directory and outside the staging filter, and hands the path to omarchy-plymouth-set. That path was copied twice into world-readable /usr/share — once by the user into the Plymouth theme, and once by `sudo cp` into the SDDM theme. A symlink there was followed both times, so a theme could name a file it cannot read and have root publish it.

Refuse a symlinked logo, and copy the staged logo to SDDM instead of rereading the caller's path as root. The staged copy is made by the user, so nothing privileged opens a path the caller chose.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Codex XHigh <codex@openai.com>

* Limit only what an installed theme could run

Two corrections to the rule this branch introduced, both narrowing it to what it was actually for.

It applied to every theme under ~/.config/omarchy/themes, which swept up themes the user wrote themselves. Their machine, their file: a theme they wrote is theirs to fill however they like, and Omarchy's own themes were never in scope. Only a theme that came from someone else needs limiting, and the repo already knows which those are — omarchy-theme-extras calls a theme with a `.git` directory an extra and a symlink someone's working copy, because that is what `omarchy theme install` leaves behind when it clones. Use the same test.

It was also an allowlist, which dropped files that carry nothing but colour and left theme authors worse off for no gain. Drop only what can run: any `*.lua`, since Hyprland requires a theme's hyprland.lua and gum_env.lua at login and Neovim loads neovim.lua at startup; the four terminal configs, since each names the program the terminal launches; and vscode.json, whose extension field reaches `code --install-extension` and a VS Code extension is arbitrary JavaScript. Everything else an installed theme ships is kept, so btop.theme, chromium.theme, helix.toml, icons.theme, keyboard.rgb and shell.toml go back to being the theme's to set.

Symlinks are still dropped, now at any depth rather than only where an allowlist happened to look.

A denylist is wrong the moment someone adds a template and does not think about it, so the decision is forced rather than remembered: the test fails on any default/themed/*.tpl whose output is recorded as neither code nor colour, and a new terminal or a new Lua-loading editor cannot be added without classifying it.

What this does not cover, and is written down in docs/theming.md rather than implied: a theme shipped as an archive and unpacked by hand looks exactly like one the user wrote. `omarchy theme install` only takes git URLs, so the supported path is always filtered, but this marks where a theme came from and is not a sandbox.

🤖 Generated by Opus 5 in Claude Code.

* Fix what the review found

Four things, all confirmed against the source before changing anything.

The migration failed permanently when the active theme had been removed. `omarchy theme remove` deletes the directory without repointing theme.name, so the name survives, the staged copy survives, and omarchy-theme-refresh exits 1 because neither source directory exists — leaving the migration pending forever and the stale staged Lua exactly where it was, which is the one thing it existed to remove. Seed the default theme in that case: there is nothing to re-stage from, and the removal should have left a working theme behind anyway.

The staging test skipped the strict-mode header that docs/testing.md makes the contract for every shell test. Adding it means the patterns that fail on purpose have to stop being bare `cmd && fail` compounds, which errexit reads as the script itself failing; the mutations were re-run afterwards to confirm the assertions still fire rather than the run dying early and looking like something else.

The guards in omarchy-theme-install and omarchy-theme-remove had no coverage — they were checked by hand and left that way. theme-install-guards-test.sh stubs git and the themes directory and proves an option-shaped URL, a transport helper, and a name that would climb out all stop before git or rm runs, that a dash inside the path no longer becomes a basename option, and that an ordinary URL still clones and applies.

The new docs/theming.md prose was hard-wrapped, which AGENTS.md forbids for docs/. Unwrapped. The rest of that file is wrapped from before and is left alone rather than churned through this change.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh and Copilot.

---------

Co-authored-by: Codex XHigh <codex@openai.com>
2026-08-23 16:43:31 +02:00
David Heinemeier HanssonandClaude Fable 5 f4189398bc Bring docs/ up to date and cover the undocumented subsystems
Every reference doc was audited claim-by-claim against the code.
file-layout and omarchy-shell were the most decayed (renamed commands,
the etc/ overrides source split, dead IPC entry points and example keys);
update-process lagged the recent pipeline changes and gains a channels
section; theming and audio-tuning were accurate but thin around their
lifecycles.

New reference docs for the subsystems that had none: the menu system,
the CLI router, the notification daemon, and the non-acceptance test
architecture. AGENTS.md links the two of those agents will need most.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 16:21:25 +02:00
David Heinemeier HanssonandClaude Fable 5 16c8888f96 Sort documentation by genre and document the layout
Task procedure lives in agents/skills/ (migrations.md moves there),
system-shape reference in docs/ (AUDIO-TUNING.md renamed to match),
end-user documentation in manual/. AGENTS.md now states the split.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 16:21:25 +02:00
Ryan Hughes 05bb82b34e Add checkout bin to sudoers path 2026-08-11 11:04:50 -04:00
David Heinemeier HanssonandClaude Opus 5 1c9dfc55f4 Greet the first login with a keybindings toast again
Opening the cheatsheet outright put a menu in front of someone who had not
asked for one, and it blocked first run until they dismissed it. Go back to a
toast that opens the same menu when clicked.

The body carries real newlines now. It was written with a literal \n, which the
card renders as the two characters rather than a line break.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 05:32:47 -07:00
David Heinemeier HanssonandClaude Opus 5 f97ba7375e Wait for a connection before prompting to update
A ping at hyprland.start answers for a machine that has not finished coming up.
Ethernet is still negotiating DHCP, so a working desktop was told to set up
Wi-Fi and offered an update it could already have run. Ask NetworkManager
instead: -s returns once it has tried every connection it could auto-activate,
which is the first moment the answer means anything, and -x then takes that
answer as it stands rather than waiting out a timeout that a laptop with
nothing to connect to would spend in silence.

The update prompt now waits for a connection rather than being phrased around
not having one. There is nothing to update against until a link lands, and one
usually does land later on the machines that started without it, so the prompt
follows the connection whenever it arrives.

That wait runs detached. It outlasts first run by design, and the keybindings
menu is on screen behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:56:30 +02:00
David Heinemeier HanssonandClaude Opus 5 23d85a992b Open the keybindings menu on first login
The welcome toast spent three lines telling you about a cheatsheet that takes
one keystroke to read, and the only way to act on it was to click the toast,
which opened that cheatsheet. Open it directly instead.

Dismissing the menu exits non-zero, since no selection was made, so the step
tolerates that rather than failing first run and retrying the whole sequence
next login.

It also goes last now. The menu blocks until it is answered, and the Wi-Fi and
update toasts are the only other things left to show, so sending those first
leaves them waiting underneath rather than behind an open menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:56:03 +02:00
David Heinemeier HanssonandClaude Opus 5 536fcd5c6c Move install-time plumbing out of the setup namespace
setup is where a user goes to configure something: direct boot, security
keys, hibernation. These three are not that. omarchy-apply-system is the
ISO's entry point in the target chroot, omarchy-apply-hardware is what it
calls for device quirks, and omarchy-apply-lock is called by
install/config/lockscreen-pam.sh.

apply is the verb they already used to describe themselves, and it carries
the contract: declared state under install/ converged onto the machine,
idempotent, safe to repeat.

The group gets no GROUP_DESCRIPTIONS entry on purpose. That table drives the
top-level group list on its own, so an entry would put apply back in front of
users even with every command in it hidden, the way provision already stays
out. A test covers it.

The ISO installs the runtime from the mirror it ships with, so it moves to
the new names in lockstep and no compatibility route is needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 14:14:57 -07:00
David Heinemeier HanssonandClaude 6fa4f78ee1 Add deferred first-boot provisioning and factory reset (#6621)
* Add OEM first-boot setup and factory reset

An OEM-mode ISO install (or omarchy-reset-computer) leaves the machine in OEM
state: fully installed, no user, /var/lib/omarchy/oem/pending armed. On the
next boot omarchy-oem-setup.service runs the configurator's user form on tty1,
creates the user with the groups system setup recorded, finalizes it offline
from the stashed Node tarball, re-keys LUKS from the throwaway install
passphrase to the user's password, and hands off to SDDM.

omarchy-reset-computer returns a machine to that state: it swaps the running
root for a fresh clone of the @factory snapshot the ISO takes at install time,
scrubs machine identity and prior users, and stages omarchy-factory-wipe to
drop the old root and recreate @home/@log on the next boot. Machines installed
before @factory existed get a degraded reset (current system kept, users and
state wiped) with that caveat surfaced in the confirmation.

omarchy-setup-system/-hardware gain --oem to run without an install user; the
group-granting install scripts now record their groups in
/var/lib/omarchy/oem/groups and only call usermod when the user exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Harden OEM setup: correct cryptsetup key-file usage, retry on failure

cryptsetup reads --test-passphrase/--key-file inputs byte-for-byte, so feed
passphrases through process substitution consistently instead of positional
args or stdin (which has different newline semantics). Run each first-boot
setup attempt as its own process so a failure offers a retry instead of
stranding the machine at a user-less login screen — bash ignores errexit
inside `while !` conditions, a child process does not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Always grant wheel sudo in OEM first-boot setup

Detecting an existing %wheel grant by grepping sudoers is error-prone:
omarchy ships narrow '%wheel ALL=(ALL) NOPASSWD: <command>' rules (e.g.
asdcontrol) that match the naive pattern, which left the OEM-created user
matching sudoers entries but unable to run anything. Write the drop-in
unconditionally — a duplicate of an existing full grant is harmless.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix LUKS re-key device resolution and OEM state readability

archinstall's encrypted installs put cryptdevice=PARTUUID=... on the kernel
cmdline, not UUID=, so the first-boot re-key never found its device and
silently skipped — leaving the throwaway auto-unlock keyfile in place, i.e.
the disk effectively unencrypted. Parse every cryptdevice= source spec form
and make any re-key failure abort the attempt loudly: a retry prompt beats a
machine that quietly boots without a passphrase forever.

The OEM state directory also has to be world-readable (its one secret,
luks-key, stays 0600): user finalization reads the stashed Node tarball as
the new user, and the 0700 directory forced it onto the network fallback.

Step markers now land in /var/log/omarchy-oem-setup.log for debuggability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Purge stale machine-id boot entries when resetting or re-keying

limine-entry-tool keys its limine.conf OS entries by machine-id. A factory
reset gives the machine a fresh identity, so the previous system's entry
survived every rebuild, sorted first, and made Limine stop at a Blake2b
hash-mismatch warning once the UKI was rebuilt. Start limine.conf over from
the shipped template (and drop foreign machine-id history directories on the
ESP) before any post-reset rebuild: in the staged chroot rebuild, in the
first-boot LUKS re-key, and — for unencrypted resets, where nothing else
rebuilds — in a dedicated first-boot refresh when foreign entries are found.

The staged rebuild also verifies every UKI hash referenced by limine.conf
against the file on the ESP before the subvolume swap, and the running
system's limine-snapper-sync is runtime-masked during staging so it cannot
rewrite the config behind the rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Harden reset and first-boot setup failure paths

Review findings from codex and Copilot:

- Generate throwaway passphrases without a trailing head stage: under
  pipefail, SIGPIPE from the infinite tr failed the substitution and errexit
  aborted every encrypted reset before it could stage anything.
- Stage the fallible parts of a degraded reset (LUKS re-key, boot rebuild)
  before arming the wipe, so a staging failure leaves the machine untouched
  instead of scheduling a wipe for a reset that never finished.
- Gate first-boot setup on the factory wipe having succeeded
  (ConditionPathExists=!wipe-pending plus an in-script guard): creating the
  new user on a half-wiped system would hand their data to the wipe retry.
- Abort the wipe (keeping its retry marker) when deleting the old root or
  recreating @home/@log fails, and abort resets that cannot remove a prior
  account — a surviving account keeps its password and wheel membership.
- Resume a partially-created account on setup retry instead of rejecting the
  username the failed attempt just created.
- Only purge machine-id directories the old limine.conf actually referenced;
  a shared ESP may hold other installations' boot artifacts.
- Recreate the hibernation swapfile (nested subvolume, so never captured by
  the factory snapshot) inside the factory root before its UKI rebuild, so a
  reset machine keeps disk-backed swap and a valid resume offset.
- Source base-test.sh in the OEM groups test per test conventions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Recreate the hibernation swapfile even when resume drop-ins survive

omarchy-hibernation-setup short-circuits as 'already set up' when the resume
mkinitcpio drop-in exists — which it always does in a factory root, while the
swapfile itself never survives the snapshot (nested subvolume). Drop the
marker when the swapfile is gone so setup reconfigures from scratch, and
verify the swapfile actually exists before proceeding with the reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Second review pass: encrypted-config coverage, factory-baseline sanitization, recoverable rekey

Codex xhigh round 2:

- Detect the LUKS backing device by walking the root's device tree, not only
  the cmdline cryptdevice=; reset/first-boot now re-key roots reached via
  rd.luks/crypttab too, instead of silently leaving the seller's slots valid.
- Sanitize the retained @factory baseline (accounts, /etc/shadow, machine
  identity) during a full reset: the new wheel user could otherwise mount it
  to recover the seller's data, and a second reset would restore the account.
- Re-key the disk recoverably: rebuild the no-auto-unlock UKI before killing
  the throwaway slot or destroying the staged key, and restore the keyfile if
  that rebuild fails, so a retry with a different password can never leave the
  disk locked to the first attempt's password.
- Roll back a degraded reset's live-root auto-unlock material if its boot
  rebuild fails, instead of leaving it for a later rebuild to embed.
- Treat a missing current-machine limine entry as stale so a retry after a
  failed rebuild repairs the config instead of clearing OEM state over it.
- Erase fingerprint enrollments (/var/lib/fprint) in degraded wipes.
- Remove the resume-offset drop-in too when recreating the factory swapfile,
  so the rebuilt UKI gets a correct offset.
- Pin first-boot retries to the account the first attempt created.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Expose factory reset in the Setup menu

Add a 'Reset Computer' entry under Setup (Omarchy's Settings menu, where OS
factory resets conventionally live), guarded to btrfs roots and launched in a
floating terminal. omarchy-reset-computer now self-elevates via sudo so the
menu entry needs no sudo prefix, forwarding the caller's gum theme env as
env arguments so styling survives an env_reset sudoers. The typed 'reset'
confirmation and the sudo password prompt remain as the guards against
accidental triggering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Defer keyboard selection to first boot for OEM installs

The OEM first-boot setup now runs a keyboard step before the user form,
mirroring the ISO configurator: it loads the chosen layout on the live VT so
the password (and the LUKS re-key that follows) are typed under it, and
persists it with systemd-firstboot so the installed system gets both the
console KEYMAP and the XKB layout Hyprland reads — exactly what a normal
install writes. Layouts localectl doesn't know keep the default, same as the
installer.

This lets the OEM operator set nothing user-specific: the machine's owner
picks their keyboard alongside their account at first boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Rename factory-reset commands to omarchy-system-factory-reset[-finish]

omarchy-reset-computer      -> omarchy-system-factory-reset
omarchy-factory-wipe        -> omarchy-system-factory-reset-finish
(and its systemd unit, log path, and temp mount to match)

Pure rename: every reference — the Setup menu action, the first-boot finish
service the reset stages and enables, the oem-setup ordering/gating, comments,
and the menu test — moves together, with no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Rename OEM vocabulary to provisioning (runtime)

Commands unify under the provisioning family:
  omarchy-oem-setup      → omarchy-provision-owner
  omarchy-finalize-user  → omarchy-provision-user
  omarchy-first-run      → omarchy-provision-first-run

And the deferred-provisioning state/vocabulary replaces 'OEM':
  /var/lib/omarchy/oem/          → /var/lib/omarchy/provisioning/
  /etc/omarchy/oem.key           → /etc/omarchy/provisioning.key
  install/oem/                   → install/provisioning/
  OMARCHY_SETUP_CONTEXT=oem-firstboot → provision-owner
  omarchy-setup-system/-hardware --oem → --defer-provisioning

All callers (provision-first-run→provision-user, autostart, factory-reset
staging the provisioning units, the group-recording scripts) and comments
move together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop remaining OEM mentions from the provisioning groups test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Finish the omarchy-first-run rename in the docs

Two doc references to omarchy-first-run were missed when the script was renamed
to omarchy-provision-first-run; update them to match.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 21:54:21 +02:00
David Heinemeier HanssonandClaude Opus 5 0f1e0ced36 Remove the omarchy-update-perform compatibility wrapper
Nothing calls it anymore; new code calls omarchy-update directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 02:30:48 -07:00
David Heinemeier HanssonandClaude Fable 5 6ee243cc37 Split the end-user omarchy skill into topic guides (#6602)
* Split the end-user omarchy skill into topic guides

Move default/omarchy-skill to default/agents/skills/omarchy and break the
monolithic SKILL.md into on-demand topic files for Hyprland config, shell
plugins, theming, and hooks. Update the skill symlink wiring, relink
existing installs through a migration, and correct claims that had drifted
from the implementation: plugin hot-reload, terminal reload, menu
customization, refresh scopes, theme overlays, background locations, hook
timing, and the packaged (not git-managed) system directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Add capture and contributing guides to the omarchy skill

Cover screenshots, screen recording, OCR text capture, and LocalSend or
Taildrop sharing, plus how to route bug reports, suggestions, and support
questions upstream with diagnostics and captures of the problem attached.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Scope Hyprland reload guidance to the Lua config files

hyprsunset.conf and xdph.conf are read by separate processes, so hyprctl
neither applies nor validates them. Document restarting hyprsunset after
editing its config, including in the night light example.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 14:36:42 +02:00
David Heinemeier HanssonandClaude Fable 5 c2d81220d9 Split task-specific agent guidance into agents/ guides
Keep only universal rules in AGENTS.md and point to per-task guides
for shell development, acceptance tests, visual verification, command
metadata, and install scripts. Fold the migration notes into
docs/migrations.md and replace .claude/CLAUDE.md with a root CLAUDE.md
importing AGENTS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:33:27 +02:00
Luke WalkerandDavid Heinemeier Hansson caeffdc27b fix: add bg/fg aliases for theme color resolution (#6546)
* fix: add bg/fg aliases for theme color resolution

Themes define bg/fg but the template system expects
background/foreground. The fallback chain only checked color0/color7,
leaving background/foreground empty for themes using bg/fg naming.

* Complete legacy theme palette compatibility

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-04 13:26:26 -05:00