Terminal Workflows · Software Engineering

Omarchy 4 on Asahi Arch: Every Trap I Hit on an M1 MacBook

Omarchy 4 on the M1, recorded on the machine this post is about: a theme switch (wallpaper, bar and window borders change together), then Neovim and lazygit on one workspace, btop and lazydocker on the next, and the browser on the external 4K.

I am a Rails developer by heart. My comfort zone is models, controllers and a test suite that goes green. Spending a day inside initramfs hooks, evdev ioctls and device-tree platforms is not where I normally live — which is exactly why this was worth writing down.

Earlier this year I got an external monitor working on Asahi Fedora by building the Fairydust kernel. This time I went further: a clean wipe, Asahi Arch (ALARM) underneath, and Omarchy 4 on top.

The install itself takes an hour. The remaining day went to four bugs that share one shape: an assumption from x86 that does not crash on ARM — it just quietly returns the wrong answer. Those are the interesting ones, so they get the most space here.

Overview

  • Installed Asahi Arch (ALARM) from macOS, then Omarchy 4 by following the omarchy-mac fork's own README.
  • Rebooted into a machine with no Wi-Fi, no Bluetooth, no Touch Bar, no audio — one missing initramfs hook.
  • Restored the audio stack, which Omarchy does not install on Apple Silicon.
  • Built the Fairydust kernel for USB-C DisplayPort Alt Mode.
  • Fixed clamshell mode, screen brightness, and Bluetooth pairing.

Omarchy 4, "Quattro"

Omarchy 4 shipped as Quattro, and DHH presents the release himself in an 81-minute walkthrough. If you are arriving from macOS and want to know what this thing actually is before wiping a disk for it, that is the place to start — it is a tour of the system by the person who designed it, not a feature list.

One stretch of it explains something you will feel directly on Apple Silicon. From 49:50 he covers packaging: after a rough year for the AUR — outages, and some security trouble — Omarchy stopped depending on it at runtime and now builds, verifies and hosts its own packages instead. That repository is x86-only. Everything in section 1 below that takes 40 minutes instead of five traces back to this: on ARM there is nothing prebuilt to install, so the fork compiles the same packages on your machine.

Omarchy Quattro walkthrough by David Heinemeier Hansson Omarchy Quattro — David Heinemeier Hansson · opens at 49:50
The Quattro release walkthrough, opening at the packaging section. Plays here on click; nothing is loaded from YouTube until then.

1) Install Asahi Arch, then Omarchy

Follow the guide in the repository itself, not this post. The omarchy-mac README is the maintained install document — partitioning, the Asahi Alarm installer, initial Arch setup, creating your user, and the install itself. It is kept current with the fork; everything below is only a summary of the path I took, and the rest of this post starts after the install has finished.

The short version. Run the Asahi Alarm installer from macOS and pick Asahi Arch (ALARM) Minimal:

curl https://asahi-alarm.org/installer-bootstrap.sh | sh

Boot the result, update, and create a regular user — the Omarchy installer refuses to run as root and calls sudo where it needs to. Then install from the Apple Silicon fork:

sudo pacman -Syu
git clone --branch quattro https://codeberg.org/malik-na/omarchy-mac.git ~/.local/share/omarchy
cd ~/.local/share/omarchy && bash install.sh

Budget around 40 minutes, almost all of it building AUR packages, because Omarchy's own package repo has no Apple Silicon builds — the packaging split described above. A few packages have no ARM build at all and are reported at the end instead of failing the run — that is expected. If mirrors fail, bash fix-mirrors.sh from the repository root and retry.

Two things the README covers that are easy to skip and expensive to skip: back macOS up first, and leave at least 50 GB free (100 GB if you intend to build the kernel in section 4). Check the reported package failures before you reboot.

Let it finish, then reboot. This is where the trouble starts.

2) The bootstrap trap: no Wi-Fi after install

The machine came up with no Wi-Fi, no Bluetooth, no Touch Bar, no ambient light sensor and no audio. dmesg showed every one of them failing the same way:

brcmfmac 0000:01:00.0: Direct firmware load for brcm/brcmfmac4378b1-pcie.bin failed with error -2
hci_bcm4377 0000:01:00.1: Unable to load firmware; tried 'brcm/brcmbt4378b1-apple,honshu-m.bin' ...
apple-z2 spi0.0: unable to load firmware
iio_aop_als als.0.auto: probe with driver iio_aop_als failed with error -2

Error -2 is ENOENT. Every Apple vendor firmware blob was missing at once, which points at one thing: the asahi initcpio hook, which unpacks <ESP>/vendorfw/firmware.cpio into a tmpfs at /lib/firmware/vendor during early boot.

One command confirms it never ran:

findmnt -t tmpfs /usr/lib/firmware/vendor   # silence = the hook did not run

The cause is /etc/mkinitcpio.conf.d/omarchy_hooks.conf, which assigns HOOKS rather than amending it:

HOOKS=(base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt filesystems fsck btrfs-overlayfs)

Arch ARM ships /etc/mkinitcpio.conf with HOOKS=(base asahi udev ...), and drop-ins are sourced after the main config — so asahi is silently discarded.

This is a genuine bootstrap trap: with no Wi-Fi you cannot pacman -S your way out. /var/log/pacman.log pins the moment exactly:

[13:24:14] [PACMAN] starting full system upgrade          <- network fine
[13:35:14] omarchy_hooks.conf written
[13:36:28] ==> Using drop-in configuration file: 'omarchy_hooks.conf'
                 ^- initramfs rebuilt without `asahi`

Fix. A drop-in that sorts after Omarchy's and puts the hook back:

# /etc/mkinitcpio.conf.d/zz-asahi-hook.conf
if [[ -e /usr/lib/initcpio/install/asahi ]] && [[ " ${HOOKS[*]:-} " != *" asahi "* ]]; then
  _hooks=(); _added=0
  for _h in "${HOOKS[@]}"; do
    _hooks+=("$_h")
    [[ $_h == base ]] && { _hooks+=(asahi); _added=1; }
  done
  ((_added)) || _hooks=(asahi "${_hooks[@]}")
  HOOKS=("${_hooks[@]}")
  unset _hooks _h _added
fi

Then rebuild and verify before rebooting, because getting this wrong leaves you offline:

sudo mkinitcpio -P
sudo lsinitcpio /boot/initramfs-linux-asahi.img | grep hooks/asahi

After a reboot, /usr/lib/firmware/vendor is a tmpfs with ~339 files, and Wi-Fi, Bluetooth, Touch Bar, the light sensor and the camera ISP all come back at once. Reported as omarchy-mac#156.

Terminal output showing the vendor firmware tmpfs mounted with 339 files, Broadcom firmware blobs present, Wi-Fi connected and neither radio blocked
The same machine today: the hook ran, the vendor firmware is unpacked into a tmpfs, and both radios are alive.

3) Audio

Firmware alone is not enough — Omarchy does not pull in the Apple Silicon audio stack. Both ALSA cards existed but PipeWire had zero sinks:

sudo pacman -S --needed asahi-audio speakersafetyd pipewire-pulse pipewire-alsa
sudo systemctl enable --now speakersafetyd

speakersafetyd is not optional. The UCM profile refuses to enable the speakers without it, because it is what stops you physically destroying them. Once running, the default sink becomes MacBook Pro J293 Speakers through a convolver DSP chain.

While here, set the regulatory domain — mine defaulted to country 00, which forces passive scan on 5 GHz:

sudo sed -i 's/^#WIRELESS_REGDOM="PL"/WIRELESS_REGDOM="PL"/' /etc/conf.d/wireless-regdom
Terminal output listing both MacBook Pro J293 ALSA cards, speakersafetyd reported active, and the J293 convolver and microphone filter chains in PipeWire
Both ALSA cards, speakersafetyd running, and the J293 convolver chain that shapes what actually reaches the speakers.

4) The Fairydust kernel for the external monitor

Stock linux-asahi still has no USB-C DisplayPort Alt Mode, so the external monitor needs a patched kernel. I packaged it for pacman rather than building it by hand as I did on Fedora — that keeps the stock kernel installed as a rescue entry, and GRUB lists both.

The build is pinned to one exact Asahi release (7.1.6-1 plus a 13-commit patch). Budget 1–3 hours, keep it plugged in, and expect ~35 GiB of scratch space. Afterwards:

$ uname -r
7.1.6-1-1-fairydust-ARCH
$ ls /sys/class/drm/ | grep DP
card3-DP-1
Terminal on the finished machine: fastfetch reporting a MacBook Pro M1 on the Fairydust kernel, and verify.sh printing OK for every hardware check
The whole machine checked in one pass: Fairydust booted, the DisplayPort connector present, and every other piece of hardware answering.

The scripts

Three files did the work, and they are on this site if you want them:

  • PKGBUILD — builds linux-asahi-fairydust pinned to Asahi 7.1.6-1, and sets a distinct localversion so it installs beside stock linux-asahi rather than over it:

    echo "-${_asahirel}-${pkgrel}-fairydust" > localversion.10-pkgrel
    
  • build-and-install.sh — refuses to run unless it is on aarch64, on a J293, on Arch and with 35 GiB free; then installs the build dependencies and the pinned Rust toolchain, runs makepkg, installs the package, rebuilds the initramfs, updates m1n1 with the patched DTBs and regenerates the GRUB menu.

  • verify.sh — the OK/FAIL list in the screenshot above. Run it after the first Fairydust boot.

Two things the package needs are not mine to hand out, so the README explains how to produce them: the kernel config (start from zcat /proc/config.gz on a machine running stock linux-asahi) and fairydust.patch itself, which is the Asahi Linux project's work on their fairydust branch and should be generated from their tree:

git diff asahi-7.1.6-1..origin/fairydust > fairydust.patch
updpkgsums   # the PKGBUILD pins checksums

Read them before running them. The hardware guard is for my machine, and the version pin is the whole point — see below.

The part worth remembering: this package does not follow linux-asahi updates. When the stock kernel moves past 7.1.6, Fairydust must be rebuilt against the new tree and the patch may not apply. Do not uninstall the working version until the rebuild is tested, or you will boot into a machine with no external display and no obvious reason why.

5) Clamshell mode — three layers deep

Closing the lid with the monitor attached should blank the internal panel and keep working. It did nothing. This one took three attempts because each fix revealed the next problem.

Layer one. omarchy-hw-laptop-closed reads the lid from ACPI:

for state in /proc/acpi/button/lid/*/state; do ...; done
exit 1

Apple Silicon is a device-tree platform with no ACPI at all. The glob never matches, the loop body never runs, and the script always reports "open".

Layer two. The obvious fix is to read SW_LID from evdev instead. My first probe suggested the driver did not maintain the switch-state bitmap — but that turned out to be my own measurement racing the event. A proper probe, reading state immediately after the event and sanity-checking the ioctl encoding against a keyboard, showed EVIOCGSW works correctly. No kernel bug. Worth saying plainly, because I nearly filed one.

Layer three. Dropping a fixed script in /usr/local/bin appeared to work for exactly one second, then reverted. omarchy-hyprland-monitor-watch re-runs the clamshell handler 1s, 3s and 7s after any monitor change, and its PATH starts with /usr/share/omarchy/bin — ahead of /usr/local/bin. The override was being bypassed by the packaged version.

Fix. Stop fighting the watcher and move the decision into monitors.lua, which Hyprland re-evaluates on every reload, including the watcher's:

local state_home = os.getenv("XDG_STATE_HOME") or (os.getenv("HOME") .. "/.local/state")
local lid_closed = file_exists(state_home .. "/omarchy/lid-closed")

if lid_closed and external_display_connected() then
  hl.monitor({ output = "eDP-1", disabled = true })
else
  hl.monitor({ output = "eDP-1", mode = "preferred", position = "0x0", scale = 2 })
end

The lid flag is written by Hyprland's switch bindings, which are reliable:

o.bind("switch:on:Apple SMC power/lid events",  nil, "omarchy-lid-clamshell closed", { locked = true })
o.bind("switch:off:Apple SMC power/lid events", nil, "omarchy-lid-clamshell open",   { locked = true })
Terminal output showing no ACPI lid path, the Hyprland Apple SMC lid switch device, the lid-closed state file, a clamshell log, and only the external DP-1 monitor active
Captured with the lid shut: no ACPI path to read, the switch device Hyprland does see, and eDP-1 gone while the external 4K keeps running.

One nice surprise: systemd-logind gets this right on its own. It counts connected non-eDP connectors, treats the machine as docked, and applies HandleLidSwitchDocked=ignore — so it never suspends while the monitor is attached. That part needed no work at all.

6) Screen brightness was dimming the Touch Bar

The brightness keys "did nothing". Capturing the actual keycodes showed the keys were fine — both the Touch Bar and my Bluetooth keyboard emit a correct KEY_BRIGHTNESSDOWN, already bound by Omarchy. The bug was in device selection:

# omarchy-hw-display
device="$(ls -1 /sys/class/backlight | grep -vx appletb_backlight | head -n1)"

First entry alphabetically, then a search for gmux / amdgpu / intel / acpi_video — all x86-only names. On this machine:

228600000.dsi.0    <- digits sort first, so this wins
apple-panel-bl     <- the actual panel, max 509
Terminal output showing two backlight devices, the Touch Bar device winning the alphabetical pick, and its max brightness of 255 against the panel's 509
The two backlight devices side by side. The one that sorts first is the Touch Bar strip; the panel is the one with 509 steps.

228600000.dsi.0 is the Touch Bar backlight. The keys had been dimming the strip above the keyboard the whole time — which is why it looked like they controlled the keyboard light. Fixing it is a one-line preference for apple-panel-bl in a /usr/local/bin override.

Note that external monitor brightness is not fixable: ddcutil detect reports No display adapters with i2c buses appear to exist, so DDC/CI is unavailable and the Samsung has to be adjusted with its own buttons.

7) Bluetooth: use the right transport

My Magic Trackpad and keyboard paired but never connected. Two separate causes:

The trackpad was plugged in over USB. Apple Magic devices switch to wired mode and refuse Bluetooth while connected. Unplugging fixed it — and as a bonus, the magicmouse: unable to request touch data (-32) errors that broke gestures turn out to happen only over USB. Over Bluetooth it registers cleanly.

The keyboard needed a re-pair, but would not show up in scans. Both devices are Bluetooth Classic (BR/EDR), not BLE, and a plain scan on misses them:

bluetoothctl
> menu scan
> transport bredr
> back
> scan on

They appear immediately. Pair while the scan is still running — BlueZ drops undiscovered devices from its cache the moment scanning stops, which is what made this look harder than it was.

What still does not work

Worth being honest about the remaining gaps:

  • Hardware video decode. avd wants apple/avd-fw-v2-t0.bin, which Apple does not ship in the vendor firmware set. Software decode covers it.
  • External monitor brightness. No I2C buses, so no DDC/CI.
  • mpv crashes with the webcam overlay — a SIGSEGV inside libvulkan_asahi while uploading a frame via libplacebo. Recording itself works; the webcam overlay does not.
  • Wi-Fi Direct / P2P is unsupported by this firmware (-52 on every attempt).

Sources

Was it worth it?

Yes, and not only for the machine. As a Rails developer, most of my debugging is one layer deep: read the stack trace, fix the code. Every bug here failed silently — a glob that matched nothing, a head -n1 that picked the wrong device, a PATH that put the packaged script first. Nothing crashed. Everything returned a plausible wrong answer.

The habit that actually worked was refusing to trust my own reasoning. Twice I concluded "this should work now" from reading configuration, and twice I was wrong. What settled it each time was measuring at the moment of the event — probing the lid switch as it closed, timing the monitor watcher at t+0/1/2/3s, capturing the real keycode instead of assuming which one fired.


Note: If you follow this guide, proceed with caution. I don't take responsibility if you damage your system. This involves building and installing a custom kernel and editing boot configuration, which can potentially break things. Keep the stock kernel installed, and verify your initramfs before you reboot.

Grzegorz Smajdor
Product builder and technical partner helping founders and companies turn ideas and complex technical challenges into working software.

Back to all insights

Keep reading

All writing

How to Scope an MVP Before Development

A practical framework for reducing a product idea to a useful first release, exposing risk and creating a roadmap before developme…

Modernise or Rewrite? Start With Evidence

A practical way to assess a difficult software product and decide what to stabilise, modernise or replace without defaulting to a …