| title | nix-home |
|---|---|
| date | 2026-10-19 |
My home environment. Packages are pinned using niv.
The same configuration supplies the terminal tools and coding agents on the Mac and an Ubuntu server. macOS defaults to the desktop profile; Linux defaults to headless. Both use the same agent definitions and version pins.
xcode-select --install
curl -L https://nixos.org/nix/install > nix-install
chmod +x nix-install
./nix-install
nix --version
echo "trusted-users = root $USER" | sudo tee -a /etc/nix/nix.conf && sudo pkill nix-daemon
- Install home-manager:
git clone git@github.com:pwm/nix-home.git ~/nix-home && cd ~/nix-home
bin/hm-install -n <git_name> -e <git_email>
bin/hm-run home-manager switch -b backup
Install Nix using its Linux multi-user instructions,
then open a shell with Nix on PATH. Run the following as your normal user:
git clone git@github.com:pwm/nix-home.git ~/nix-home
cd ~/nix-home
bin/hm-install -n "Your Name" -e "you@example.com" --headless
bin/hm-run home-manager build
bin/hm-run home-manager switch -b backupbuild prepares the Home Manager generation without activating it. switch
applies it. The installer uses the actual home directory and checkout location,
and writes ~/.config/home-manager/home.nix. It replaces that configuration
when rerun, so keep any manual additions before doing so.
Keep the working checkout at ~/nix-home: the shared shell adds that directory's
bin/ to PATH for commands such as hm and the package-update helpers.
The headless profile includes Claude Code, Codex, OMP, Fish, Zellij, Neovim, direnv and the CLI tools. It leaves out Alacritty, VS Code, Zed, desktop fonts, Inkscape and SQLite Browser. The oMLX shim and Mac clipboard helper remain Mac-only. Agent login, mutable settings, MCP registration, conversations and memories are separate from this package installation and stay outside the Nix store.
Use ~/.nix-profile/bin/fish to enter the configured shell. This repository
does not change the account's login shell, SSH access or operating-system
services. For remote Zellij, attach to an explicit session, for example
zellij attach -c pml. Headless Fish sessions do not start Zellij automatically;
desktop sessions keep automatic startup.
~/.config/home-manager/home.nix imports this checkout with ext_config:
import /home/pwm/nix-home {
ext_config = {
user = "pwm";
homeDirectory = "/home/pwm";
desktop = false;
git = {
name = "Your Name";
email = "you@example.com";
};
};
}homeDirectory defaults to /Users/<user> on macOS and /home/<user> on Linux.
desktop defaults to true on macOS and false on Linux. --headless writes an
explicit false. The supported targets for this preparation are Apple Silicon
macOS and x86_64 Linux; the pinned agent packages may not support other targets.
NIXPKGS_ALLOW_UNFREE=1 nix-instantiate --eval --strict --json checks.nix
bin/hm-run home-manager buildThe first command checks the complete Home Manager configuration and package derivations for both targets, a custom Linux home and an explicitly headless Mac. It builds nothing. The second builds your native configuration without switching to it. Linux packages still need to be built and exercised on Linux; cross-platform evaluation on a Mac does not establish that they run.
Do whatever change and then run
hm switch
niv update nixpkgs
niv update home-manager
hm switch
claude-code, codex and pi-coding-agent come from one pin that tracks
nixpkgs master (not nixos-unstable), as master gets the newest agent
versions days before they land on the unstable channel. claude-code and
codex take only their build recipes from that pin, their versions are set
in pkgs/ (see below). The -b master flag keeps the pin on master and is
idempotent, so just run:
niv update claude-code-nixpkgs-pin -b master
hm switch
nixpkgs master usually follows Claude Code within a day, but the pin only
moves when it is updated by hand, and each pin update also moves everything
else on it (which rebuilds codex). So pkgs/claude-code.nix takes the
claude-code derivation from the pin and swaps in a newer upstream release
manifest, pkgs/claude-code-manifest.zst.json. That is the same file nixpkgs
ships next to its own derivation. It carries the version and the checksum of
the prebuilt binary for each platform, so there are no hashes to work out and
no build, only a download.
To bump Claude Code to the latest upstream release:
bump-claude
hm switch
bump-claude (in bin/) looks up the latest version at
https://downloads.claude.ai/claude-code-releases/latest, downloads that
release's manifest.zst.json, checks that it names the expected version and
has a binary for each platform nixpkgs supports, and copies it over
pkgs/claude-code-manifest.zst.json. Pass a version to pick a specific
release instead, e.g. bump-claude 2.1.281. To do the same by hand, download
https://downloads.claude.ai/claude-code-releases/<version>/manifest.zst.json
over pkgs/claude-code-manifest.zst.json.
As with codex below, a pin update that moves nixpkgs past the manifest
version is caught at evaluation time: hm switch fails with a message that
says to run bump-claude (or to drop the override). bump-claude itself
refuses a version older than the pin's.
Even nixpkgs master is a few releases behind codex, and codex has several
releases per week. So pkgs/codex.nix takes the codex derivation from the
pin and swaps in the source of a newer upstream tag. Everything else (Rust
build, prebuilt librusty_v8, darwin lld workaround, ripgrep wrapper,
shell completions) is reused from nixpkgs. The cost is a local Rust build of
codex and its dependencies, which takes some minutes on the first build and
on each bump.
To bump codex to the latest upstream release:
bump-codex
hm switch
bump-codex (in bin/) looks up the latest release tag, checks the v8
crate version (see below), rewrites version, srcHash and cargoHash in
pkgs/codex.nix, and gets the two real hashes by building each fetch with a
fake hash and reading the got: value from the hash mismatch error. If
anything fails it restores pkgs/codex.nix. Pass a version to pick a specific
release instead, e.g. bump-codex 0.153.4. The hm switch afterwards is the
real (long) build.
To do the same by hand, in pkgs/codex.nix:
- Set
versionto the new version (without therust-vprefix). - Set
srcHashandcargoHashtopkgs.lib.fakeHash. - Run
hm switch. It fails withhash mismatchfor the source. Copy thegot:value intosrcHash. - Run
hm switchagain. It fails withhash mismatchforcodex-<version>-vendor-staging. Copy thegot:value intocargoHash. - Run
hm switchonce more. This is the real build.
Two things can break a bump. Both are fixed by updating the pin
(niv update claude-code-nixpkgs-pin -b master) so that nixpkgs has the
newer version:
- The
v8crate version in upstreamcodex-rs/Cargo.lockmust match thelibrusty_v8version in the pin'spkgs/by-name/co/codex/librusty_v8.nix.bump-codexprints both and refuses to bump on a mismatch. To check by hand, the upstream side iscurl -sL https://raw.githubusercontent.com/openai/codex/rust-v<version>/codex-rs/Cargo.lock | grep -A1 'name = "v8"'and the pin's local path isnix-instantiate --eval --expr '(import ./nix/sources.nix).claude-code-nixpkgs-pin.outPath'. - The nixpkgs
postPatcheditslto = "thin"andcodegen-units = 4in the workspaceCargo.tomlwith--replace-fail, so it fails with a clear error if upstream changes those release profile settings.
The opposite case, a pin update that moves nixpkgs past the version in
pkgs/codex.nix, is caught at evaluation time: hm switch fails with a
message that says to run bump-codex (or to drop the override). Without
that check the override would silently hold codex back.
Why not just override cargoHash? Because buildRustPackage reads
cargoHash from the original package arguments and not from the final
attributes, so an overridden cargoHash does not change the vendored crates.
That is why pkgs/codex.nix overrides cargoDeps with its own
fetchCargoVendor call. It still sets cargoHash too, so the derivation is
identical to the one nixpkgs would build for that version.
pkgs/omp.nix pins the official OMP release and checksums. It supports Apple
Silicon macOS and Linux x86_64 through the same package definition. The Linux
binary has its ELF interpreter and library paths fixed for Nix, with stripping
disabled to preserve the embedded Bun application. macOS keeps upstream signing
intact by skipping fixups. The install check runs omp --version on the build
platform and requires the pinned version.
fff-mcp (the file search MCP server from
dmtrKovalenko/fff) rides the same pin.
Nix installs the binary, but the MCP registration lives in ~/.claude.json,
which Claude Code rewrites at runtime, so home-manager cannot manage it. After
the first hm switch, register it once by hand:
claude mcp add -s user fff -- ~/.nix-profile/bin/fff-mcp --no-update-check
claude mcp get fff
Do not install nixpkgs fff. That is an unrelated bash file manager by a
different author that happens to share the name.
Running the following:
vscode-update-extensions
will look at the current extensions used (via code --list-extensions), download their latest version and write it out to hm/programs/vscode/extensions.json.
then, as usual, run:
hm switch