Packages and programs

Updated

July 30, 2026

Packages and programs

Goal: Install software declaratively on the host—know when to use environment.systemPackages versus programs.* modules, and keep ad-hoc profiles from becoming the real SoT.

Why this chapter matters

New NixOS users nix-env / nix profile install a zoo, then wonder why rebuilds do not reproduce the machine. Declarative packages are how the host stays honest.

Ops motivation: reproducible admin tooling is part of incident response. The box you SSH into should carry jq and friends because the flake says so—not because someone remembered to install them once.


Theory 1 — environment.systemPackages

environment.systemPackages = with pkgs; [
  vim
  git
  curl
  htop
  jq
];
Property Meaning
On PATH system-wide For all users (typical)
Closure in system gen Rolled back with system
No service config Just binaries/libs

This is the blunt instrument—and it is correct for many CLI tools.

Organization pattern

environment.systemPackages = with pkgs; [
  # editors
  vim
  # net
  curl
  # observability
  htop
];

Readable policy beats an unsorted dump.


Theory 2 — programs.* modules

Many packages have first-class modules:

programs.git.enable = true;
programs.zsh.enable = true;
programs.tmux.enable = true;

Modules may:

  • Install the package
  • Drop config snippets
  • Set shells in /etc/shells
  • Enable related services

Prefer programs.* when you need integration, not only a binary.

Discovery

man configuration.nix  # huge
nixos-option programs.git 2>/dev/null

Online: https://search.nixos.org/options (select 26.05 when available).


Theory 3 — Decision table

Need Prefer
Just a CLI on PATH environment.systemPackages
Shell as login shell + vendor config programs.zsh / programs.fish / programs.bash
Git system-wide with config hooks programs.git and/or HM later
Language toolchains for one project devShell, not systemPackages
Desktop app packages or specialized modules
Always-on service services.* / systemd chapter

Theory 4 — Closure discipline (light)

Every package on the system increases:

  • Download size
  • Eval/build time
  • Attack surface
  • Cognitive load
Smell Fix
Entire language toolchains system-wide devShell / HM
Three editors “just in case” Pick one for system
Random nix profile duplicates Remove; declare in flake
Debug tools forever Keep a small admin set
nix path-info -Sh /run/current-system
nix path-info -rS /run/current-system | sort -n | tail

Theory 5 — Overlays and pins (awareness only)

You might see:

nixpkgs.overlays = [];

or pinned packages. Later parts cover overlays properly. Here: stick to nixpkgs 26.05 attribute names from your lock.


Theory 6 — Unfree and allow flags

Some packages need:

nixpkgs.config.allowUnfree = true;

Or per-package predicates:

nixpkgs.config.allowUnfreePredicate = pkg:
  builtins.elem (pkgs.lib.getName pkg) [ "vscode" ]; # example only

Only enable when you understand licensing implications for your lab. Narrow predicates beat global allow when possible.


Theory 7 — System vs user packages

Layer Mechanism
System environment.systemPackages, programs.*
User (later) Home Manager home.packages
Project nix develop shells
Imperative nix profile — minimize

Stage-shaped later work moves dotfiles and user CLIs to HM; this chapter is system layer only.


Theory 8 — environment.defaultPackages trap

NixOS may install a small default package set. Know it exists so you do not double-declare or fight removals:

# advanced / intentional only
# environment.defaultPackages = [ ];

Prefer adding what you need over emptying defaults on day one unless you understand the fallout (e.g. nano/perl expectations in scripts).


Theory 9 — Package name ≠ binary name

pkgs attr Binary might be
ripgrep rg
neovim nvim
some wrappers different meta.mainProgram
nix search nixpkgs ripgrep
command -v rg

Theory 10 — Layers interaction with generations

Adding/removing system packages creates new generations. Rollback brings the old package set back—another reason not to rely on imperative profiles for “real” tools.


Worked examples bank

Example A — Baseline admin tools

{ pkgs, ... }: {
  environment.systemPackages = with pkgs; [
    vim
    git
    curl
    wget
    htop
    tmux
    jq
    ripgrep
    tree
  ];
}

Example B — programs integration

{
  programs.zsh.enable = true;
  users.users.alice.shell = pkgs.zsh;
}

Example C — Search package attr

nix search nixpkgs ripgrep

Example D — Conditional unfree single package

{
  nixpkgs.config.allowUnfreePredicate = pkg:
    builtins.elem (pkgs.lib.getName pkg) [ "vscode" ];
}

Example E — Avoid double install

If programs.git.enable = true already installs git, do not also list git unless you have a reason—collapse to one path.


Lab 1 — Declare your daily CLI set

List 8–15 tools you truly want on the lab host. Add them via environment.systemPackages. Rebuild switch. Verify each:

command -v rg
command -v jq
# …

Lab 2 — Convert one package to programs.*

Pick something with a module (git, zsh, tmux, …):

  1. Remove from systemPackages if redundant
  2. Enable programs.<name>
  3. Rebuild
  4. Note extra behavior (completions, config files)

Lab 3 — Anti-profile audit

nix profile list 2>/dev/null || true

If tools exist only imperatively, either migrate into flake or remove them. Document: flake owns system tools.


Lab 4 — Size awareness

nix path-info -Sh /run/current-system
nix path-info -rS /run/current-system | sort -n | tail

Write one sentence on the cost of your package list.


Lab 5 — Split comments in config

Organize packages with section comments. Commit as readable policy.


Lab 6 — programs.* option archaeology

nixos-option programs.zsh 2>/dev/null | head -40

List three options under a programs.* module that are not just “install the binary.”


Lab 7 — Remove one redundant package

Find a package present both via systemPackages and a programs.* enable (or profile). Collapse to one path. Rebuild. Confirm still on PATH.


Exercises

  1. Build a “minimal rescue” package set vs “comfortable admin” set; compare sizes.
  2. Move a language toolchain out of systemPackages into a project devShell.
  3. Enable programs.tmux and note config paths created.
  4. Document unfree policy for the lab in README.
  5. Find three packages where attr ≠ binary name.
  6. Use nix-tree or path-info to find heavy dependencies (if available).
  7. Prove rollback restores a removed package.
  8. Write a PR-style commit message for package policy changes.
  9. Avoid with pkgs; for a section; use explicit pkgs. and compare readability.
  10. List tools that should never be system-wide on a shared host.
  11. Prepare HM split: which packages will leave system later?
  12. Search options for a program you use; enable one non-obvious toggle.

Common pitfalls

Symptom Theory
Package name ≠ binary name Check pkgs attr vs meta.mainProgram
Unfree errors allowUnfree policy
Tool missing after rebuild Not in config; was profile-only
Giant system closure Too many packages / wrong layer
Shell not changing users.users.*.shell not set
Double-declared tools programs + systemPackages
Emptying defaultPackages casually Surprising script breakage

Checkpoint

  • Daily tools declared in flake
  • At least one programs.* used
  • Imperative profile not required for lab tools
  • Rough system path size noted
  • Config comments organized
  • Layering (system vs project) explained

Further depth (this book)

Topic Chapter / part
Custom services using pkgs systemd via Nix
Module options Modules as a consumer
Home packages Home Manager chapters
Overlays / packaging Packaging part