Raspberry Pi Imager - How does it work?
Update: I used the info from this article to automate trixie installs, see: Command Line Only Install For Headless Raspberry Pi OS (Trixie).
Contents
Background
The Raspberry Pi Imager is a great tool that has helped make Raspberry Pi more accessible to a wider range of users.
But there are a few things that make it less appealing for me. In the past, it annoyed me that I had to run it over X Windows, and that it didn't save my configuration, but recently I learned it can be run from the cli, see here. Still, to use it, I have to install a 3rd party application on my machine and run it with root permissions. Also, it's not well documented, so I wasn't really sure what it was adding to my images.
This is a bit frustrating for me because we run a lot of headless raspis on the geekfarm. We have an automated process for burning an image and configuring the OS. We use what we believed to be the "standard" mechanisms, e.g. mount the bootfs filesystem and touch /mnt/bootfs/ssh to enable ssh. But the process for initializing a headless raspi running Raspi OS has changed a number of times over the years, and in my experience, the new processes don't tend to be documented.
With Bookworm, the old process for configuring wifi stopped working. Here's the wording from the docs:

There's no alternate solution here--the only documented solution for this was just gone. This is especially problematic for headless installs. If wifi isn't configured on first boot, then it's a bunch of extra pain to bring a keyboard and monitor to my raspi, or bring my raspi to a keyboard and monitor. Ideally I never want to have to do either of these things.
Clearly, somehow the Raspberry Pi Imager has solved these problems.
Initially, I assumed it was using the same mechanisms I was using, e.g. that there would be some step where it mounted the bootfs filesystem and touched /bootfs/ssh to enable ssh. But when I mounted an image created by the imager, I didn't see that file on the filesystem.
So, I decided to dig a little deeper.
Starting with the Source
To understand how the Raspberry Pi Imager solves these problems, I spent a little time looking at the source code. If I could just find how it was configuring users, wifi, and other resources, then my questions should be answered.
But I'm not a C++ developer. There appeared to be tens of thousands of lines of code in the project. From what I could tell, the code didn't seem to be well documented or structured in a way that made it obvious what was happening at the high level. I just wasn't sure how much behavior might be nestled in all that code.
I saw some references to firstrun.sh. I'm somewhat familiar with systemd, but I didn't recall ever hearing about that file. How does that get executed? I did some searching around for information about firstrun.sh but I wasn't finding much and it wasn't answering my questions.
So, I decided to resort to a bit of simple reverse engineering.
Diff the Images
To figure out exactly what the app is doing, I built two images--one by simply burning the Trixie image file using dd, and one using the Raspberry Pi Imager. Then I mounted the filesystems that got created and copied them to my workstation.
# created image using rpi-imager from 2025-10-01-raspios-trixie
cd ~/projects/rpi-imager
rsync -av --progress --stats /media/wu/bootfs/ imager-image/bootfs/
rsync -av --progress --stats /media/wu/rootfs/ imager-image/rootfs/
# created image from dd alone
cd ~/projects/rpi/images
sudo dd if=2025-10-01-raspios-trixie-arm64-lite.img of=/dev/sda status=progress bs=4M
cd ~/projects/rpi-imager
rsync -av --progress --stats /media/wu/bootfs/ dd-image/bootfs/
rsync -av --progress --stats /media/wu/rootfs/ dd-image/rootfs/
Then I diffed them.
# look for diffs on the bootfs volume
diff -r --brief dd-image/bootfs/ imager-image/bootfs/
Files dd-image/bootfs/cmdline.txt and imager-image/bootfs/cmdline.txt differ
Only in imager-image/bootfs/: firstrun.sh
# look for diffs on the rootfs volume
diff -r --brief dd-image/rootfs/ imager-image/rootfs/
# no diffs
This is not what I was expecting. As I had noticed previously, there's no /bootfs/ssh or /bootfs/userconf.txt. There's just this firstrun.sh file and a change to the cmdline.txt file. What's that about?
cmdline.txt
So, let's take a look at that cmdline.txt file changes:
diff dd-image/bootfs/cmdline.txt imager-image/bootfs/cmdline.txt
1c1
< console=serial0,115200 console=tty1 root=PARTUUID=7351b90c-02 rootfstype=ext4 fsck.repair=yes rootwait resize
---
> console=serial0,115200 console=tty1 root=PARTUUID=7351b90c-02 rootfstype=ext4 fsck.repair=yes rootwait resize cfg80211.ieee80211_regdom=US systemd.run=/boot/firstrun.sh systemd.run_success_action=reboot systemd.unit=kernel-command-line.target
The changes boil down to these four options:
cfg80211.ieee80211_regdom=USsystemd.run=/boot/firstrun.shsystemd.run_success_action=rebootsystemd.unit=kernel-command-line.target
I hadn't seen these options before, but two things seem pretty obvious here– it's going to run /boot/firstrun.sh when booting up, and if that script exits with a successful exit status, the system will automatically reboot. As I look at this, I'm asking myself why it doesn't get into an infinite loop running firstrun.sh and then rebooting.
As for the other options, 'systemd.unit' appears to be related to setting systemd.run to a shell script, and cfg80211.ieee80211_regdom relates to setting the default regulatory domain for wifi.
firstrun.sh
Ok, then, so what's in this magical firstrun.sh file? Here are the contents of mine, with a few sensitive bits removed.
#!/bin/bash
set +e
CURRENT_HOSTNAME=`cat /etc/hostname | tr -d " \t\n\r"`
if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then
/usr/lib/raspberrypi-sys-mods/imager_custom set_hostname MY-HOSTNAME-HERE
else
echo test >/etc/hostname
sed -i "s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\tMY-HOSTNAME-HERE/g" /etc/hosts
fi
FIRSTUSER=`getent passwd 1000 | cut -d: -f1`
FIRSTUSERHOME=`getent passwd 1000 | cut -d: -f6`
if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then
/usr/lib/raspberrypi-sys-mods/imager_custom enable_ssh -k 'MY-SSH-KEY-HERE'
else
install -o "$FIRSTUSER" -m 700 -d "$FIRSTUSERHOME/.ssh"
install -o "$FIRSTUSER" -m 600 <(printf "'MY-SSH-KEY-HERE'\n") "$FIRSTUSERHOME
/.ssh/authorized_keys"
echo 'PasswordAuthentication no' >>/etc/ssh/sshd_config
systemctl enable ssh
fi
if [ -f /usr/lib/userconf-pi/userconf ]; then
/usr/lib/userconf-pi/userconf 'MY-USER-HERE' 'MY-ENC-PW-HERE'
else
echo "$FIRSTUSER:"'MY-ENC-PW-HERE' | chpasswd -e
if [ "$FIRSTUSER" != "MY-USER-HERE" ]; then
usermod -l "MY-USER-HERE" "$FIRSTUSER"
usermod -m -d "/home/MY-USER-HERE" "MY-USER-HERE"
groupmod -n "MY-USER-HERE" "$FIRSTUSER"
if grep -q "^autologin-user=" /etc/lightdm/lightdm.conf ; then
sed /etc/lightdm/lightdm.conf -i -e "s/^autologin-user=.*/autologin-user=MY-USER-HERE/"
fi
if [ -f /etc/systemd/system/getty@tty1.service.d/autologin.conf ]; then
sed /etc/systemd/system/getty@tty1.service.d/autologin.conf -i -e "s/$FIRSTUSER/MY-USER-HERE/"
fi
if [ -f /etc/sudoers.d/010_pi-nopasswd ]; then
sed -i "s/^$FIRSTUSER /MY-USER-HERE /" /etc/sudoers.d/010_pi-nopasswd
fi
fi
fi
if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then
/usr/lib/raspberrypi-sys-mods/imager_custom set_wlan 'swordfishhd' '8d9f7baa276bd90392efafe637c502b9b13cf9a74675b867579e1296d73e16cd' 'US'
else
cat >/etc/wpa_supplicant/wpa_supplicant.conf <<'WPAEOF'
country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
ap_scan=1
update_config=1
network={
ssid="MY-WIFI-SSID-HERE"
psk=MY-WIFI-ENC-PW-HERE
}
WPAEOF
chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf
rfkill unblock wifi
for filename in /var/lib/systemd/rfkill/*:wlan ; do
echo 0 > $filename
done
fi
if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then
/usr/lib/raspberrypi-sys-mods/imager_custom set_keymap 'us'
/usr/lib/raspberrypi-sys-mods/imager_custom set_timezone 'America/Los_Angeles'
else
rm -f /etc/localtime
echo "America/Los_Angeles" >/etc/timezone
dpkg-reconfigure -f noninteractive tzdata
cat >/etc/default/keyboard <<'KBEOF'
XKBMODEL="pc105"
XKBLAYOUT="us"
XKBVARIANT=""
XKBOPTIONS=""
KBEOF
dpkg-reconfigure -f noninteractive keyboard-configuration
fi
rm -f /boot/firstrun.sh
sed -i 's| systemd.run.*||g' /boot/cmdline.txt
exit 0
Here's a high-level summary of what's happening.
- set the hostname - if
/usr/lib/raspberrypi-sys-mods/imager_customexists, use it to set the hostname, else update/etc/hostnameand/etc/hosts - enable ssh - if imager_custom exists, use it to enable ssh, else enable ssh using systemctl, add the user's ssh key to their
authorized_keysfile, and update thesshd_configfile to disable password authentication. - add the initial user - if imager_custom exists, use it to add the first user, else add the user using usermod and groupmod. Also update the autologin user in
lightdm.confandautologin.conf, and then update the config so sudo will not require a password for the user. - configure wifi - if imager_custom exists, use it to set up the wlan configuration. If not, then create the
wpa_supplicant.conffile. - configure the keyboard - if imager_custom exists, use it to set the keyboard keymap to
us. If not, then create/etc/default/keyboardto configure XKBMODEL and XKBLAYOUT, and then calldpkg-reconfigure -f noninteractive keyboard-configuration - configure the timezone - if imager_custom exists, use it to set the timezone. If not, remove /etc/localtime, add the timezone to
/etc/timezone, and then calldpkg-reconfigure -f noninteractive tzdata - remove /boot/firstrun.sh - and remove the configuration from
/boot/cmdline.txtthat ran it
Conclusion
So, it turns out that what the Raspberry Pi Imager does is much simpler than what I've been doing.
It creates one file, firstrun.sh, where all the configuration happens on the initial boot of the raspi when all the filesystems are mounted and writable. So, it can simply run commands like usermod and groupmod to set up a user rather than using the documented method of adding the user to /mnt/bootfs/userconf.txt. Even better, it can just run rfkill unblock wifi after the system boots since there doesn't appear to be any configuration item to enable that before the system boots.
Then at the end, firstrun.sh deletes itself from the filesystem and from cmdline.txt. So, if you look at the filesystem after the first boot is done, you would never know that firstrun.sh ever existed. This explains my early confusion about why it doesn't get into an infinite loop running firstrun.sh and then rebooting.
While I wish there was a better mechanism to accomplish these things, admittedly part of the beauty of this approach is that it doesn't need to change every time the initialization mechanisms change--which has happened a lot over over the years.
I used this knowledge to automate my builds for Trixie, see: Command Line Only Install For Headless Raspberry Pi OS (Trixie).
cloud-init
According to the article below, Raspberry Pi OS is moving away from firstrun.sh to cloud-init. This is great news because cloud-init is a standard cross-platform and distribution-agnostic tool.