On Fedora, grub2-mkconfig does not build the list of kernels in the boot menu. Each kernel has its own Boot Loader Specification (BLS) file in /boot/loader/entries, GRUB reads those files at boot, and grubby is the tool that edits them. What grub2-mkconfig does do is less obvious and more dangerous: it rewrites the kernel arguments in every one of those files from /etc/default/grub, so an argument you added with grubby can disappear the next time anyone changes the menu timeout.
This guide follows each setting to the file that holds it: kernel arguments for existing and future kernels, the default kernel, the menu timeout, and old kernels that fill up /boot, plus the stale entry a cloned VM can leave behind. We tested both common layouts of /etc/default/grub, the Cloud image’s as shipped and the installer’s simulated by adding its GRUB_CMDLINE_LINUX line, because grubby behaves differently on each. Everything below was run on a Fedora 45 VM with UEFI and Secure Boot in October 2026, with reboots to confirm the kernel arguments and the default kernel.
Kernel entries live in BLS files
Every installed kernel gets one small text file, written when the kernel package installs and deleted when it is removed. Because each kernel has its own file, a new kernel appears in the boot menu without anyone regenerating grub.cfg. List them:
sudo ls /boot/loader/entries/
The lab VM had three kernels installed, so it had three entries. Installs that include the rescue image also carry a 0-rescue entry. The long hex prefix is the machine ID of the system that installed each kernel, which matters later in this guide:
102a5f76d1b2433a9276533006c7e792-7.2.11-300.fc45.x86_64.conf
47b1cc226ab5409b845db2649a222ef8-7.2.7-300.fc45.x86_64.conf
dddf189766fa47ffb7ea3f6b4780f0cf-7.2.0-61.fc45.x86_64.conf
Each file names the kernel image, the initramfs and the full kernel command line in its options line. That last line is what GRUB passes to the kernel, so it is the line every argument change ends up editing:
sudo cat /boot/loader/entries/102a5f76d1b2433a9276533006c7e792-7.2.11-300.fc45.x86_64.conf
The linux and initrd paths start with /boot because this VM keeps /boot as its own Btrfs subvolume next to the root subvolume on the same filesystem. On an install with a separate /boot partition the paths start at the root of that partition instead, so the lines read linux /vmlinuz-...:
title Fedora Linux (7.2.11-300.fc45.x86_64) 45 (Cloud Edition)
version 7.2.11-300.fc45.x86_64
linux /boot/vmlinuz-7.2.11-300.fc45.x86_64
initrd /boot/initramfs-7.2.11-300.fc45.x86_64.img
options root=UUID=d04d5cb8-9cf4-4b6f-8f86-aa556776eeb5 ro rootflags=subvol=root no_timer_check console=tty1 console=ttyS0,115200n8 systemd.firstboot=off rootflags=subvol=root
grub_users $grub_users
grub_arg --unrestricted
grub_class fedora
Now look at what grub.cfg holds. With GRUB_ENABLE_BLSCFG=true, which is Fedora’s default, the generated file loads the blscfg module and has no per-kernel menu entries at all. Count them yourself:
sudo grep -nE "blscfg" /boot/grub2/grub.cfg
sudo grep -c "menuentry " /boot/grub2/grub.cfg
The single menuentry left is the “UEFI Firmware Settings” item. Every kernel line in the boot menu comes from the blscfg command reading the entries directory at boot:
148:# The blscfg command parses the BootLoaderSpec files stored in /boot/loader/entries and
164:insmod blscfg
165:blscfg
1
So which file holds what? Four files are involved, and most broken boot menus come from changing one without knowing that another overwrites it:
/boot/loader/entries/*.conf: per-kernel entries, edited bygrubbyand rewritten bygrub2-mkconfig/etc/default/grub: menu settings and the argumentsgrub2-mkconfigwrites into every entry/etc/kernel/cmdline: arguments for kernels installed later/boot/grub2/grubenv: the saved default and menu flags, edited withgrubbyorgrub2-editenv
grubby finds entries by path, not by version
grubby numbers entries, and it’s tempting to use those numbers. Don’t: the index follows the file name, and the file name starts with a machine ID, not the kernel version. Print the index and kernel for every entry:
sudo grubby --info=ALL | grep -E "^(index|kernel)="
Index 0 is the oldest kernel here and the newest is index 1, because the files sort by their machine ID prefix. A script that assumes index 0 is the newest kernel would change the wrong entry on this machine:
index=0
kernel="/boot/boot/vmlinuz-7.2.0-61.fc45.x86_64"
index=1
kernel="/boot/boot/vmlinuz-7.2.11-300.fc45.x86_64"
index=2
kernel="/boot/boot/vmlinuz-7.2.7-300.fc45.x86_64"
Notice the doubled /boot/boot/ as well. When /boot is a mount point, grubby prints the mount point in front of the entry’s own /boot path. It still accepts the normal /boot/vmlinuz-... path as input, which is what every command below uses. A path keeps pointing at the right kernel after an install or removal renumbers everything, which is why it beats the index forms such as --set-default-index.
grubby adds arguments to every entry
grubby --update-kernel=ALL writes an argument into every entry and into /etc/kernel/cmdline, the template for kernels installed later. Add sysrq_always_enabled=1 as an example, then check the entries, the template and /etc/default/grub:
sudo grubby --update-kernel=ALL --args="sysrq_always_enabled=1"
sudo grubby --info=ALL | grep -E "^(index|args)="
cat /etc/kernel/cmdline
grep CMDLINE /etc/default/grub
The argument landed in all three entries and in /etc/kernel/cmdline. /etc/default/grub kept its old line, and the next section explains why that matters on this layout:
index=0
args="no_timer_check console=tty1 console=ttyS0,115200n8 systemd.firstboot=off rootflags=subvol=root sysrq_always_enabled=1"
index=1
args="ro rootflags=subvol=root no_timer_check console=tty1 console=ttyS0,115200n8 systemd.firstboot=off rootflags=subvol=root sysrq_always_enabled=1"
index=2
args="ro rootflags=subvol=root no_timer_check console=tty1 console=ttyS0,115200n8 systemd.firstboot=off rootflags=subvol=root sysrq_always_enabled=1"
root=UUID=d04d5cb8-9cf4-4b6f-8f86-aa556776eeb5 ro rootflags=subvol=root no_timer_check console=tty1 console=ttyS0,115200n8 systemd.firstboot=off rootflags=subvol=root sysrq_always_enabled=1
GRUB_CMDLINE_LINUX_DEFAULT="no_timer_check console=tty1 console=ttyS0,115200n8 systemd.firstboot=off rootflags=subvol=root"
Only a reboot proves the argument reached the kernel, because GRUB reads the entries at boot and nowhere else. After one, check the command line the kernel received and the setting the argument controls:
cat /proc/cmdline
cat /proc/sys/kernel/sysrq
BOOT_IMAGE shows which kernel booted, and the sysrq value of 1 confirms the argument took effect. Seeing it in /proc/cmdline but not in its setting would mean the kernel ignored it, so check both:
BOOT_IMAGE=(hd0,gpt3)/boot/vmlinuz-7.2.11-300.fc45.x86_64 root=UUID=d04d5cb8-9cf4-4b6f-8f86-aa556776eeb5 ro rootflags=subvol=root no_timer_check console=tty1 console=ttyS0,115200n8 systemd.firstboot=off rootflags=subvol=root sysrq_always_enabled=1
1
Seen together on the Cloud image layout, the change reached /etc/kernel/cmdline and the running kernel, and left /etc/default/grub untouched:

--update-kernel decides how far a change reaches, and picking the target is the whole decision:
ALL: every entry,/etc/kernel/cmdline, andGRUB_CMDLINE_LINUXif that line exists and is not empty/boot/vmlinuz-VERSION: one kernel only, for testing an argument on a single boot targetDEFAULT: the current default entry only, which moves with the next kernel install
grub2-mkconfig rewrites every entry’s arguments
Fedora’s grub2-mkconfig does more than write grub.cfg. By default it also rebuilds the options line of every BLS entry from GRUB_CMDLINE_LINUX and GRUB_CMDLINE_LINUX_DEFAULT, and refreshes /etc/kernel/cmdline from /etc/default/grub when that file is newer. Kernel installs run the same sync whenever /etc/kernel/cmdline is older than /etc/default/grub. So whether a grubby argument survives depends on one question: did grubby also write it into /etc/default/grub?
On the Cloud image it didn’t, because that file has no GRUB_CMDLINE_LINUX line at all, only GRUB_CMDLINE_LINUX_DEFAULT. We added a test argument with grubby, changed the menu timeout the normal way, and counted what was left:
sudo grubby --update-kernel=ALL --args="cfgtest=1"
echo "entries with cfgtest: $(sudo grubby --info=ALL | grep -c cfgtest)"
echo "kernel/cmdline: $(grep -c cfgtest /etc/kernel/cmdline)"
sudo sed -i "s/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=6/" /etc/default/grub
sudo grub2-mkconfig -o /boot/grub2/grub.cfg >/dev/null 2>&1
echo "after mkconfig, entries with cfgtest: $(sudo grubby --info=ALL | grep -c cfgtest)"
echo "after mkconfig, kernel/cmdline: $(grep -c cfgtest /etc/kernel/cmdline)"
We ran this test after the stale-entry cleanup described later, so two entries were left. The argument was in both of them and in the template, and one grub2-mkconfig later it was gone from all of them:
entries with cfgtest: 2
kernel/cmdline: 1
after mkconfig, entries with cfgtest: 0
after mkconfig, kernel/cmdline: 0
Installer-built systems are different, because their /etc/default/grub has a GRUB_CMDLINE_LINUX line (Workstation’s holds rhgb quiet). We added the same line to the lab VM, regenerated once, and repeated the test:
sudo sed -i "/^GRUB_CMDLINE_LINUX_DEFAULT=/i GRUB_CMDLINE_LINUX=\"rhgb quiet\"" /etc/default/grub
sudo grub2-mkconfig -o /boot/grub2/grub.cfg >/dev/null 2>&1
sudo grubby --update-kernel=ALL --args="cfgtest=1"
grep -E "^GRUB_CMDLINE" /etc/default/grub
This time grubby wrote the argument into /etc/default/grub as well:
GRUB_CMDLINE_LINUX="rhgb quiet cfgtest=1"
GRUB_CMDLINE_LINUX_DEFAULT="no_timer_check console=tty1 console=ttyS0,115200n8 systemd.firstboot=off rootflags=subvol=root"
After another timeout change and grub2-mkconfig, we counted again with the same two echo checks as before, printed on one line. The argument was still in both entries and in /etc/kernel/cmdline, because the file mkconfig rebuilds from now contained it:
after mkconfig entries: 2 kernel/cmdline: 1
That gives a simple rule. Check the file first, because the right command depends on what it holds:
grep -E "^GRUB_CMDLINE_LINUX=" /etc/default/grub
- If the line exists and has a value,
grubby --update-kernel=ALL --args=...is enough, because grubby keeps it in sync - If it is missing, as on Cloud images, or empty (
GRUB_CMDLINE_LINUX=""), which grubby leaves alone, add aGRUB_CMDLINE_LINUX="..."line with your argument to/etc/default/grub(or put the argument inside the existing empty one), then rungrub2-mkconfig -o /boot/grub2/grub.cfgto push it into every entry - Either way, confirm with
grubby --info=ALLand, after a reboot,/proc/cmdline
Removing an argument follows the same rule. On the installer layout, --remove-args with ALL took it out of the entries, /etc/kernel/cmdline and GRUB_CMDLINE_LINUX in one step:
sudo grubby --update-kernel=ALL --remove-args="cfgtest=1"
grep -E "^GRUB_CMDLINE_LINUX=" /etc/default/grub
The line was back to what the installer wrote:
GRUB_CMDLINE_LINUX="rhgb quiet"
Clean up the sysrq_always_enabled=1 example from earlier with sudo grubby --update-kernel=ALL --remove-args="sysrq_always_enabled=1" if you added it, because magic SysRq left on is a small relaxation you probably don’t want. On the Cloud layout the grub2-mkconfig run above already removed it, which is the whole problem this section is about.
The default kernel is a saved entry
Fedora sets GRUB_DEFAULT=saved, so the default is whatever entry name sits in /boot/grub2/grubenv. A new kernel becomes the saved entry when it installs, because /etc/sysconfig/kernel ships with UPDATEDEFAULT=yes. To boot an older kernel by default, for example while a regression in the new one gets fixed, set it with grubby:
sudo grubby --set-default=/boot/vmlinuz-7.2.7-300.fc45.x86_64
sudo grub2-editenv list | grep saved_entry
grubby confirms the entry file it picked, and saved_entry now holds that file’s name. GRUB looks the name up at boot, so the setting keeps working even when the index numbers shift:
The default is /boot/loader/entries/102a5f76d1b2433a9276533006c7e792-7.2.7-300.fc45.x86_64.conf with index 2 and kernel /boot/boot/vmlinuz-7.2.7-300.fc45.x86_64
saved_entry=102a5f76d1b2433a9276533006c7e792-7.2.7-300.fc45.x86_64
After the reboot, uname -r returned 7.2.7-300.fc45.x86_64. The pin is temporary by design: the next kernel install moves the default forward again. If the old kernel must stay the default, set it again after each update or set UPDATEDEFAULT=no in /etc/sysconfig/kernel. For a one-time choice instead, pick the entry from the menu at boot, or see booting an older kernel when an update breaks Fedora for the recovery path. Switch back by path when you’re done:
sudo grubby --set-default=/boot/vmlinuz-7.2.11-300.fc45.x86_64
The same UPDATEDEFAULT rule applies when you reinstall an older kernel. We ran kernel-install add for the 7.2.7 kernel while 7.2.11 was the default, and saved_entry switched to 7.2.7 without being asked, so check grub2-editenv list after reinstalling or repairing any kernel.
grub2-mkconfig also builds the menu
Menu behaviour is the other half of /etc/default/grub. The lab VM came from a Cloud image with GRUB_TIMEOUT=0, which hides the menu entirely; raise it to five seconds and regenerate:
sudo sed -i "s/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=5/" /etc/default/grub
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
The warning about GRUB_SERIAL_COMMAND is harmless, because GRUB falls back to default serial parameters. It also points at a trap on Cloud images: they set GRUB_TERMINAL_OUTPUT="serial", so the menu shows only on the serial console. They set GRUB_TERMINAL_INPUT="serial" too, so change both lines to "console serial" and run grub2-mkconfig again if you want to see and drive the menu from a screen and keyboard:
Generating grub configuration file ...
Warning: Requested serial terminal but GRUB_SERIAL_COMMAND is unspecified. Default parameters will be used.
Adding boot menu entry for UEFI Firmware Settings ...
done
Write the output to /boot/grub2/grub.cfg on UEFI machines too. The file under /boot/efi/EFI/fedora/ is a small stub that only finds the real one:
sudo cat /boot/efi/EFI/fedora/grub.cfg
All four lines do is locate the filesystem that holds /boot/grub2 and load the real config from it:
search --no-floppy --root-dev-only --fs-uuid --set=dev d04d5cb8-9cf4-4b6f-8f86-aa556776eeb5
set prefix=($dev)/boot/grub2
export $prefix
configfile $prefix/grub.cfg
Older Fedora guides tell you to write to the EFI path, because older releases kept the full config there. The current tool refuses, which saves you from that advice:
sudo grub2-mkconfig -o /boot/efi/EFI/fedora/grub.cfg
It exits with status 1 and leaves the stub alone:
Running `grub2-mkconfig -o /boot/efi/EFI/fedora/grub.cfg' will overwrite the GRUB wrapper.
Please run `grub2-mkconfig -o /boot/grub2/grub.cfg' instead to update grub.cfg.
GRUB configuration file was not updated.
Workstation installs hide the menu a different way, with a menu_auto_hide flag in grubenv. With it set, the menu stays hidden after a successful boot and shows when the previous boot failed or when you press Esc, Shift or F8. To always show the menu, clear the flag:
sudo grub2-editenv - unset menu_auto_hide
To hide it again later, set it back:
sudo grub2-editenv - set menu_auto_hide=1
Neither change needs grub2-mkconfig, and sudo grub2-editenv list shows the flag next to the saved_entry from the previous section.
Old kernels leave on a schedule
You do not have to clean up kernels by hand on most systems. dnf keeps a fixed number of kernels and removes the oldest when a new one arrives. Check the limit and what one kernel costs on disk, because the two numbers together tell you whether /boot can ever fill up on your layout:
dnf5 --dump-main-config | grep -E "^installonly_limit"
ls -lh /boot/vmlinuz-7.2.11-300.fc45.x86_64 /boot/initramfs-7.2.11-300.fc45.x86_64.img
sudo du -sh /lib/modules/7.2.11-300.fc45.x86_64
Three kernels, and about 64 MB of each one sits in /boot. On a layout with a small separate /boot partition that is the number that matters, because the modules under /lib/modules live on the root filesystem, and a rescue image adds a large initramfs of its own:
installonly_limit = 3
-rw-------. 1 root root 45M Oct 20 11:17 /boot/initramfs-7.2.11-300.fc45.x86_64.img
-rwxr-xr-x. 1 root root 19M Oct 15 00:00 /boot/vmlinuz-7.2.11-300.fc45.x86_64
85M /lib/modules/7.2.11-300.fc45.x86_64
Before you remove anything, check these three things, because each one can leave you without a bootable fallback:
uname -r, so you know which kernel is runningrpm -q kernel-core, so you know which ones are installeddf -h /boot, so you know how much room you actually need
When dnf refuses a kernel update because /boot is full, remove old kernels before anything else. --oldinstallonly keeps the newest ones and the running kernel, and a lower limit for this one command frees an extra slot (dnf5 also accepts --limit=2 for the same thing):
sudo dnf remove --oldinstallonly --setopt=installonly_limit=2
On the lab VM that removed the 7.2.0 kernel and kept 7.2.7 and 7.2.11. Don’t go below two: the previous kernel is your fallback when a new one does not boot, and you don’t get it back without network access.
Cloned systems can keep stale entries
The kernel removal above deleted the kernel files but left its boot entry behind. What happened? Kernel removal only deletes the entry named with the current machine ID, and this entry was written before the VM got its own ID, when the kernel was installed into the image it was cloned from:
cat /etc/machine-id
ls /boot/vmlinuz-7.2.0-61.fc45.x86_64
sudo grubby --info=ALL | grep -E "^(index|kernel)="
The kernel image is gone, yet index 0 still points at it, so the boot menu offers an entry that can only fail. Nobody notices until someone picks it during a recovery, which is the worst moment to find a dead menu item:
102a5f76d1b2433a9276533006c7e792
ls: cannot access '/boot/vmlinuz-7.2.0-61.fc45.x86_64': No such file or directory
index=0
kernel="/boot/boot/vmlinuz-7.2.0-61.fc45.x86_64"
index=1
kernel="/boot/boot/vmlinuz-7.2.11-300.fc45.x86_64"
index=2
kernel="/boot/boot/vmlinuz-7.2.7-300.fc45.x86_64"
Any kernel installed before the machine ID changed is affected, whether it came with the image build or was added on a template before cloning; the lab VM had two such prefixes, dddf... for the image kernel and 47b1... for 7.2.7. During testing we reinstalled 7.2.7 with kernel-install add, which wrote a second entry under the current machine ID, and we deleted the old 47b1 file by hand, which is why the later outputs show 102a... for it; on an untouched clone, the 47b1-style entry stays until that kernel is removed, then needs the same cleanup. Remove a dead entry by kernel path, since grubby matches the file by name whatever its prefix:
sudo grubby --remove-kernel=/boot/vmlinuz-7.2.0-61.fc45.x86_64
sudo ls /boot/loader/entries/
Only entries for installed kernels remain, and the boot menu now matches what dnf thinks is installed:
102a5f76d1b2433a9276533006c7e792-7.2.11-300.fc45.x86_64.conf
102a5f76d1b2433a9276533006c7e792-7.2.7-300.fc45.x86_64.conf
A quick check on any cloned machine is to compare the entry prefixes with /etc/machine-id. Prefixes that do not match belong to kernels installed before the clone, so run grubby --info=ALL after dnf removes one of them and delete any entry whose kernel file is missing, because nothing else will clean it up.
Where each GRUB setting lives
Use this table to find the command for a change before you touch any file. The last two columns are where most fixes go wrong, because a change can look right today and vanish at the next regeneration or kernel update:
| Change | Stored in | Command | Survives grub2-mkconfig? | Survives a kernel update? |
|---|---|---|---|---|
| Kernel argument, installer layout | BLS entries, /etc/kernel/cmdline, GRUB_CMDLINE_LINUX | grubby --update-kernel=ALL --args=... | Yes | Yes |
| Kernel argument, Cloud image layout | GRUB_CMDLINE_LINUX line you add | Edit /etc/default/grub, then grub2-mkconfig | Yes | Yes |
| Argument for one kernel only | That kernel’s BLS entry | grubby --update-kernel=/boot/vmlinuz-... --args=... | No | No, by design |
| Default kernel | /boot/grub2/grubenv (saved_entry) | grubby --set-default=/boot/vmlinuz-... | Yes | No, a new kernel install takes over |
| Menu timeout | /etc/default/grub | grub2-mkconfig -o /boot/grub2/grub.cfg | Yes | Yes |
| Hidden menu | /boot/grub2/grubenv (menu_auto_hide) | grub2-editenv - set|unset menu_auto_hide | Yes | Yes |
| Stale entry for a removed kernel | /boot/loader/entries/ | grubby --remove-kernel=/boot/vmlinuz-... | Not applicable | Not applicable |
If a change you made does not show up after a reboot, read /proc/cmdline, grubby --info=ALL and the GRUB_CMDLINE lines in /etc/default/grub first. One of them always shows where the change never reached. The hibernation guide for Fedora explains why a UEFI machine doesn’t need a resume= argument at all, the Secure Boot guide covers the signed boot chain GRUB sits in, and the LUKS TPM2 guide for Fedora shows why boot changes matter when the TPM measures them.