diff --git a/krebs/3modules/setuid.nix b/krebs/3modules/setuid.nix
index 97cf21cdd..64fedb911 100644
--- a/krebs/3modules/setuid.nix
+++ b/krebs/3modules/setuid.nix
@@ -1,24 +1,20 @@
-{ config, pkgs, lib, ... }:
 with import <stockholm/lib>;
-let
-  cfg = config.krebs.setuid;
+{ config, pkgs, ... }: let
 
   out = {
     options.krebs.setuid = api;
-    config = mkIf (cfg != {}) imp;
+    config = mkIf (config.krebs.setuid != {}) imp;
   };
 
   api = mkOption {
     default = {};
     type = let
-      # TODO make wrapperDir configurable
-      inherit (config.security) wrapperDir;
       inherit (config.users) groups users;
-    in types.attrsOf (types.submodule ({ config, ... }: {
+    in types.attrsOf (types.submodule (self: let cfg = self.config; in {
       options = {
         name = mkOption {
           type = types.filename;
-          default = config._module.args.name;
+          default = cfg._module.args.name;
         };
         envp = mkOption {
           type = types.nullOr (types.attrsOf types.str);
@@ -51,6 +47,10 @@ let
             merge = mergeOneOption;
           };
         };
+        wrapperDir = mkOption {
+          default = config.security.wrapperDir;
+          type = types.absolute-pathname;
+        };
         activate = mkOption {
           type = types.str;
           visible = false;
@@ -58,21 +58,22 @@ let
         };
       };
       config.activate = let
-        src = pkgs.exec config.name {
-          inherit (config) envp filename;
+        src = pkgs.exec cfg.name {
+          inherit (cfg) envp filename;
         };
-        dst = "${wrapperDir}/${config.name}";
+        dst = "${cfg.wrapperDir}/${cfg.name}";
       in ''
+        mkdir -p ${cfg.wrapperDir}
         cp ${src} ${dst}
-        chown ${config.owner}.${config.group} ${dst}
-        chmod ${config.mode} ${dst}
+        chown ${cfg.owner}.${cfg.group} ${dst}
+        chmod ${cfg.mode} ${dst}
       '';
     }));
   };
 
   imp = {
     system.activationScripts."krebs.setuid" = stringAfter [ "wrappers" ]
-      (concatMapStringsSep "\n" (getAttr "activate") (attrValues cfg));
+      (concatMapStringsSep "\n" (getAttr "activate") (attrValues config.krebs.setuid));
   };
 
 in out
diff --git a/krebs/3modules/shadow.nix b/krebs/3modules/shadow.nix
index cff66492d..9505efb0c 100644
--- a/krebs/3modules/shadow.nix
+++ b/krebs/3modules/shadow.nix
@@ -4,19 +4,21 @@ with import <stockholm/lib>;
   cfg = config.krebs.shadow;
 
   mergeShadowsJq = pkgs.writeJq "merge-shadows.jq" ''
-    def fields_3_to_9: ["1", "", "", "", "", "", ""];
+    def is_int: . == (. | floor);
+    def fields_4_to_9: ["", "", "", "", "", ""];
+    def check_fields_3_to_9: (.[2] | tonumber | is_int) and .[3:] == fields_4_to_9;
 
     def read_value:
       split(":") |
       if length == 9 then
-        if .[2:] == fields_3_to_9 then
+        if check_fields_3_to_9 then
           .
         else
           error("unrecognized field contents")
         end
       elif length == 2 then
         if .[1] | test("^\\$6\\$") then
-          . + fields_3_to_9
+          . + ["1"] + fields_4_to_9
         else
           error("unrecognized hashed password")
         end
diff --git a/tv/5pkgs/haskell/xmonad-tv/src/Paths.hs b/tv/5pkgs/haskell/xmonad-tv/src/Paths.hs
index 6b7235530..b2ad01ae7 100644
--- a/tv/5pkgs/haskell/xmonad-tv/src/Paths.hs
+++ b/tv/5pkgs/haskell/xmonad-tv/src/Paths.hs
@@ -29,3 +29,6 @@ urxvtc = findExecutable "urxvtc"
 
 xcalib :: FilePath
 xcalib = findExecutable "xcalib"
+
+xdpychvt :: FilePath
+xdpychvt = findExecutable "xdpychvt"
diff --git a/tv/5pkgs/haskell/xmonad-tv/src/main.hs b/tv/5pkgs/haskell/xmonad-tv/src/main.hs
index 50b03d81c..e5a4473fe 100644
--- a/tv/5pkgs/haskell/xmonad-tv/src/main.hs
+++ b/tv/5pkgs/haskell/xmonad-tv/src/main.hs
@@ -190,6 +190,9 @@ myKeys conf = Map.fromList $
     , ((_4, xK_Prior), forkFile Paths.xcalib ["-invert", "-alter"] Nothing)
 
     , ((0, xK_Print), forkFile Paths.flameshot [] Nothing)
+
+    , ((_C, xF86XK_Forward), forkFile Paths.xdpychvt ["next"] Nothing)
+    , ((_C, xF86XK_Back), forkFile Paths.xdpychvt ["prev"] Nothing)
     ]
     where
     _4 = mod4Mask
diff --git a/tv/5pkgs/simple/xdpytools/default.nix b/tv/5pkgs/simple/xdpytools/default.nix
new file mode 100644
index 000000000..7d1ee071e
--- /dev/null
+++ b/tv/5pkgs/simple/xdpytools/default.nix
@@ -0,0 +1,31 @@
+{ lib, pkgs }:
+
+let
+  install = name: { path }: /* sh */ ''
+    (
+      mkdir -p $out/bin
+      touch $out/bin/${name}
+      chmod +x $out/bin/${name}
+      exec >$out/bin/${name}
+
+      echo '#! ${pkgs.dash}/bin/dash'
+      echo export PATH=${lib.makeBinPath path}
+      sed 1d ${./src + "/${name}"}
+    )
+  '';
+in
+
+pkgs.runCommand "xdpytools" {}
+  (toString
+    (lib.mapAttrsToList install {
+      xdpychvt.path = [
+        "$out"
+        "/run/wrappers/'$LOGNAME'"
+        "/run/wrappers"
+      ];
+      xdpysel.path = [
+        "$out"
+        pkgs.findutils
+        pkgs.jq
+      ];
+    }))
diff --git a/tv/5pkgs/simple/xdpytools/src/xdpychvt b/tv/5pkgs/simple/xdpytools/src/xdpychvt
new file mode 100755
index 000000000..84c1907b9
--- /dev/null
+++ b/tv/5pkgs/simple/xdpytools/src/xdpychvt
@@ -0,0 +1,11 @@
+#! /bin/sh
+# usage: xdpychvt {prev,next}
+# Changes to the VT based on the selected X display.
+#
+# This allows switching between X servers when display names and VT numbers
+# correlate.  A more sophisticated tool would try to determine the correct VT
+# by e.g. looking at /proc, but this might not possible when e.g.  using
+# security.hideProcessInformation.
+#
+
+chvt "$(xdpysel "$1")"
diff --git a/tv/5pkgs/simple/xdpytools/src/xdpysel b/tv/5pkgs/simple/xdpytools/src/xdpysel
new file mode 100755
index 000000000..e08015576
--- /dev/null
+++ b/tv/5pkgs/simple/xdpytools/src/xdpysel
@@ -0,0 +1,49 @@
+#! /bin/sh
+# usage: xdpysel {prev,next}
+# Print the number of the selected X display.
+
+find /tmp/.X11-unix -mindepth 1 -maxdepth 1 |
+jq -Rrs --arg command "$1" '
+  (
+    split("\n") |
+    map(
+      select(.!="") |
+      match("^.*/X([0-9]+)$").captures[0].string |
+      tonumber
+    )
+  )
+    as $all_displays |
+
+  (
+    env.DISPLAY |
+    match("^:([0-9]+)(?:[.][0-9]+)?$").captures[0].string |
+    tonumber
+  )
+    as $current_display |
+
+  ($all_displays | length) as $all_displays_count |
+
+  ($all_displays|index($current_display))
+    as $current_index |
+
+  (($current_index + 1) % $all_displays_count)
+    as $next_index |
+
+  (($all_displays_count + $current_index - 1) % $all_displays_count)
+    as $prev_index |
+
+  $all_displays[$prev_index] as $prev_display |
+  $all_displays[$next_index] as $next_display |
+
+  {
+    prev: $prev_display,
+    next: $next_display,
+  }[$command]
+    as $result |
+
+  if $result | type == "number" then
+    $result
+  else
+    "xdpysel: bad argument: \($command)\n" | halt_error(-1)
+  end
+'