Home Manager

Home Manager manages one user's environment as code. It owns the packages on your PATH, the dotfiles in your home directory, your shell configuration, and a set of long running user services, all from a single Nix module you commit to git. The technical claims here follow the official Home Manager manual .

How it differs from NixOS system config

NixOS configures the whole machine. It sets up the bootloader, kernel, filesystems, system wide packages, and daemons that run as root. Home Manager configures a single user account and nothing else. The split matters in practice for a few reasons.

  • A NixOS rebuild needs root and changes the system for everyone. A Home Manager switch runs as your user and only touches your home directory.
  • Home Manager runs on a NixOS host, but it also runs on Ubuntu, Fedora, Arch, or macOS, anywhere the Nix package manager is installed. Your dotfiles travel with you even to machines you do not administer.
  • System config is the right home for hardware, networking, and shared services. Home Manager is the right home for your editor, your shell, your prompt, and per user secrets in your environment.

A useful rule of thumb is that anything you would put in your dotfiles repo belongs in Home Manager, and anything that needs root belongs in NixOS.

The shape of home.nix

A Home Manager configuration is a module, the same function shape used across Nix. It takes an attribute set of arguments and returns an attribute set of options. Ternix generates the file as home.nix.

{ config, pkgs, ... }:
 
{
  home.username = "alice";
  home.homeDirectory = "/home/alice";
 
  # Set this to the release you first installed Home Manager with.
  home.stateVersion = "24.11";
 
  # Let Home Manager manage itself.
  programs.home-manager.enable = true;
}

config lets you read other options back (handy for paths like config.xdg.dataHome), and pkgs is the package set you install from. The ... absorbs any other arguments the module system passes in, so you do not have to name them all.

home.username and home.homeDirectory tell Home Manager whose environment it is building. On macOS the home directory is usually /Users/alice instead.

home.stateVersion pins the defaults for stateful pieces (things like the default format of a generated config). Set it once to the release you started on and leave it. Bumping it can silently change those defaults, so treat it as a record of when you began rather than a number to keep current. Upgrading the packages themselves is done by updating your flake inputs, not by raising stateVersion.

Installing packages

home.packages is a list of derivations that land on your PATH. The with pkgs; prefix saves you writing pkgs. in front of every name.

home.packages = with pkgs; [
  ripgrep
  fd
  bat
  htop
  jq
  fzf
  unzip
];

After a switch these tools are available in any new shell. This is the user level equivalent of environment.systemPackages in NixOS, except it installs only for you and needs no root.

Configuring programs

The programs.* options do more than install a package. They install the tool and write its configuration file from the Nix values you provide, so the binary and its config stay in sync. The examples below are the ones you will reach for first.

Git

programs.git = {
  enable = true;
  userName = "Ada Lovelace";
  userEmail = "ada@example.com";
 
  aliases = {
    co = "checkout";
    st = "status";
    lg = "log --oneline --graph --decorate";
  };
 
  extraConfig = {
    init.defaultBranch = "main";
    pull.rebase = true;
    push.autoSetupRemote = true;
  };
};

aliases becomes [alias] entries and extraConfig is a structured way to write any other section of ~/.gitconfig. The nested attribute init.defaultBranch maps onto the [init] section with a defaultBranch key.

Zsh

programs.zsh = {
  enable = true;
  autosuggestion.enable = true;
  syntaxHighlighting.enable = true;
  enableCompletion = true;
 
  shellAliases = {
    ll = "ls -alh";
    gs = "git status";
    update = "home-manager switch --flake .#user";
  };
 
  history = {
    size = 10000;
    path = "${config.xdg.dataHome}/zsh/history";
  };
 
  oh-my-zsh = {
    enable = true;
    plugins = [ "git" "sudo" "docker" ];
    theme = "robbyrussell";
  };
 
  initContent = ''
    bindkey -e
    export EDITOR=nvim
  '';
};

oh-my-zsh pulls in the framework and enables the plugins and theme you name. initContent holds raw lines appended to .zshrc for anything the structured options do not cover. Note that the option is initContent in current Home Manager, which replaced the older initExtra.

Bash

If you prefer bash, the shape is the same with bashrcExtra for raw lines.

programs.bash = {
  enable = true;
  shellAliases = {
    ll = "ls -alh";
    ".." = "cd ..";
  };
  bashrcExtra = ''
    export EDITOR=nvim
  '';
};

Starship

programs.starship = {
  enable = true;
  enableZshIntegration = true;
  settings = {
    add_newline = false;
    character = {
      success_symbol = "[➜](bold green)";
      error_symbol = "[➜](bold red)";
    };
  };
};

settings is written out as starship.toml, so any key from the Starship documentation maps onto a Nix attribute here.

Neovim

programs.neovim = {
  enable = true;
  viAlias = true;
  vimAlias = true;
  defaultEditor = true;
 
  plugins = with pkgs.vimPlugins; [
    nvim-treesitter.withAllGrammars
    telescope-nvim
    gruvbox-nvim
  ];
 
  extraConfig = ''
    set number
    set relativenumber
    set expandtab
    set shiftwidth=2
    colorscheme gruvbox
  '';
};

Plugins come from pkgs.vimPlugins, so they are fetched and pinned by Nix rather than by a plugin manager at runtime. extraConfig takes Vimscript. If you write your config in Lua, wrap it with lua << EOF ... EOF or use extraLuaConfig instead.

Direnv

programs.direnv = {
  enable = true;
  nix-direnv.enable = true;
};

This pair is worth setting up early. direnv loads a per project environment when you cd into a directory with an .envrc, and nix-direnv makes the Nix flavour of that fast by caching the dev shell so it does not rebuild on every entry. Drop use flake in a project .envrc and the shell from its flake.nix loads automatically.

Tmux

programs.tmux = {
  enable = true;
  prefix = "C-a";
  baseIndex = 1;
  mouse = true;
  keyMode = "vi";
 
  plugins = with pkgs.tmuxPlugins; [
    sensible
    resurrect
  ];
 
  extraConfig = ''
    set -g status-position top
    bind | split-window -h
    bind - split-window -v
  '';
};

prefix rebinds the leader key, plugins installs from pkgs.tmuxPlugins, and extraConfig appends raw lines to tmux.conf for anything else.

Managing arbitrary dotfiles

Not every tool has a programs.* module. For those, write the config file directly. home.file places files relative to your home directory, and you can either inline the content with .text or point at a file in your repo with .source.

# Write a literal file into the home directory.
home.file.".config/foot/foot.ini".text = ''
  font=JetBrains Mono:size=11
 
  [colors]
  background=1d2021
'';
 
# Symlink a file from your repo into place.
home.file.".gitignore_global".source = ./dotfiles/gitignore_global;

xdg.configFile is the same idea scoped to ~/.config, which keeps the paths shorter and respects XDG_CONFIG_HOME if you have set it.

# Writes to ~/.config/alacritty/alacritty.toml
xdg.configFile."alacritty/alacritty.toml".text = ''
  [font]
  size = 12
'';
 
# Symlink a whole directory of config from your repo.
xdg.configFile."nvim".source = ./dotfiles/nvim;

Files managed this way are read only symlinks into the Nix store, which is the mechanism that makes a switch atomic and a rollback clean.

Session variables and PATH

home.sessionVariables sets environment variables for your login session, and home.sessionPath adds directories to PATH. Both are written into the files your shell sources at login.

home.sessionVariables = {
  EDITOR = "nvim";
  PAGER = "less -FR";
  MANPAGER = "nvim +Man!";
};
 
home.sessionPath = [
  "$HOME/.local/bin"
  "$HOME/go/bin"
];

These take effect in newly started login shells. After a switch, open a fresh terminal (or log out and back in) to pick up the changes.

User services

services.* declares long running processes that run as your user through systemd user units. Because they rely on systemd, these options work on Linux, including NixOS and other distributions. The GnuPG agent is a common one to manage this way.

services.gpg-agent = {
  enable = true;
  enableSshSupport = true;
  defaultCacheTtl = 1800;
  pinentry.package = pkgs.pinentry-curses;
};

enableSshSupport lets the agent also serve as your SSH agent, and defaultCacheTtl controls how long it remembers an unlocked key in seconds. A switch starts the unit immediately, and you can inspect it afterwards with systemctl --user status gpg-agent.

Applying and updating

Ternix exposes the Home Manager output as homeConfigurations.user in the generated flake, so the name after # is user. From the directory holding flake.nix and home.nix, run a switch to build the configuration and activate it.

# Build and activate the configuration named "user".
home-manager switch --flake .#user

Every switch creates a generation, a numbered snapshot you can return to. List them with their timestamps.

home-manager generations

If a change misbehaves, roll back without editing any files. Each line of home-manager generations ends with a store path. Run the activate script inside the generation you want to return to.

/nix/store/<hash>-home-manager-generation/activate

The rollback reactivates the last generation, so it is a fast way to recover while you work out what went wrong in the config.

Advanced

Standalone versus a NixOS module

The setup above is standalone Home Manager, driven by its own home-manager command. On a NixOS machine you can instead fold the same home.nix into the system build so a single nixos-rebuild handles both. Import the Home Manager NixOS module and assign your user.

# configuration.nix
{
  imports = [ home-manager.nixosModules.home-manager ];
 
  home-manager.useGlobalPkgs = true;
  home-manager.useUserPackages = true;
  home-manager.users.alice = import ./home.nix;
}

useGlobalPkgs makes Home Manager use the system nixpkgs and its configuration rather than its own, and useUserPackages installs user packages into the system profile. This needs home-manager declared as a flake input that feeds home-manager.nixosModules.home-manager into the system. The tradeoff is that you can no longer switch your user environment on its own. It rebuilds with the system. Standalone keeps the two independent, which is the better fit when your user config has to run on machines that are not NixOS.

Activation scripts

When an option does not exist for something you need to run at switch time, home.activation lets you attach a shell snippet to the activation. Order it against the built in writeBoundary so it runs after files are linked, and use the run helper so dry runs stay safe.

{ config, lib, pkgs, ... }:
 
{
  home.activation.makeProjectDirs =
    lib.hm.dag.entryAfter [ "writeBoundary" ] ''
      if [ ! -d "$HOME/projects" ]; then
        run mkdir -p "$HOME/projects"
      fi
    '';
}

lib.hm.dag.entryAfter places your snippet in the activation graph after the named step, and run wraps the command so home-manager build and dry runs report what would happen without doing it. Keep activation scripts small and idempotent, since they run on every switch.

Next

  • Flakes explains the lock file and inputs that pin your config.
  • Using a config covers applying a flake and rolling back.
  • Editing shows how to add packages and split a config into modules.
  • Examples collects ready to adapt configurations.