Udev script always fails with exit code 1

Even if the script is completely empty or is just exit 0, the output of journalctl -ze | grep udev is always “Process ‘script’ failed with exit code 1.”

The udev rule as written in /nix/store/...-udev-rules/99-local.rules is
ACTION=="change", KERNEL=="card[0-9]*", SUBSYSTEM=="drm", RUN+="script"

# Edit this configuration file to define what should be installed on
# your system.  Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).

{ config, pkgs, ... }:

  imports =
    [ # Include the results of the hardware scan.

  # Bootloader.
  boot.loader.grub.enable = true;
  boot.loader.grub.device = "/dev/vda";
  boot.loader.grub.useOSProber = true;

  networking.hostName = "nixos"; # Define your hostname.
  # networking.wireless.enable = true;  # Enables wireless support via wpa_supplicant.

  # Configure network proxy if necessary
  # networking.proxy.default = "http://user:password@proxy:port/";
  # networking.proxy.noProxy = ",localhost,internal.domain";

  # Enable networking
  networking.networkmanager.enable = true;

  # Set your time zone.
  time.timeZone = "America/Los_Angeles";

  # Select internationalisation properties.
  i18n.defaultLocale = "en_US.UTF-8";

  i18n.extraLocaleSettings = {
    LC_ADDRESS = "en_US.UTF-8";
    LC_MONETARY = "en_US.UTF-8";
    LC_NAME = "en_US.UTF-8";
    LC_NUMERIC = "en_US.UTF-8";
    LC_PAPER = "en_US.UTF-8";
    LC_TELEPHONE = "en_US.UTF-8";
    LC_TIME = "en_US.UTF-8";

  # Configure keymap in X11
  services.xserver = {
    enable = true;
    layout = "us";
    xkbVariant = "";

    libinput.enable = true;
    videoDrivers = [ "qxl" ];
    displayManager.session = [ 
        manage = "desktop";
        name = "kitty";
        start = ''
          /run/current-system/sw/bin/kitty --title kitty &
    displayManager.autoLogin.enable = true;
    displayManager.autoLogin.user = "flat";
    displayManager.defaultSession = "kitty";

 services.spice-vdagentd.enable = true;

  # Define a user account. Don't forget to set a password with ‘passwd’.
  users.users.user = {
    isNormalUser = true;
    description = "user";
    extraGroups = [ "networkmanager" "wheel" ];
    packages = with pkgs; [];

  # Enable automatic login for the user.
  services.getty.autologinUser = "user";

  # Allow unfree packages
  nixpkgs.config.allowUnfree = true;

  # List packages installed in system profile. To search, run:
  # $ nix search wget
  environment.systemPackages = with pkgs; [
  #  vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
  #  wget
    (pkgs.writeScriptBin "script" ''
      date >> /var/log/script.log
      xrandr --output Virtual-1 --auto
      xdotool search --class "kitty" windowsize $(xrandr | awk '/current/ {gsub(/,/, ""); print $8, $10}')

  # Some programs need SUID wrappers, can be configured further or are
  # started in user sessions.
  # programs.mtr.enable = true;
  # programs.gnupg.agent = {
  #   enable = true;
  #   enableSSHSupport = true;
  # };

  # List services that you want to enable:

  # Enable the OpenSSH daemon.
  # services.openssh.enable = true;

  # Open ports in the firewall.
  networking.firewall.allowedTCPPorts = [ 30100 ];
  networking.firewall.allowedUDPPorts = [ 30100 ];
  # Or disable the firewall altogether.
  # networking.firewall.enable = false;

  # This value determines the NixOS release from which the default
  # settings for stateful data, like file locations and database versions
  # on your system were taken. It‘s perfectly fine and recommended to leave
  # this value at the release version of the first install of this system.
  # Before changing this value read the documentation for this option
  # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
  system.stateVersion = "23.11"; # Did you read the comment?

  # Custom
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
  boot.readOnlyNixStore = false;
  services.udev.path = [ "/run/current-system/sw/bin" ];
#  services.udev.extraRules = ''ACTION=="change",	KERNEL=="card[0-9]*",	SUBSYSTEM=="drm",	RUN+="/run/current-system/sw/bin/script"'';
#  services.udev.extraRules = "ACTION==change, KERNEL==card[0-9]*, SUBSYSTEM==drm, RUN+=script";
  services.udev.extraRules = "ACTION=='change', KERNEL=='card[0-9]*', SUBSYSTEM=='drm', RUN+='script'";
#  services.udev.extraRules =
#    ''
#      ACTION=="change", KERNEL=="card[0-9]*", SUBSYSTEM=="drm", RUN+="/run/current-system/sw/bin/script"
#    '';

The system is running 23.11.3326.d2003f2223cb (Tapir) inside Proxmox VE 8.1.3.

Sidenote: I can only build with services.udev.extraRules without failure by using the syntax services.udev.extraRules = "ACTION=='change', KERNEL=='card[0-9]*', SUBSYSTEM=='drm', RUN+='script'";. However, the single-quote strings is not valid syntax for udev. I would also appreciate help with this issue. As of this moment I am manually editing the store for the udev-rules and reloading.


Hi @ThatGuy,

  1. In my experience, writing and debugging udev rules is not something I would call fun. Sorry.

  2. Whenever I have previously tried using RUN="command" in udev rules, I have had a really bad time.

  3. I have no idea whether single quotes are valid udev rules syntax – probably not – but in any case they are ugly.

  4. Within a double-quoted nix string, you can escape double quotes (") with backslashes (\").

  5. For any strings which might have “interesting” contents, I find that the double-single-quote ('') syntax works best. This is the multi-line string syntax. Then you don’t need to escape double quotes. Your udev rules will thank you for it.

  6. In double-single-quote strings, the only thing which you would sometimes need to quote is variable interpolation (${variable}). Do this by preceding it with a double-single-quote like this: ''${variable}. Strange but true.

  7. Back to your udev script, logging is your friend, because pretty much everything than can go wrong will go wrong, and give you exit code 1. For example, the PATH variable is probably not what you think. It’s missing /run/wrappers/bin for example.

  8. For sake of sanity, don’t bother with services.udev.path. Use absolute paths like RUN+="${my-script}/bin/my-script". Use:

    { lib, config, pkgs, ... }: let
      my-script = pkgs.writeScriptBin "my-script" ''
        #! ${pkgs.runtimeShell}
        # NB. Use ${pkgs.runtimeShell} instead of /bin/bash,
        # because the latter does not always exist.
    in {
      environment.systemPackages = [
      services.udev.extraRules = ''
        ACTION="change", SUBSYSTEM="usb", RUN+="${my-script}/bin/my-script"
      # etc.
  9. But the output from commands run by udev is swallowed by udev and never shown anywhere. So you can’t just log with echo. I used this method to redirect bash script output to the systemd-journal:

    # redirect outputs to journal, using the script's filename as the log identifier
    name=$(basename "$0")
    exec > >(systemd-cat -t "$name" -p info ) 2> >(systemd-cat -t "$name" -p err )
  10. If what you are doing is manually editing the /nix/store path corresponding to /etc/udev/rules.d, then don’t do that. You can put rules for debugging into the volatile directory /run/udev/rules.d.

  11. I’m not sure what you’re trying to achieve with the script – probably doing some action when a monitor is plugged or something? Anyway, in my experience, the best way to run these scripts which respond to hardware events is to have udev trigger starting/reloading a systemd service with the SYSTEMD_WANTS= property. I refer you to the systemd.device(5) manual page. It’s not easy this way, because you really need to get down and dirty with systemd, but it’s far more sane and reliable than RUN=.

  12. If you don’t like that your rules appear in 99-local.rules, then you can choose a filename like this:

    { lib, config, pkgs, ... }: {
      services.udev.packages = [(pkgs.writeTextFile {
        name = "my-awesome-udev";
        destination = "/etc/udev/rules.d/42-my-awesome.rules";
        text = ''
          ACTION=="add|change", KERNEL=="card[0-9]*", SUBSYSTEM=="drm", TAG+="systemd", SYSTEMD_WANTS+="my-awesome@"
  13. services.autorandr.enable might work for you.

Great write-up @rvl.

If OP chooses to take away a single item from that list, I will highly recommend that be number 11. It also happens to be the official udev upstream recommendation.

EDIT: I had a look at the script you’re trying to run and that’s going to be causing problems for a variety of reasons.

  1. you are expecting that a whole bunch of binaries are present in the udev execution environment so you absolutely should be using resholve.writeScriptBin instead to ensure everything is there.
  2. you need to ensure that the required DISPLAY and XAUTHORITY variables are set correctly for xrandr and xdotool to be able to do their thing. As a consequence the script will probably not work as expected with multiple users logged in.
  3. the script will fail in a wayland environment

If I were you, I would instead see if there was a different way to solve your problem.

1 Like