Skip to content

Instantly share code, notes, and snippets.

@mawkler
Last active January 12, 2026 22:45
Show Gist options
  • Select an option

  • Save mawkler/195def384fd3f73aeb9a965c82781483 to your computer and use it in GitHub Desktop.

Select an option

Save mawkler/195def384fd3f73aeb9a965c82781483 to your computer and use it in GitHub Desktop.
A home-manager directory that auto-creates dotfile symlinks to it in `~/.config/` based on the file structure

The code below lets you create a directory in your home-manager config that auto-creates symlinks for non-Nix dotfiles from ~/.config/ using xdg.configFile and mkOutOfStoreSymlink. This gives the following advantages:

  1. You can keep all your non-Nix dotfiles in your home-manager repo.
  2. You don't have to do home-manager switch on each dotfile change. Changes are instantly reflected.
  3. You don't need to hard-code the paths for each symlink, the linking is instead file-based. Just add the new dotfile to the directory, and you're done.

For example, if your home-manager config is in ~/nixos/ and looks like this...:

~/nixos
├── ...
├── flake.nix
└── configs
    ├── starship.toml
    └── fish
        └── config.fish

...then home-mamager will create the following symlinks in ~/.config:

~/.config
├── ...
├── starship.toml -> ~/nixos/configs/starship.toml
└── fish
    └── config.fish -> ~/nixos/configs/fish/config.fish

Don't forget to git add when adding a new dotfile. Otherwise, they will be ignored by Nix. Then run home-manager switch to create the new symlink.

{ config }:
let
  # -------------------------------------------------------------------------
  #   `configsNixPath`: a Nix path type to a directory who's files/directories
  #   should be symlinked to from `~/.config/`.
  #
  #   `configsAbsPath`: an absolute string path to `configsPath`.
  #
  #   The reason this function requires two path parameters to the same
  #   directory is that it uses `builtins.readDir` which would require
  #   `--impure` if only an absolute path were used. If using only a literal
  #   Nix path, the symlinks would point to the Nix store, and thereby require
  #   a build whenever a config file is edited.
  #
  #   `returns`: an attribute set suitable for `xdg.configFile`.
  # -----------------------------------------------------------------
  mkSymlinks =
    configsAbsPath: configsNixPath:
    let
      inherit (config.lib.file) mkOutOfStoreSymlink;

      # Turn relative paths into xdg.configFile entries
      mkSymlink = nixPath: {
        name = nixPath;
        value.source = mkOutOfStoreSymlink "${configsNixPath}/${nixPath}";
      };

      # Recursively read in all files in all subdirectories
      readDirRecursive =
        relPath: nixPath:
        nixPath
        |> builtins.readDir
        |> builtins.attrNames
        |> map (
          name:
          let
            entryType = nixPath |> builtins.readDir |> (entries: entries.${name});
            relPath' = if relPath == "" then name else "${relPath}/${name}";
            nixPath' = "${nixPath}/${name}";
          in
          # Don't include paths to the directories themselves
          if entryType == "directory" then readDirRecursive relPath' nixPath' else [ relPath' ]
        )
        |> builtins.concatLists;
    in readDirRecursive "" configsAbsPath |> map mkSymlink |> builtins.listToAttrs;
in {
  # Change the arguments to point to your `configs` directory
  xdg.configFile = mkSymlinks ./configs "~/nixos/configs";
}

Please let me know if there's some way to improve the implementation so that you only have to pass in one path parameter.

@Pablo1107
Copy link

@mawkler
Copy link
Author

mawkler commented Jan 5, 2026

@Pablo1107 Thank you for clarifying! Does persistence create one symlink per item in your ~/dotfiles/config/ directory like my solution does, or just one symlink for the entire directory? And what happens to files in ~/.config/ that are not tracked by persistence?

Also, what is the files attribute doing there? I don't really understand its purpose even after reading its documentation. Why are you treating your Neovim dotfiles differently, when you have a bunch of other dotfiles in ~/dotfiles/config/?

@Pablo1107
Copy link

Pablo1107 commented Jan 5, 2026

Does persistence create one symlink per item in your ~/dotfiles/config/ directory like my solution does, or just one symlink for the entire directory? And what happens to files in ~/.config/ that are not tracked by persistence?

It works similary to stow when the removePrefixDirectory is set to true, so does files like nvim/.config/nvim/init.lua are symlinked to ~/.config/nvim/init.lua. Other files that lives in ~/.config are not tracked if not in the repo.

Also, what is the files attribute doing there? I don't really understand its purpose even after reading its documentation. Why are you treating your Neovim dotfiles differently, when you have a bunch of other dotfiles in ~/dotfiles/config/?

Neovim sometimes add other files in the ~/.config/nvim folder that I don't mind tracking as most of my config lives in either init.lua or lua folder. You could also just symlink nvim folder directly and track everything.

@mawkler
Copy link
Author

mawkler commented Jan 6, 2026

Ok, thank you for your explanation!

@mawkler
Copy link
Author

mawkler commented Jan 12, 2026

I did some proompting and was able to improve the script to create symlinks recursively for files in any subdirectories, instead of just symlinking the first level of subdirectories themselves. This solves an issue I had with some home-manager modules that themselves generate files in subdirectories ~/.config/, including hyprpaper. I've updated the original gist with my new implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment