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.
-
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.) - Network:
iwctl(wifi) or ethernet, thenping archlinux.org. 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.