No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
sam f52708255c Refuse --list from root as well
The markers it counts are root's, and root's migrations are ones that
must never run: it answered '86 pending' where the user had none left --
a number that looks like work to do and is not. The message now gives
the command that reads the right ones.
2026-09-01 07:16:13 +02:00
bin Add omarchy-netsetup for a static address or a bridge 2026-09-01 05:41:35 +02:00
conf Take out limine's mkinitcpio wrapper, which is not a hook 2026-08-29 23:17:44 +02:00
hooks Refuse --list from root as well 2026-09-01 07:16:13 +02:00
lib Let bootstrap install from the real repos, without build-repo 2026-08-30 23:28:29 +02:00
patches Drop the Development menu by key, not by patch 2026-08-27 09:44:20 +02:00
vendor Shim vercmp as well 2026-08-27 10:52:36 +02:00
.gitignore Ship pacman-static with the repo 2026-08-27 01:44:46 +02:00
README.md Refuse --list from root as well 2026-09-01 07:16:13 +02:00

Omarchy 4.0.0 on a CachyOS base

Installing Omarchy 4.0.0 on a CachyOS system, without using the ISO installer.

Why adding the CachyOS repos to Omarchy doesn't work

Omarchy doesn't use the Arch mirrors. The pacman.conf its installer writes points core, extra and multilib at its own versioned mirrors:

[core]     → https://stable-mirror.omarchy.org/$repo/os/$arch
[extra]    → https://stable-mirror.omarchy.org/$repo/os/$arch
[omarchy]  → https://pkgs.omarchy.org/stable/$arch

Those are snapshots promoted upstream, while CachyOS rolls with current Arch: two timelines of the same repos. Putting them together, with CachyOS on top, pulls rolling glibc and gcc underneath a userspace built against the snapshot — the partial upgrade that breaks Arch.

This project inverts the hierarchy: CachyOS is the base, and only the delta comes from Omarchy — the 19 packages CachyOS doesn't carry, installed from files with pacman -U.

The install turned out to be practical because the Omarchy 4.0.0 snapshot and the CachyOS repos share the same glibc (2.44+r24+g16be1518495f-1, same commit), and the highest GLIBC_ symbol any Omarchy binary needs is 2.39.

How the official ISO installs (and what is reproduced here)

1. archinstall partitions
2. pacstrap from the [offline] repo — file:///var/cache/omarchy/mirror/offline
   (1247 packages inside the ISO, no network)
3. copies the live /etc/pacman.conf into the target
4. arch-chroot → omarchy-apply-system  (install/config, hardware, login, post-install)
5. arch-chroot → omarchy-provision-user (install/user)
6. post-install/pacman.sh rewrites /etc/pacman.conf with the Omarchy mirrors
7. Limine as the bootloader

Steps 1, 2 (partitioning) and 7 (bootloader) are left to whoever installs. The offline repo is rebuilt from the CachyOS repos instead of the ISO, and step 6 is neutralised.

Starting on another machine

git clone https://git.snix.me/omarchy/cachyos.git
cd cachyos

Then work through the procedure below. Nothing is tied to a particular machine: the package list comes from an Omarchy release, and the drivers a machine needs are installed by Omarchy's own hardware scripts, from inside the target, in step 4f.

Four commands, in order:

./bin/list-packages -w                       # write packages.txt
sudo ./bin/build-repo                        # download it into /repo
sudo ./bin/bootstrap                         # pacstrap the target
sudo ./bin/install-omarchy-cachyos -u <name> # configure it

Each leaves a file the next one reads, and both are yours to edit in between: packages.txt says what to download, bootstrap-packages.txt what to install. Commenting a line out of the second keeps the package in the repo, one pacman -S away, without putting it on the target.

The kernel is a choice, not something to detect, so it is a variable:

sudo ./bin/build-repo                        # linux-cachyos, ZFS root
sudo ./bin/build-repo -k                     # which kernels there are
sudo ./bin/build-repo -k linux-cachyos-lts   # a different one
sudo ./bin/build-repo --zfs                  # ZFS root
sudo ./bin/build-repo --no-zfs               # not a ZFS root

Tag and channel are two different things, easily confused. --tag is an Omarchy release on GitHub, and it belongs to list-packages: it decides which Omarchy packages get listed. --channel is the pacman repository at pkgs.omarchy.org — stable, rc or edge — and it decides where they are downloaded from. build-repo takes both, though its --tag only spells out the list-packages command to run when packages.txt is missing. bootstrap --net takes --channel, since it downloads too.

A channel name is checked when the flag is read, not left to fail at the sync — where pacman fetches every database before reporting a 404 on a URL nobody looks at closely. stable, rc and edge are taken on trust; anything else gets a HEAD on its repo database before being refused, so a channel Omarchy adds tomorrow works without this repo being edited first. With no network to ask, the name is taken as given and a note says so: refusing there would fail an offline --dry-run over a channel that is very likely fine.

Whether the kernel's -zfs module, zfs-utils and cachyos-zfs come along is asked rather than detected: the target disk is usually not formatted yet when the repo is built, and this machine's filesystem says nothing about it. --zfs and --no-zfs answer in advance; with neither, and no terminal to ask on, they are included -- ~350 MB wasted if unneeded, against a target that will not boot if they are missing.

--repo <dir> builds somewhere other than /repo; bootstrap --repo and install-omarchy-cachyos --repo read it back. bootstrap works out which kernel to install by looking at the repo -- a kernel is the package with a -headers of its own -- so the choice made here does not have to be repeated.

Procedure

1. Prerequisites

  • A working CachyOS system (it supplies the repos and mirrors)
  • The Omarchy 4.0.0 ISO mounted, to extract the packages only Omarchy has
  • A partition or dataset for the new system

Running from something other than Arch

The scripts need pacman, repo-add, pacstrap and arch-chroot. The last three are bash and are checked in under vendor/; for pacman, build the pacman-static PKGBUILD and drop the binary in vendor/bin/:

cp pacman-static-*/usr/bin/pacman-static vendor/bin/

build-repo and install-omarchy-cachyos pick it up on their own when the host has no pacman of its own -- PACMAN_STATIC=/path/to/binary overrides the search. On an Arch host none of this applies.

Still needed from the host: bsdtar (libarchive) and chroot, mount, unshare (util-linux). On Alpine: apk add libarchive-tools util-linux.

Compile it for a CPU that will actually run it -- a -march with AVX-512 dies with SIGILL on anything that lacks it, which includes every Zen 2/3 and Intel's hybrid-core parts.

2. The Omarchy-only packages

The 19 packages only [omarchy] carries are declared in the generated config like every other repo, with SigLevel = Optional TrustAll, so pacman -Sw fetches them in the same transaction as the rest. No ISO, and no keyring for the isolated dbpath to be missing.

3. Refreshing the list for a newer Omarchy

list-packages -w rewrites packages.txt from a release, reading the repo databases over HTTP to group each name under the repo that serves it:

./bin/list-packages -w            # the latest stable release
./bin/list-packages -w -t v4.0.1  # a particular one

The two things you decided are written onto the lines that carry them, so regenerating keeps them: a package commented out by hand stays commented, and a -git/-bin package answered for once is not asked about again. An existing packages.txt is never rewritten without asking first; --regenerate starts from scratch instead.

4. Build the local repo

sudo ./bin/build-repo

Downloads everything left active in packages.txt with every dependency -- around 850 files, ~2.3 GB -- then splits them into one local repo per source repo, keeping the original names:

/repo/
  cachyos-v3/   cachyos-core-v3/   cachyos-extra-v3/   cachyos/
  core/         extra/             multilib/           omarchy/

The split is what makes package origins survive the install. With a single merged repo every package would be recorded as coming from that one name; here the target's database says cachyos-extra-v3 or omarchy, and that stays accurate once the real repos are declared. The 19 packages only [omarchy] has go into /repo/omarchy, named to match the upstream repo added later.

It uses an isolated --dbpath rather than a plain pacman -Sw, because -Sw resolves the transaction against the installed system: an existing linux-cachyos-zfs blocks downloading a newer kernel, and poppler can resolve to a different repo than poppler-glib, breaking their exact dependency.

On the way out it copies packages.txt to bootstrap-packages.txt, asking first if one is already there. That copy is what the pacstrap installs, so the repo can hold more than the target takes.

5. Partition, mount, pacstrap

# mount the destination on /mnt (your choice: btrfs, ext4, ZFS...)

sudo ./bin/bootstrap

bootstrap reads the kernel out of the repo, adds the ZFS module if build-repo was asked for one, prints every package it is about to install grouped by repo, and waits for a yes. --dry-run prints the pacstrap command instead of running it.

conf/pacman-install.conf declares all eight local repos with SigLevel = Never, in the same order as conf/pacman-target.conf. pacstrap resolves across every one of them in a single transaction, so the Omarchy packages install alongside the rest instead of needing a separate pacman -U pass.

The config it actually uses is generated, with HookDir pointed at conf/hooks-off/: the kernel, linux-firmware and every dkms package trigger 90-mkinitcpio-install, and a pacstrap would otherwise rebuild the initramfs and the UKI a dozen times, each one thrown away by the next. Nothing builds one at any point -- see step 7.

Without the local repos

--net installs the same packages from the real repositories, skipping build-repo entirely:

sudo ./bin/bootstrap --net          # the same bootstrap-packages.txt, over the network
sudo ./bin/bootstrap --net --zfs    # with the ZFS module

The package list is unchanged — bootstrap-packages.txt either way. What changes is where they come from, and what that costs. The local repos buy two things: a second identical install without downloading again, and a record of which repo each package came from that survives the pacstrap (see package origins). --net trades both for not having to build them.

Two things the repo answered on its own have to be said instead. The kernel: locally it is the package with a -headers of its own, which identifies exactly one; the real repositories carry a dozen that fit that test, so --net uses linux-cachyos unless --kernel names another. And the ZFS module, which locally rides along when build-repo was asked for it — --zfs says so here.

The repository list comes from lib/repo-conf, shared with build-repo so the two cannot drift: the host's mirrorlists when it has them, otherwise fallback servers reachable from anywhere. Signatures are verified when the host has a keyring, and SigLevel = Never with a note when it does not — which changes less than it sounds, since the local repos carry Never of their own and a pacstrap has never verified one either way.

install-omarchy-cachyos needs no flag to match: it looks for the repo databases and adjusts the two steps that read them.

6. Run the setup

sudo ./bin/install-omarchy-cachyos -u <name>

pacman.conf into the target, a check that the Omarchy packages arrived, patching the scripts the omarchy package installed, user creation, then omarchy-apply-system and omarchy-provision-user.

Step 4d creates the user, asks for their password, and then offers one for root. root arrives locked the way pacstrap leaves it, and sudo through the wheel group is the way in — so the offer defaults to no, and exists for the day sudo or the user's own login is what broke. If root somehow has an empty password rather than a locked one, the question is put the other way round and defaults to yes.

Step 4g is what archinstall would have asked: it writes fstab with genfstab -U, then asks for the hostname, timezone, locale and console keymap, one question each with a default to accept by pressing enter. The defaults come from the target when it already has them and from the host otherwise, so re-running the script and holding enter changes nothing. Each answer is checked before anything is written — a timezone that exists in the target, a locale listed in its locale.gen, a keymap under kbd/keymaps — and en_US.UTF-8 is generated alongside the chosen locale, since that is what error messages and man pages are written in.

Step 4g also writes the mkinitcpio presets. No package ships them: /usr/share/libalpm/scripts/mkinitcpio generates one per kernel, and only when the mkinitcpio package itself is being installed — which during the pacstrap happened with that hook masked. The target then has a kernel, its modules, and an empty /etc/mkinitcpio.d, so the mkinitcpio -P this script closes by asking for answers No presets found. The installer does the same two steps by hand — read each /usr/lib/modules/*/pkgbase, substitute %PKGBASE% into /usr/share/mkinitcpio/hook.preset — and writes a byte-identical file, without the image build that script would go on to do: the initramfs is yours to make once the bootloader is settled.

On a root that is not btrfs, step 4g also writes an empty /etc/snapper/configs/root. Snapper only does btrfs, and install/config/snapper.sh runs snapper -c root create-config / with no check on the filesystem — so on ZFS or ext4 it fails with invalid filesystem type. Step 4c patches that script out of install/config/all.sh, but the migrations call it directly, and one of them then takes the whole of omarchy update down with it, after the packages have already upgraded. The script skips create-config when the config file exists, which is what the stub is for; the template it installs over it on the next line makes the file real.

/etc/locale.conf gets LANG and the nine LC_* categories, all set to the chosen locale. LANG alone would behave identically — glibc takes every unset category from it — but archinstall writes them out, and a file that looks like the one on an ordinary Arch install is easier to read and to edit later.

The keymap is asked apart from the locale on purpose: it is the physical keyboard, and it_IT.UTF-8 with a us keymap is an ordinary pairing. -y/--noconfirm takes every default without asking, and is assumed when there is no terminal to ask at.

Omarchy sets none of this itself, and step 5 runs in a chroot that inherits the host's locale — a target left unconfigured boots into C.UTF-8 with no keymap.

Before omarchy-apply-system and omarchy-provision-user it ranks the mirrors, syncs the target's databases and puts a wrapper in front of omarchy-pkg-add (step 4f). The ranking runs cachyos-rate-mirrors, which rewrites /etc/pacman.d/mirrorlist along with the three CachyOS lists — so both sets are ordered before step 5 pulls the graphics drivers, which on an NVIDIA machine is 300 MB from whichever mirror happened to be first. Omarchy's install/hardware/*.sh run inside the target, detect the machine and ask for the drivers it needs; without synced databases every one of those calls dies with target not found. The repos are already in the right order by then, CachyOS ahead of Arch, so a driver resolves to the CachyOS build on its own. The wrapper names the repo each package comes from, logs them to /var/log/omarchy-hardware-packages, honours a skip list at /usr/local/share/omarchy-pkg-skip, and drops to a shell in the target when pacman fails -- the install carries on when that shell exits.

The patches apply straight to the target, so no Omarchy checkout is needed -- this repo is enough.

7. Bootloader and initramfs

The two that depend on a partition layout this repo knows nothing about. No step builds an initramfs before here, since every build before the bootloader exists would be thrown away by the next.

sudo arch-chroot /mnt mkinitcpio -P

limine ends up installed (it's a hard dependency of the omarchy package) but is not configured, and is kept out of the way by limine-guard — see limine below. To use it as the bootloader instead: sudo limine-guard --enable.

Updates

omarchy update asks Ready to update? before it knows itself what it will do: the packages appear only once you have said yes, and the migrations are shell scripts that install whatever they install. Most of that is ordinary — 67 of Omarchy's 88 migrations touch no package, and the ones that do mostly name packages this system already has — but a few reach for things that do not belong here, and by the time the name appears pacman is already resolving it.

hooks/omarchy-update-preview goes first and shows the work: the pending upgrades grouped under the repo each resolves to, the pending migrations with their descriptions, and in yellow the ones that would install something not yet present. On those it asks. Grouping by repo is what makes the handful from [omarchy] stand together rather than scattered down a list forty long — an Omarchy build standing in for the CachyOS one is the thing worth noticing here.

omarchy-update-preview              # show, ask, then offer to run the update
omarchy-update-preview --list       # show and exit
omarchy-update-preview --list --all # every pending migration, not just those that install
omarchy-update-preview --reset 1784672586    # put a retired migration back

Only the migrations that would install something are listed in full; the rest — usually most of them, since 67 of the 88 touch no package — are summarised in one line, so the questions are not eighty rows down the screen.

Run as root it refuses and the update stops there — and cleans up after it. omarchy-update opens its transcript and takes its lock before it ever calls the confirmation, so by the time the refusal happens /tmp/omarchy-update.log and $XDG_RUNTIME_DIR/omarchy-update.lock already exist and belong to root; left there they turn the next attempt, the correct one, into Permission denied on a file nobody would think to look for. Only files this run owns are removed, so a lock belonging to the user — a running update — is left alone. 45 of the 88 migrations write into $HOME, which as root is /root: the settings never reach the user who logs in, and the markers recording them as done land there too. A user session is also what the migrations touching systemd --user need — over ssh as root they reach root's bus instead, and one of them stops the update partway through by design, expecting to be retried. --list is refused too. The markers it counts are root's, and root's migrations are ones that must never run: it would answer "86 pending" where the user has none left — a number that looks like work to do and is not. The message gives the command that reads the right ones.

Declining a migration writes its marker file into ~/.local/state/omarchy/migrations, which is how omarchy-migrate records one as done — so it is skipped from then on.

The marker carries the reason inside it: the date, the package declined, and the migration's description. omarchy-migrate only ever tests that the file exists, so the contents are free — and without them a declined migration is indistinguishable from one that ran, both being empty files. Every run lists the retired ones under their own heading, so a decision taken months ago is still visible, and --reset refuses a marker it did not write: removing one omarchy-migrate left would run that migration a second time.

It is installed twice: as itself, and symlinked to omarchy-update-confirm. omarchy-update calls that one by name, so /usr/local/bin comes first and the preview runs inside every omarchy update without a wrapper around any file the omarchy package owns — the router invokes its other steps by absolute path, so this is the one place a /usr/local/bin override lands. Through the symlink it hands over to Omarchy's own confirmation prompt when the questions are done. Remove the symlink and updates go back to asking nothing.

The reading is a preview, not a contract: it parses the install lines out of each migration, and one of them builds its package list from an lspci scan, which reads as installing nothing.

Networking

NetworkManager arrives configured for DHCP on whatever is plugged in — enough to reach the network, not enough for a machine that wants a fixed address, or a bridge so its containers take addresses on the real network instead of sitting behind NAT.

hooks/omarchy-netsetup writes either, from the target, after the first boot:

omarchy-netsetup                                    # asks
omarchy-netsetup -i enp5s0 -b wan -s 10.0.0.88/24 -g 10.0.0.254 -y
omarchy-netsetup -i wlo1 -S MyNetwork --dhcp

It lists the interfaces, defaulting to the one carrying the default route, and on a wired one offers a bridge. Choosing static reads the machine's current address and gateway as the defaults, so pressing enter reproduces what it answers on today rather than inventing something. Give it an address on another subnet and the gateway follows that one instead — the running gateway would be a default that cannot work.

DNS defaults to none, which is not no DNS: it sets ignore-auto-dns on both families, so NetworkManager writes nothing and systemd-resolved stays the only thing deciding — which is where split-DNS and a VPN's search domain live. Naming servers instead writes those.

A wifi interface is scanned and its networks listed, strongest first and one row per network rather than per access point — on a booted system. Scanning is NetworkManager's, so from a chroot the name is typed instead, and nothing checks it: there is nothing there to check it against, and a typo reads the same as a network out of range. The password is asked for either way and sits in the pending file, 600, until the boot that hands it to NetworkManager and deletes it. A bridge on one is refused: a card in client mode cannot bridge, since the access point drops frames from any MAC other than the one that associated. That is the hardware, and the answer to it is NAT — incus admin init makes an incusbr0 that does it.

Nothing is applied while the questions are being answered. The answers go to /etc/omarchy-netsetup.pending, and omarchy-netsetup.service feeds them back to the script at the next boot, where NetworkManager is running and validates every one of them. That is what makes it work from a chroot: nmcli does nothing on its own — it asks NetworkManager over D-Bus, and in a chroot the service is installed with nothing running it.

The interface is remembered by its permanent MAC, read from ip -d link, not by name and not by the address in sysfs. A chroot sees the host's interface names and the installed system need not agree — net.ifnames=0 on one of the two, a different udev, a different driver — but the card is the same card. The address in sysfs is no good either: on an interface with a random MAC, which is what this sets, it is different on every connection.

The old profile on that interface has autoconnect=false written into it, and for an ethernet a no-auto-default drop-in goes in /etc/NetworkManager/conf.d. The service runs after NetworkManager — nmcli needs it answering — so without both, the boot would bring the old configuration up and replace it a moment later, and NetworkManager would make itself a DHCP connection for an interface it found without a profile. Disabled rather than deleted: a pending file that cannot be applied then leaves something to go back to, one nmcli connection modify <name> autoconnect yes away.

Applying is asked for, and the default is no: it takes the interface down and back up, which over ssh ends the session. Either answer leaves the service armed, so it applies at the next boot; sudo systemctl start omarchy-netsetup does it without one.

An attempt that fails puts autoconnect back on whatever it turned off, so the machine comes up on the network it had — which is what makes it reachable enough to find out why. no-auto-default stays: NetworkManager inventing a DHCP connection would claim the interface the next attempt wants, and the old profile coming back covers the same ground properly. The pending file records what it disabled, so each attempt knows what to restore.

A pending file that cannot be applied is retried three times and then left alone, with the reason in the journal. A service failing at every boot forever is noise that stops being read. The connection is written either way and comes up at the next boot. An existing connection on the same interface is shown and its deletion asked for separately — two of them claiming one interface is a coin toss at boot.

Browsers

omarchy-install-browser installs a browser and configures it — the policy directory, the flags file, the two native messaging hosts, the theme. A browser that arrives with the pacstrap, or from the CachyOS repos rather than the AUR, gets the install without the configuration.

hooks/omarchy-config-browser is that second half on its own, installed to /usr/local/bin. It never touches a package:

omarchy-config-browser                                  # interactive
omarchy-config-browser brave                            # configure brave
omarchy-config-browser brave -d bravemod-drive          # and make that .desktop the default
omarchy-config-browser brave -d bravemod-drive -n       # resolve the file, leave the default alone

The executable is checked but never interpreted: it proves the browser is there, which a package query would miss for anything installed outside pacman, but it does not say which browser to configure. A .desktop can be named anything and its Exec can point at a wrapper script, so neither identifies the browser and the script asks rather than guessing — configuring the wrong one writes policy into a directory the browser never reads, which looks like nothing happening at all. A bare name that matches the table is taken as given.

-d accepts a path, or a bare name looked up in ~/.local/share/applications then /usr/share/applications, with or without the .desktop suffix. The home directory comes first, since a file there shadows the system copy.

The browser table — seven browsers, their policy directories and flags files — is parsed out of omarchy-install-browser rather than restated here, so a path that moves upstream moves with it. That makes the shape of its case statement the dependency: if upstream rewrites it, the parse comes back empty and the script stops with a message saying so, rather than configuring against stale paths.

8. First boot

sudo pacman -S brave-bin                        # CachyOS ships it; no AUR needed
omarchy-config-browser brave -d brave-browser   # configure it and make it the default

Or omarchy-install-browser chromium to do both in one go, if chromium was left in the package list.

Without a default browser among the ones omarchy-launch-webapp knows how to drive (brave, chrome, edge, opera, vivaldi, helium), Omarchy's web apps won't launch.

The patches

patches/omarchy-cachyos.patch touches 5 files. Every change answers a real failure seen during the install.

File Change Why
install/post-install/pacman.sh disables the pacman.conf cp -f it would rewrite the target config with the Omarchy mirrors, mixing the two timelines
install/config/all.sh disables snapper.sh snapper isn't in the package set and it targets btrfs; the snapper create-config line has no || true, so set -e halts the install
install/config/all.sh disables firewall.sh ufw dropped from the set; the script calls it unguarded
install/config/enable-services.sh disables docker.socket docker dropped from the set
install/user/all.sh disables chromium.sh chromium dropped; it would create empty NativeMessagingHosts dirs for eight browsers
bin/omarchy-provision-user picks the first browser present xdg-settings set default-web-browser chromium.desktop exits non-zero without chromium, and set -e aborts finalisation before omarchy-done mark

They are applied to the target in step 4c, split by where the package puts the files: patches/install.patch under /usr/share/omarchy, patches/bin.patch under /usr. Re-running the install is safe -- an already-applied patch is recognised and skipped.

A patch that no longer applies stops the install. That means upstream has touched the same lines, and the patch needs rewriting for the new Omarchy version; carrying on would leave the target half-patched.

Boot hooks

install-omarchy-cachyos temporarily masks 60-mkinitcpio-remove.hook and 90-mkinitcpio-install.hook during steps 4-6, the way the ISO orchestrator does: the hardware scripts install firmware and modules, and each one would rebuild the initramfs for every kernel — builds that are always thrown away. An EXIT trap restores them even on failure.

conf/pacman-target.conf keeps limine's pacman hooks out of the target for good, via NoExtract:

NoExtract = etc/pacman.d/hooks/90-mkinitcpio-install.hook
NoExtract = usr/share/libalpm/hooks/*limine*
NoExtract = usr/local/bin/mkinitcpio

The first file belongs to limine-mkinitcpio-hook, and its name is the whole problem: /etc/pacman.d/hooks/ takes precedence over /usr/share/libalpm/hooks/, so it shadows mkinitcpio's own hook of the same name. Same triggers, same Updating linux initcpios... description — but it runs limine-mkinitcpio-install, and with no ESP configured every kernel upgrade ends in FAT32 boot partition not found. Keeping it out hands the job back to mkinitcpio's hook, which was there all along.

The second line covers limine's own hooks, which were left in at first on the grounds that they do other work. That work is deploying to an ESP and pruning boot entries, which needs the ESP that is not configured either, so a kernel upgrade still ended in two errors — from 90-limine-mkinitcpio-remove-post.hook and 80-limine-efi-deploy.hook. The initramfs was built correctly by then; the failures were limine's alone.

The third is not a hook. limine ships /usr/local/bin/mkinitcpio, which comes before /usr/bin on PATH: it runs the real mkinitcpio, then on -P prints This does not update Limine boot entries and asks whether to run limine-mkinitcpio too. That question is what outlived every hook above — and in a chroot, where nothing answers it, read returns empty and its case treats the empty reply as yes. The error then arrives from inside the initcpio step rather than as a step of its own, right after Initcpio image generation successful.

NoExtract also only governs what pacman writes from the moment it is read, and by then the hooks are already there: the pacstrap runs against conf/pacman-install.conf, which carries no NoExtract at all, so limine and its hooks land in the target before this config reaches it. Step 4a deletes the four files for that reason — the NoExtract lines keep them from coming back on the next upgrade.

NoExtract skips a file rather than deleting it, so removing the lines is not enough to get the hooks back either: pacman never revisits a file it skipped, and the packages have to be reinstalled. limine-guard does both, asking pacman -Qo which packages own the hooks rather than assuming, along with everything else limine needs.

limine

Every limine command in the target is a symlink to hooks/limine-guard in /usr/local/sbin, which comes before /usr/bin on root's PATH. Running any of them explains why limine is disabled instead of failing obscurely:

limine-guard --status     # which of the two states it is in
limine-guard --enable     # restore the hook, configure the ESP, install limine
limine-guard --disable    # back to the routed commands and no hook

--enable refuses to run in a chroot or without an ESP, and it checks that the hook actually reappeared after the reinstall — putting NoExtract back if it did not, rather than leaving the target in neither state.

fastfetch

The logo is Omarchy's frame around the CachyOS mark, which is what this system is. Omarchy's /etc/fastfetch/config.jsonc is not in the package's backup= list, so an edit there is overwritten by the next upgrade without even a .pacnew. Instead:

hooks/omarchy-release-fastfetch          ->  /usr/local/bin/
hooks/fastfetch                          ->  /usr/local/bin/
hooks/release.txt                        ->  /etc/fastfetch/
hooks/96-omarchy-release-fastfetch.hook  ->  /etc/pacman.d/hooks/

omarchy-release-fastfetch derives /etc/fastfetch/release.jsonc from Omarchy's config, changing three keys (logo.type to file-raw, logo.source to the combined logo, dropping logo.color) and passing the module layout through untouched. The pacman hook re-derives it whenever the omarchy package rewrites the config, so the modules follow upstream and only the logo differs. The fastfetch wrapper points at the result, and steps aside for -c, --config, -h and --version, or if release.jsonc is missing.

To go back to Omarchy's own logo: rm /usr/local/bin/fastfetch.

Listing what gets installed

bin/list-packages answers the question at three levels of detail:

./bin/list-packages             # 148, annotated
./bin/list-packages -l          # 148, names only
./bin/list-packages -t          # which releases there are
./bin/list-packages -t v4.0.0   # a specific one
./bin/list-packages -d          # plus every dependency
./bin/list-packages -r          # each name prefixed with its repo
./bin/list-packages -s          # how many per repo, and how much they weigh
./bin/list-packages -dlr        # short options bundle
cachyos-extra-v3        81    1.5 GiB
extra                   34    778 MiB
omarchy                 19    326 MiB
...
total                  148    2.9 GiB

The default output is a package a line with its size and description, which is what makes it worth reading before writing anything:

aether                             #  18.9 MiB  Desktop theming application...
alsa-utils                         #   2.6 MiB  Advanced Linux Sound Archite...
asdcontrol                         #    30 KiB  Control brightness on Apple ...

It reads omarchy-base.packages from a release tag on GitHub, the newest unless -t names one. -b adds the ten the ISO installs before it reaches that list -- seven are in it already, and of the rest base is on the pacstrap command line, efibootmgr arrives as a dependency of limine, and base-devel only exists to build AUR packages.

This is what Omarchy selects, not necessarily what gets installed: the lines you commented out of packages.txt are the difference, and diff shows it:

diff <(./bin/list-packages -l | sort) \
     <(sed -e 's/#.*//' -e 's/[[:space:]]*$//' packages.txt | grep . | sort)

Sizes and descriptions come from the repo databases, which takes a few seconds; -l skips them. -d resolves dependencies and needs pacman; everything else only needs curl.

The package set

packages.txt starts as the 147 names in omarchy-base.packages plus the ten the ISO installs ahead of it. What this repo dropped from that, ~1.25 GB worth, is a decision per machine rather than part of the repo: chromium, libreoffice-fresh, docker and friends, ufw, obs-studio, obsidian, mpv, localsend, dotnet-runtime.

Added on top are packages Omarchy's lists don't declare because the official install gets them elsewhere:

  • sudo — archinstall installs it as part of the base system, but omarchy-settings drops three files into /etc/sudoers.d/ and several install/ scripts invoke it
  • vulkan-intel, intel-media-driver, libvpl, vpl-gpu-rt, intel-lpmd, thermald, sof-firmware — installed conditionally by the hardware scripts via omarchy-pkg-add, which fails with target not found in a chroot with no synced databases. Without sof-firmware, Intel audio exposes only a Dummy Output.
  • webkit2gtk-4.1, gtk4-layer-shell — dependencies of aether, tensaku and hyprland-preview-share-picker
  • perl, pipewire, cachyos-keyring, linux-cachyos-zfs, zfs-utils, cachyos-zfs

The Omarchy packages come from the [omarchy] local repo during the pacstrap, so pacman resolves their dependencies itself.

The 19 Omarchy packages

They come from the [omarchy] local repo built out of the ISO, so pacman records their origin properly. Until the upstream repo is declared there is nothing to upgrade them from, so they stay at the ISO's version. The upstream [omarchy] repo 404s on the stable channel while 4.0.0 is in alpha; conf/pacman-target.conf documents how to add it (last, never ahead of the CachyOS repos).

Notes

  • snapper ends up installed (hard dependency of omarchy) but snapper.sh stays disabled: on a ZFS root, snapshots are zfs snapshot. On btrfs the line in install/config/all.sh can be re-enabled.
  • The Omarchy repo doesn't list sudo, pipewire or perl among its packages: they look resolvable when you query pacman -Si from a system that already has them. The same applies to the 19 Omarchy packages' dependencies.

Tracking where packages came from

pacman does record it, in the local database's %INSTALLED_DB% field -- pacman -Qi prints it as "Installed From". But it only fills that field for packages it actually downloads, so a pacstrap over a populated target leaves it empty for everything it skips. On this install that was 686 packages out of 845, including most dependencies.

Two pieces close the gap, one per phase.

After the pacstrap, pactrack-from-repo reads the answer off the local repos: /repo/<name>/ holds exactly the files that went in, and the directory name is the repo name. Matching <pkgname>-<version> against those files is exact -- no lookup, nothing to drift. install-omarchy-cachyos runs it in step 4e:

sudo pactrack-from-repo --write-db /mnt

It writes /var/lib/pactrack/origins (name<TAB>repo), and with --write-db also fills in the empty %INSTALLED_DB% fields, so pacman -Qi answers for every package. It never overwrites a value pacman wrote: that one records what happened, while this one is inferred from which repo holds the file today.

From the first boot, the hook takes over. There %INSTALLED_DB% is reliable and pacman fills it in for dependencies too, so the hook simply copies it into the file after every transaction:

hooks/95-pactrack.hook  ->  /etc/pacman.d/hooks/
hooks/pactrack-hook           ->  /usr/local/bin/

Keeping the file alongside pacman's own field is the point: if the field is ever lost, --write-db puts it back.

To read it:

pactrack              # every package, grouped by repo
pactrack omarchy      # only what came from the omarchy repo
pactrack -c           # per-repo counts
pactrack -u           # packages the file has never seen

pactrack omarchy is the useful one here: it lists the 19 packages that do not come from CachyOS.

A package installed with pacman -U from a file has no source repo and is recorded as local -- accurate, since it came from none. The 19 Omarchy packages avoid that by coming from the [omarchy] local repo during the pacstrap.

Adding the upstream [omarchy] repo

The repo carries 202 packages, 25 of which also exist in CachyOS/Arch. On every one of those overlaps CachyOS is at an equal or newer version, so declaring it last is enough to keep CachyOS in charge:

sudo tee -a /etc/pacman.conf > /dev/null <<'EOF'

# Last, and without Upgrade: the repo is a source to pull from explicitly,
# never one that decides during a -Syu.
[omarchy]
SigLevel = Optional TrustAll
Usage = Sync Search Install
Server = https://pkgs.omarchy.org/stable/$arch
EOF

Usage without Upgrade keeps it out of -Syu resolution entirely: no CachyOS package can be replaced by an Omarchy build. The Omarchy packages are then upgraded by naming them, which is what pactrack is there to answer:

sudo pacman -S $(pactrack omarchy)