Files
omarchy-iso/plans/oem-install.md
T
David Heinemeier HanssonandClaude Fable 5 98a345c017 Update OEM comments to the renamed factory-reset command
Reference omarchy-system-factory-reset (was omarchy-reset-computer) in the
orchestrator comments and the plan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 00:52:14 -07:00

7.7 KiB

OEM Install + Factory Reset Plan

Goal

An install mode that installs the entire system but defers user account setup to first boot. On first power-on the machine asks for the user, creates it, and lands in the already-installed system — the classic OEM out-of-box experience. The same machinery doubles as a "reset computer" option for wiping a machine before selling it.

Core idea: "OEM state" as a first-class system state

Define one canonical state — fully installed, no user, setup pending — and make both paths produce it:

  • ISO OEM mode ends the install in OEM state instead of creating the user.
  • Reset returns a running machine to OEM state.

OEM state is: a fully installed system plus /var/lib/omarchy/oem/ containing:

File Purpose
pending Flag that arms the first-boot setup service
packages/node-v*.tar.gz Node tarball copied off the ISO's /opt/packages (~30MB) so omarchy-finalize-user works offline at first boot
groups Deferred usermod -aG groups written by system setup (see below)
luks-key Throwaway LUKS passphrase for encrypted installs, staged for auto-unlock until first-boot re-key

Why the seam is nearly clean already

Everything in the orchestrator through finalize_limine_boot is user-independent except five touch points:

  1. installer.create_users — useradd + password (root gets the same password).
  2. Three usermod -aG calls inside omarchy-setup-system's scripts (config/docker.sh, hardware/input-group.sh, hardware/apple/fix-t2.sh — docker/input/video). This is the only reason omarchy-setup-system requires the user to exist (its getent check).
  3. run_chroot_finalizer → omarchy-finalize-user as the user. Already offline-safe: /etc/skel does the heavy seeding at useradd time, omarchy-mise-install writes lazy wrappers, and the only external artifact is the bundled Node tarball from /opt/packages (install/user/mise.sh).
  4. configure_login — SDDM state.conf and the encrypted-install autologin.
  5. Encryption — the user's password is the LUKS passphrase.

Nothing in hardware setup, Limine, hibernation, snapper, or package installation cares who the user is.

First boot: omarchy-oem-setup.service

Ships in the omarchy runtime package (so reset can arm it without the ISO):

  • ConditionPathExists=/var/lib/omarchy/oem/pending
  • Before=display-manager.service, conflicts getty@tty1, TUI on tty1.
  • Runs the user form extracted from the configurator (Step 2: username, password, full name, email). The configurator is bash+gum, so the form moves into a runtime-shipped omarchy-oem-setup script that reuses the exact ISO look (logo, Tokyo Night VT palette).

Then it does what the ISO would have done:

  1. useradd with wheel + the groups from /var/lib/omarchy/oem/groups, set user + root password.
  2. omarchy-finalize-user as the user, with a setup context that points at the stashed Node tarball (a sibling of the existing iso-chroot context).
  3. The configure_login writes: SDDM state.conf, autologin for encrypted installs.
  4. If luks-key exists: cryptsetup luksChangeKey to the user's password, delete the stash, remove the auto-unlock keyfile, rebuild initramfs.
  5. Git identity from the form (the OMARCHY_USER_NAME/OMARCHY_USER_EMAIL path in install/user/git.sh).
  6. Remove /var/lib/omarchy/oem/, hand off to SDDM.

The user lands in their session exactly like a normal install.

Encryption: keep it, Pop!_OS-style

Do not force OEM installs unencrypted. Install encrypted with a generated throwaway passphrase, stage a keyfile so boot auto-unlocks during the OEM window, and have first-boot setup re-key to the user's chosen password (step 4 above). Limine and mkinitcpio don't care what the passphrase is. This preserves encrypted-by-default without the machine builder ever knowing the end user's password.

ISO-side changes (this repo)

  • Configurator: an OEM choice in the install-mode step that skips Step 2 (user), plus a cidata oem marker file that replaces the user_credentials.json requirement — that's what actual OEM imaging rigs will use via autoinstall.
  • Orchestrator: oem: true in the omarchy_install JSON. Swaps the three user phases (create_users inside arch_install_system, run_chroot_finalizer, the user parts of configure_login) for a "stage OEM state" phase that writes /var/lib/omarchy/oem/ and enables omarchy-oem-setup.service in the target. InstallContext.username needs an OEM-safe fallback (creds are absent).
  • Factory snapshot: at the end of every install (not just OEM), take a read-only snapshot of @ kept at the top level as @factory — outside snapper's .snapshots, so cleanup timers and the Limine snapshot menu never touch it. Zero bytes at creation; grows only with drift. This is what makes reset a true factory reset.

Runtime-side changes (omarchy repo)

  • omarchy-oem-setup + omarchy-oem-setup.service (the OOBE above).
  • omarchy-system-factory-reset (below).
  • Relax omarchy-setup-system: when the install user doesn't exist yet, the three usermod -aG scripts append to /var/lib/omarchy/oem/groups instead of failing the getent check.

Reset: omarchy-system-factory-reset

Behind a very explicit confirmation (typed, not y/n):

  1. Point the default subvolume at a writable snapshot of @factory; stage a next-boot wipe unit.
  2. On reboot: recreate @home (subvolume delete/create — instant, no rm -rf), clear @log, delete snapper snapshots, reset machine-id, SSH host keys, NetworkManager connections, Tailscale state, SDDM state; run fstrim.
  3. Re-arm OEM state: pending flag, groups, Node tarball (kept from install or re-fetched into @factory at install time), and on encrypted machines a throwaway LUKS re-key — the buyer never needs the seller's passphrase.

Next boot is the same OOBE as a fresh OEM install. User-installed packages and /etc drift are gone, which is what "wipe and sell" actually promises.

Machines installed before @factory existed get a degraded reset — keep the current system, wipe users/state only — with that caveat surfaced in the confirmation.

Honest limitations (document these)

  • On unencrypted disks this is deletion, not forensic erasure. fstrim helps on SSDs; offer cryptsetup reencrypt or blkdiscard + reinstall for the paranoid.
  • luksChangeKey changes the passphrase but not the volume key; freed extents remain readable to someone with raw-device access and the new passphrase. Same remedy as above.
  • A reset machine boots with day-one packages until it updates — same as a fresh install.

Sequencing

The OOBE lives in the omarchy runtime package, but the ISO's OEM phase depends on it existing. So:

  1. Ship the runtime side first (omarchy-oem-setup, service, reset command, omarchy-setup-system relaxation).
  2. ISO OEM mode then asserts the service file exists in the target before offering the mode.
  3. Factory snapshot can land with either half; reset requires both.

Validation

  • VM: OEM install (encrypted + unencrypted) → reboot → OOBE → session; verify LUKS passphrase is the user's, throwaway key gone, groups applied, omarchy-finalize-user state marker present.
  • VM: normal install → use system → omarchy-system-factory-reset → OOBE as new user; verify no trace of prior user (home, NM connections, tailscale, machine-id, host keys, snapper snapshots).
  • Autoinstall: cidata drive with oem marker and no credentials → unattended OEM install.
  • Negative: OEM ISO built against a runtime package lacking the service fails the install with a clear error, not a user-less boot loop.