Standalone Home Manager
A standalone home is a Home Manager configuration that is not part of any
NixOS or nix-darwin system. You activate it with the home-manager CLI (or
nh home), without root, on any machine that has Nix, including non-NixOS
Linux, macOS, or a NixOS host whose system config lives somewhere else.
In den a standalone home is an entity declared under den.homes. It is
configured by the same aspects as a host user, so one den.aspects.tux can
drive both tux on your NixOS laptop and tux on a work machine you don’t
administer.
For Home Manager inside a host (home-manager.users.<name>), see
Home Environments.
Quick start
Section titled “Quick start”-
Declare the home
den.homes.x86_64-linux.tux = { }; -
Configure it through the user’s aspect
den.default.homeManager.home.stateVersion = "25.11";den.default.includes = [ den.batteries.define-user ];den.aspects.tux.homeManager = { pkgs, ... }: {programs.fish.enable = true;home.packages = [ pkgs.htop ];};define-usersetshome.usernameandhome.homeDirectory(/home/tux, or/Users/tuxon darwin). Home Manager refuses to build without them, so include it or set both yourself. -
Switch
Terminal window home-manager switch --flake .#tux
This needs inputs.home-manager and inputs.nixpkgs in your flake. The
default builder is inputs.home-manager.lib.homeManagerConfiguration.
Declaring homes
Section titled “Declaring homes”The key under den.homes.<system> is the home’s name. It takes one of two
forms:
# Plain: tux, on whatever machine you switch from.den.homes.x86_64-linux.tux = { };
# Host-bound: tux on the machine called igloo.den.homes.x86_64-linux."tux@igloo" = { };
# Flat form: system inside the value.den.homes."tux@work" = { system = "aarch64-darwin"; };The part before @ is the user (home.userName), the part after is the host
(home.hostName, null for a plain home). The host after @ does not have
to be declared in den.hosts. It can be a machine den knows nothing about.
| Declaration | Flake output | host arg in aspects | osConfig |
|---|---|---|---|
tux | homeConfigurations.tux | absent | absent |
"tux@astra", astra not in den.hosts | homeConfigurations."tux@astra" | { name = "astra"; system = "x86_64-linux"; } | absent |
"tux@igloo", igloo in den.hosts | homeConfigurations."tux@igloo" | the real igloo host entity | nixosConfigurations.igloo.config |
The user arg is always bound. For a declared host that has the user, it is
the host’s user record; otherwise it is built from the home’s name.
Output names
Section titled “Output names”The output is homeConfigurations.<name>. If the same name is declared on two
systems, both outputs get the system appended so neither overwrites the
other:
den.homes.x86_64-linux.pingu = { };den.homes.aarch64-linux.pingu = { };# => homeConfigurations."pingu@x86_64-linux", homeConfigurations."pingu@aarch64-linux"To choose the name yourself, set intoAttr:
den.homes.aarch64-darwin.tux.intoAttr = [ "homeConfigurations" "tux@mac" ];Building and switching
Section titled “Building and switching”home-manager picks a configuration by name:
home-manager switch --flake .#tux # explicithome-manager switch --flake .#tux@igloo # explicit, host-boundhome-manager switch --flake . # tries "$USER@$(hostname)", then "$USER"The bare --flake . form is why the user@host spelling is useful: on
igloo, logged in as tux, it selects homeConfigurations."tux@igloo"
without being told.
With nh:
nh home switch . -c tux@iglooden.lib.nh apps
Section titled “den.lib.nh apps”den.lib.nh.denPackages builds one runnable package per host and per home,
wrapping nh. With flake-parts:
{ den, ... }:{ perSystem = { pkgs, ... }: { packages = den.lib.nh.denPackages { fromFlake = true; } pkgs; };}A home’s package runs nh home <action> .# -c <home name>. The action
defaults to build, and the first argument overrides it:
nix run .#tux # nh home build . -c tuxnix run .#tux -- switch # nh home switch . -c tuxden.lib.nh.denShell gives a dev shell with the same commands on PATH. The
noflake template uses it.
Which aspects configure a home
Section titled “Which aspects configure a home”A home is configured by the aspects named after it:
| Home | Aspects applied |
|---|---|
tux | den.aspects.tux |
"tux@igloo" | den.aspects."tux@igloo" and den.aspects.tux |
Both apply. Shared configuration goes in den.aspects.tux, and
machine-specific additions go in the host-qualified one:
den.homes.x86_64-linux."tux@igloo" = { };den.homes.x86_64-linux."tux@work" = { };
den.aspects.tux.homeManager.programs.git.enable = true; # both homesden.aspects."tux@igloo".homeManager.programs.fish.enable = true; # igloo onlyden.aspects.tux.provides.work.homeManager.programs.zsh.enable = true; # work onlyprovides.<host> on the user aspect does the same job as the qualified name.
With several user@host homes for one user, each home gets only its own
host’s provides. See Host-qualified aspects
for the naming rules.
Anything gated on host works on a host-bound home, including a synthetic
host that isn’t declared in den.hosts:
den.aspects.tux.includes = [ ({ host, ... }: { homeManager.home.sessionVariables.MACHINE = "${host.name}/${host.system}"; })];On a plain home (tux, no @) there is no host. A { host, ... } include
does not fire and doesn’t raise an error. If an aspect needs the machine’s
name, declare the home as user@host.
Sharing one user between a host and standalone homes
Section titled “Sharing one user between a host and standalone homes”A standalone home and a host user named tux both read den.aspects.tux.
This is how to keep one user definition for your NixOS machines and your
non-NixOS ones:
flowchart TD fs["flake-system x86_64-linux"] -->|"system-to-os-outputs"| host["host scope: igloo"] fs -->|"system-to-hm-outputs"| home["home scope: tux@igloo"] host --> user["user scope: tux"] user -->|"forward homeManager"| nixos["nixosConfigurations.igloo<br/>home-manager.users.tux"] home --> hc["homeConfigurations."tux@igloo""] nixos -.->|"osConfig (declared host only)"| hc
The home is a sibling of the host, not a child. It resolves in its own scope
under the per-system flake scope, so nothing flows into it from the host
except host, user and osConfig.
den.hosts.x86_64-linux.igloo.users.tux = { }; # NixOS machineden.homes.x86_64-linux.tux = { }; # any other machine
den.aspects.tux = { homeManager.programs.git.enable = true; # reaches both nixos.users.users.tux.extraGroups = [ "wheel" ]; # igloo only};What differs on the standalone side:
-
No host OS classes.
nixosanddarwinkeys on the user aspect reach the host user’s system and are ignored for a standalone home. They cause no error, so the same aspect can carry both. -
No
osConfigunless the home isuser@hostand that host is declared inden.hosts. A module that has to work in both places takesosConfigwith a default:den.aspects.tux.homeManager = { osConfig ? null, ... }: {home.sessionVariables.OS_HOSTNAME =if osConfig == null then "unmanaged" else osConfig.networking.hostName;}; -
Host-side routing does not reach the home. Content that the host pushes to its users reaches
home-manager.users.tuxinside igloo, but nothomeConfigurations."tux@igloo", even when igloo is declared. This covers a host policy that includes into its users,den.aspects.igloo.provides.tux, and thehost-aspectsbattery. Put content that has to reach both on the user side instead:den.aspects."tux@igloo"andden.aspects.tux.provides.iglooapply to the host user and thetux@igloohome.
A declared host with the home managed separately
Section titled “A declared host with the home managed separately”Declaring both a host user and a user@host home lets you rebuild the system
and the home independently, with nixos-rebuild for igloo and home-manager
for tux. The home reads the host’s settings through osConfig:
den.hosts.x86_64-linux.igloo.users.tux.classes = [ "user" ];den.homes.x86_64-linux."tux@igloo" = { };
den.aspects.tux.homeManager = { osConfig, ... }: { home.sessionVariables.OS_HOSTNAME = osConfig.networking.hostName;};classes = [ "user" ] keeps Home Manager out of igloo’s system config, so
tux’s home is managed only by the standalone home. define-user still creates
the OS account on igloo. If you leave the user’s classes at homeManager, the
home is built twice, once inside the system and once standalone, and whichever
you activate last wins.
Defaults for homes
Section titled “Defaults for homes”There are three places to put shared defaults. Each reaches a different set of entities:
| Where | Standalone homes | Host users |
|---|---|---|
den.schema.home.includes | yes | no |
den.schema.user.includes | no | yes |
den.default / den.default.includes | yes | yes |
# every standalone home, and nothing elseden.schema.home.includes = [ { homeManager.targets.genericLinux.enable = true; }];
# everythingden.default.homeManager.home.stateVersion = "25.11";den.schema.home.includes is the replacement for the older den.ctx.home
spelling.
pkgs for homes
Section titled “pkgs for homes”Each home is built with home.pkgs, which defaults to
inputs.nixpkgs.legacyPackages.<system>. Override it per home:
den.homes.x86_64-linux.tux.pkgs = import inputs.nixpkgs { system = "x86_64-linux"; config.allowUnfree = true;};Or for every home at once:
den.schema.home = { home, ... }: { pkgs = import inputs.nixpkgs { inherit (home) system; config.allowUnfree = true; };};For unfree packages by name, the unfree battery works on standalone homes
too. It sets Home Manager’s nixpkgs.config.allowUnfreePredicate:
den.aspects.tux.includes = [ (den.batteries.unfree [ "vscode" ]) ];To pass extraSpecialArgs, or to use a different Home Manager input, replace
instantiate:
den.homes.x86_64-linux.tux.instantiate = { pkgs, modules }: inputs.home-manager-unstable.lib.homeManagerConfiguration { inherit pkgs modules; extraSpecialArgs.osConfig = config.flake.nixosConfigurations.igloo.config; };Quirks in standalone homes
Section titled “Quirks in standalone homes”Each standalone home is its own scope.
Quirk values emitted by its aspects are read by its own homeManager modules,
as they would be in a host user’s scope.
To read the home’s own Home Manager config from a quirk, write the value as a
config-dependent thunk. For a home,
config is the Home Manager config itself, so there is no
config.home-manager.users.<name> to reach through:
den.quirks.identities.description = "who am I, per machine";
den.aspects.tux = { identities = { config, ... }: [ "${config.home.username}@${config.home.homeDirectory}" ];
homeManager = { identities, lib, ... }: { home.sessionVariables.IDS = lib.concatStringsSep "," identities; };};Pitfalls
Section titled “Pitfalls”home-manager switch builds an old generation or says nothing changed.
Flakes only see files git tracks, so a new module you haven’t git added is
not part of the build. Stage new files before switching. A user in
#639 saw this symptom, and it did
not reproduce against den.
--flake . picks the wrong configuration, or none. Home Manager’s guess is
"$USER@$(hostname)", then "$USER". If your home is named something else,
pass it explicitly (.#name, or nh home switch . -c name).
A { host, ... } include does nothing on a plain home. A plain home has no
host, so the include is skipped with no error. Name the home user@host.
Host-pushed config is missing from a user@host home. Host policies,
den.aspects.<host>.provides.<user>, and host-aspects target the users
inside the host, not the sibling standalone home
(#614). Move that content to
den.aspects."<user>@<host>" or den.aspects.<user>.provides.<host>, which
reach both.
osConfig is missing. It exists only for user@host homes whose host is
declared in den.hosts. Take it as { osConfig ? null, ... } in shared
modules, or wire it yourself through instantiate.
options = lib.mkMerge [ ... ] in den.schema.home or den.schema.user.
mkMerge, mkIf and the other mk* combinators build definitions, not
option declarations. Write one plain attrset under options, or split the
declarations into separate modules under imports. Den reports this with an
error naming the schema option
(#672).
mutual-provider in the old docs. Cross-entity provides routing is built
in. You don’t need to include a battery for provides.<host> to work on a
home (#553).
Fixed in current den
Section titled “Fixed in current den”If you hit one of these on an older pin, update den:
| Symptom | Fixed by |
|---|---|
Several user@host homes for one user all get the first host’s provides.<host> / host-gated config (#635, #631) | #641, #643 |
A { user, ... } class module is silently dropped on a plain home or a user@host home with an undeclared host (#640) | #643 |
Host-gated config ignored on a user@host home whose host isn’t in den.hosts | #605 |
The same home name on two systems collides in homeConfigurations | #565 |
nix run .#<home> fails with 'homeConfigurations.<home>.type' is not a string (#253) | #347 |
expected a string but found null for a plain home (#364) | 7e18d2c |
Two homes collide with flake.homeConfigurations' is defined multiple times without flake-parts (#315) | homeConfigurations has merge semantics by default. If you declare flake outputs yourself, import inputs.den.flakeOutputs.homeConfigurations. |
See also
Section titled “See also”- Home Environments: Home Manager, hjem and nix-maid inside hosts
- Declare Hosts & Users: the home schema options
den.homesreference- Host↔User Mutual Config:
providesrouting