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 .#userEvery switch creates a generation, a numbered snapshot you can return to. List them with their timestamps.
home-manager generationsIf 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/activateThe 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.