Recently, while going through my routine system cleanup and “optimization”, I discovered my laptop was attempting to initialize a serial console on every boot. This was some feature I’d never configured and definitely didn’t need on a machine that spends its time rendering games, not serving headless in a datacenter!
Jun 01 23:30:17 zippyrus kernel: Kernel command line: ... console=ttyS0,115200 console=tty0 ...Jun 01 23:30:17 zippyrus kernel: printk: legacy console [ttyS0] enabledJun 01 23:30:21 zippyrus systemd-tty-ask-password-agent[315]: Starting password query on /dev/ttyS0.Jun 01 23:30:35 zippyrus systemd[1]: Expecting device /dev/ttyS0...Jun 01 23:30:35 zippyrus systemd[1]: Condition check resulted in /dev/ttyS0 being skipped.Jun 01 23:30:39 zippyrus systemd[1]: Started Serial Getty on ttyS0.Jun 01 23:30:43 zippyrus agetty[1646]: /dev/ttyS0: not a ttyJun 01 23:30:53 zippyrus systemd[1]: serial-getty@ttyS0.service: Main process exited, code=exited, status=1/FAILURE
My laptop, a device that had never seen a server rack, was desperately trying to start a serial console on a non-existent /dev/ttyS0
device. The kernel command line showed: console=ttyS0,115200 console=tty0
. Now for those unaware, /dev/ttyS0
points to a serial console, allowing you to use a serial port to connect to the system when there’s no output display or input available. And the 115200
defines the baud rate, which is not a concern for this post. Sigh, I digress.
Sure enough, my laptop does not need a serial console. The journalctl
logs were constantly being spammed with the error and I suspected that this also somehow affected my system’s battery life. This wasn’t breaking anything critical, but it bothered me deeply. If I didn’t put serial console parameters in my laptop config, where the hell were they coming from?
My first instinct was to grep through my entire configuration:
rg -i "console=\|ttyS0" . --glob="*.nix"
Nothing. Nada. Zilch. The serial console parameters weren’t hiding in any of my NixOS modules, hardware configurations, or system definitions. Out of curiosity, I checked all my other systems and found the same parameters everywhere: desktops, laptops, servers, all had these mysterious console=ttyS0,115200 console=tty0
entries. This confirmed my suspicion that something in my flake was adding them globally.
With most distros, you might hack a workaround and move on, never really knowing why. But NixOS is supposed to be explicit and traceable, and offers something exactly for scenarios like these.
How, you ask? The NixOS module system maintains provenance about every configuration option. You don’t have to guess where settings come from. Instead, you can interrogate the system directly and get definitive answers about what each input contributes to your final system state.
This is where nix repl
nix.dev - nix repl
comes handy. This interactive environment creates a live evaluation space where you can explore your fully evaluated NixOS configuration. And no, that’s not just the raw source code, but the actual evaluated result of all your modules, imports, and dependencies. It’s like having X-ray vision into your system’s configuration decisions. Let’s fire it up and see what we can find:
$ nix repl
# First, let's load the flake from the configuration pathflake = builtins.getFlake "/etc/nixos"
# Let's see what's actually in those kernel parametersflake.outputs.nixosConfigurations.zippyrus.config.boot.kernelParams[ "console=ttyS0,115200" "console=tty0" "nvidia-drm.modeset=1" "nmi_watchdog=0" "root=fstab" "loglevel=0" "lsm=landlock,yama,bpf"]
Sure enough, there were my mystery parameters, sitting right alongside the legitimate ones I’d explicitly configured. But even then, where did these kernelParams
come from?
Turns out, every evaluated NixOS option maintains provenance about its source, accessible through definitionsWithLocations
NixOS/nixpkgs - GitHub . This is exactly what we need here!
# Time to trace the sourcedefs = flake.outputs.nixosConfigurations.zippyrus.options.boot.kernelParams.definitionsWithLocations
# Let's examine the first definitionbuiltins.elemAt defs 0{ file = "/nix/store/kia92cyi6iqjq284xdk4wln1fv1jhb7m-source/nixos/common/serial.nix"; value = [ "console=ttyS0,115200" "console=tty0" ];}
Voilà! I had my answer. Well, somewhat. It was coming from srvos
nix-community/srvos - GitHub , a fantastic flake profile intended for servers, though the module does not belong here.
Now, the nix store path itself doesn’t immediately scream “this is from srvos,” but the path structure nixos/common/serial.nix
suggests this isn’t from nixpkgs core or hardware-specific modules. Yes, an alternative, easier way to figure out would have been to check the contents of that specific directory in the nix store.
Looking back at my flake configuration, there it was:
systems.modules.nixos = with inputs; [ srvos.nixosModules.common # <-- The well-intentioned culprit # ... other modules];
Here, the srvos.nixosModules.common
was being applied to all my systems since the srvos
documentation indicates it is also suitable for desktops, to which, I now disagree.
The fix was straightforward once I understood the source:
systems.modules.nixos = with inputs; [ srvos.nixosModules.common # ... other modules];
This eliminated the journalctl
errors on my system. Phew!
This debugging experience highlights something powerful about Nix. Rather than leave you guessing through layers of shell scripts and dusty forum posts, you get a direct answer to “where did this come from, and why?”. Next time you see weird config, don’t just grep blindly; fire up nix repl
and let your system tell you its secrets.