GC28-0001-0 First Edition (August 2026)

Arch laptop runbook

Plain Arch on a Framework 13 (or any UEFI x86-64 laptop). Manual install,
LUKS2 + ext4, systemd-boot, Hyprland. Substitute your own username
(ppalmer), hostname (prismo) and GitHub account where they appear.

Stage 1 runs from the USB installer as root and builds the machine; Stage 2
runs from the installed system as your user and restores your home directory.

Stage 1 - USB installer

Run as root in the live ISO. Everything before arch-chroot runs in the live
ISO; everything after it runs inside the chroot until exit.

  1. Boot the official Arch ISO. Verify firmware is UEFI:

    cat /sys/firmware/efi/fw_platform_size      # 64
    

    (If empty, the machine is BIOS and this plan needs GRUB instead of
    systemd-boot.)

  2. Network: iwctl (wifi) or ethernet, then ping archlinux.org.
  3. timedatectl set-ntp true

Partition

Adjust the device name to your disk:

fdisk /dev/nvme0n1
# 1 GiB  type "EFI System"
# rest   type "Linux filesystem"

Encrypt and format

cryptsetup luksFormat --type luks2 /dev/nvme0n1p2
cryptsetup open /dev/nvme0n1p2 cryptroot
mkfs.fat -F 32 /dev/nvme0n1p1
mkfs.ext4 /dev/mapper/cryptroot

Mount and pacstrap

mount /dev/mapper/cryptroot /mnt
mount --mkdir /dev/nvme0n1p1 /mnt/boot
pacstrap -K /mnt base linux linux-firmware amd-ucode base-devel networkmanager nano
genfstab -U /mnt >> /mnt/etc/fstab

Configure inside the chroot

arch-chroot /mnt
ln -sf /usr/share/zoneinfo/<Region>/<City> /etc/localtime
hwclock --systohc
echo 'prismo' > /etc/hostname

Locale

sed -i 's/^#en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen
locale-gen
echo 'LANG=en_US.UTF-8' > /etc/locale.conf

Bootloader

Bootloader first: bootctl install creates /boot/loader/, so neither config
file below can be written before it runs.

bootctl install

Create /boot/loader/loader.conf:

cat > /boot/loader/loader.conf << 'EOF'
default arch.conf
timeout 7
console-mode max
editor no
EOF

Create /boot/loader/entries/arch.conf:

cat > /boot/loader/entries/arch.conf << 'EOF'
title   Arch Linux
linux   /vmlinuz-linux
initrd  /amd-ucode.img
initrd  /initramfs-linux.img
options rd.luks.name=<LUKS-UUID>=cryptroot root=/dev/mapper/cryptroot rw
EOF

The UUID is the LUKS container’s (cryptsetup luksDump /dev/nvme0n1p2, or
blkid line with TYPE="crypto_LUKS"). The options keyword is required.

In /etc/mkinitcpio.conf, uncomment the HOOKS line with sd-encrypt, then
rebuild the initramfs:

sed -i 's/^#.*sd-encrypt.*/HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block sd-encrypt filesystems fsck)/' /etc/mkinitcpio.conf
mkinitcpio -P

User and sudo

useradd -m -G wheel -s /bin/bash ppalmer
echo '%wheel ALL=(ALL:ALL) ALL' > /etc/sudoers.d/10-wheel
chmod 0440 /etc/sudoers.d/10-wheel
visudo -c -f /etc/sudoers.d/10-wheel     # always validate
passwd                  # root
passwd ppalmer
systemctl enable NetworkManager

Reboot

Exit, unmount, reboot, and remove the USB when it powers back up:

exit
umount -R /mnt
reboot

Stage 2 — From the installed system

Boot into Arch and log in at the TTY as your user.

Bootstrap

sudo pacman -S --needed sudo git openssh chezmoi

AUR helper

One manual step buys automation for every AUR package after it. yay-bin is
upstream’s prebuilt binary — seconds instead of a Go compile:

git clone https://aur.archlinux.org/yay-bin.git
cd yay-bin
makepkg -si          # asks for your sudo password at the end
cd .. && rm -rf yay-bin

Skip it and the apply stops at the AUR tier, printing this same recipe.

SSH key

Copy your key from another machine, then add it to the agent:

# From your other machine:
#   scp ~/.ssh/prismo_ed25519 ppalmer@<this-ip>:~/.ssh/
#   ssh ppalmer@<this-ip> chmod 600 ~/.ssh/prismo_ed25519

systemctl --user start ssh-agent.socket
SSH_AUTH_SOCK=/run/user/1000/ssh-agent.socket ssh-add ~/.ssh/prismo_ed25519
ssh -T git@github.com                         # expect Hi <user>!

Clone and apply

chezmoi init --apply git@github.com:ppalms/dotfiles.git --branch v2

Prompts for name, email and font size. Pass --promptString for unattended runs.

Linger

sudo loginctl enable-linger $USER

Without this, /run/user/1000 is never created and the ssh-agent socket fails.

Neovim first run

nvim                                           # then :q — installs treesitter parsers

Explicit packages

sudo pacman -D --asexplicit diffutils

Desktop

Nothing to type any more. The whole session stack is declared in the
native: group of .chezmoidata/packages.yaml — hyprland and portals,
waybar, Alacritty, qutebrowser, PipeWire, GPU drivers — and installed by the
apply above; 1password rides in aur:. How pacman, the AUR and yay divide
the work, and why some packages are scoped to this machine only:
package management.

Start from the TTY:

start-hyprland

The config is Lua (chezmoi edit ~/.config/hypr/hyprland.lua). Edit colors in
.chezmoidata/gruvbox.yaml, never in the configs.

Browser

qutebrowser renders with Chromium/Blink, so pages look like they do in Chrome —
enough for testing web apps without a full Chromium install. Config, bookmarks
and quickmarks are plain text under ~/.config/qutebrowser/, managed by chezmoi:

ls ~/.config/qutebrowser/config.py ~/.config/qutebrowser/bookmarks/urls \
  ~/.config/qutebrowser/quickmarks

Bookmark pages with M, then commit the resulting diff to keep the repo current.
Test against a dev server:

qutebrowser http://localhost:<port>

Keyboard

Type-test it. hyprctl will lie. The layout is Colemak-DH. Test with
CapsLock+Space+n — if .XCompose is missing, it fails silently.

Verify

m=""; for c in diff cmp sha256sum jq awk; do command -v "$c" >/dev/null || m="$m $c"; done
[ -z "$m" ] && echo probes-ok || echo "MISSING:$m -- fix before trusting anything below"

ssh-add -l                                     # 1 (shared prismo key)
ssh -T git@github.com                         # Hi <user>!
tmux -L smoke-test -f ~/.config/tmux/tmux.conf start-server \; display -p '#{prefix}' \; kill-server   # isolated socket: safe while sessions are live
nvim --version | head -1                      # 0.12.x
ls ~/.local/share/nvim/site/parser | wc -l    # 15
[ -d /run/user/1000 ] && echo linger-ok
opencode --version                            # from pacman, /usr/bin/opencode
chezmoi diff                                  # silent

# Verify treesitter lockfile:
want=$(jq -r '.plugins["nvim-treesitter"].rev' ~/.config/nvim/nvim-pack-lock.json)
got=$(git -C ~/.local/share/nvim/site/pack/core/opt/nvim-treesitter rev-parse HEAD)
[ "$want" = "$got" ] && echo "lockfile-ok $got" || echo "MISMATCH want=$want got=$got"

First commit

Commits sign automatically via the SSH agent (git log -1 --format='%G?'
should return G). Register the key with GitHub as a Signing Key too.

Update allowed_signers with your public key:

# Replace the Arch line in private_dot_ssh/allowed_signers.tmpl:
{{ .email }} ssh-ed25519 AAAA... ppalmer@prismo
# Use: cat ~/.ssh/prismo_ed25519.pub

Then commit and push.