Skip to content
← All notes

// notes

Nix rootless: build won't start and store hash under GC

Local builds fail with unable to start build process, and a hardcoded nss-cacert path sits on the GC dead list. Two fixes: sandbox and path discovery

Published: 2026-10-176 min read
Nixrootlesssandboxgarbage collectionTLS

Local builds in rootless Nix (an install without a daemon and without root, store in $HOME) fail with error: unable to start build process, even though prebuilt binaries from cache.nixos.org download without errors. The second breakage is quieter: the path to the root certificates is hardcoded and already listed as dead, so the first nix-collect-garbage breaks TLS. Below are both causes, two fixes and a checklist for cleaning the store safely

Context: CachyOS, Nix 2.24.10, an install without a daemon, store in $HOME/.nix. Version numbers depend on the channel, what follows is about the scheme: a rootless install and the wrapper around it

Symptom

The build does not start, the download works

stdenv (the base build environment) arrived from the cache, but the derivation (a recipe for building a package) that had to be built locally did not start

$ nix build .#default --no-link --print-out-paths
copying path '/nix/store/...-stdenv-linux-no-cc' from 'https://cache.nixos.org'...
error: unable to start build process

Substituters (substituting prebuilt binaries from a cache) work fine. Only the derivations that must be built locally break

Certificates on the dead list

The path to the root certificates was written as a constant

# before: the path was hardcoded as a constant
# CACERT=/nix/store/<hash>-nss-cacert-<version>/etc/ssl/certs/ca-bundle.crt
# export NIX_SSL_CERT_FILE="$CACERT"

nix-store --gc --print-dead showed that path among the garbage. The line looks like it works as long as this particular nss-cacert is alive. One run of nix-collect-garbage -d was left before the breakage: after it the certificates would disappear and downloads would fail with a cryptic TLS error

Diagnosis

Five commands for diagnosis

# sandbox mode
nix config show sandbox

# what GC plans to delete, without touching anything
nix-store --gc --print-dead | head -30

# hardcoded store paths in your wrapper
grep -rn "/nix/store/[a-z0-9]\{32\}" ~/.local/bin/ ~/.config/direnv/ ~/.config/fish/

# store integrity and size
nix-store --verify --check-contents
du -sh ~/.nix
  • nix config show sandbox returns true, so Nix tries to isolate the build
  • --print-dead only prints the plan: the nss-cacert path in the output means it will be deleted
  • grep looks for 32-character store hashes in your scripts and in the direnv and fish configs
  • nix-store --verify --check-contents checks store integrity if you suspect damaged paths
  • du -sh ~/.nix is useful for knowing the size before cleaning up

If --print-dead shows paths from your scripts, replace them with a glob or a gcroot first, and only then run the cleanup. An empty grep output means there are no hardcoded hashes in the wrapper

Cause

Build first, then certificates: the two threads run in parallel and never cross

The sandbox needs a nested user namespace

A rootless install without a daemon runs in a single user namespace raised by a wrapper such as nix-enter (the name is an example). To build, Nix tries to create a second, nested namespace, and nesting is forbidden in this configuration. One level works, two do not, so sandbox = true gives up at startup. The install and the wrapper are covered separately: Nix on CachyOS: install impossible — run it rootless

Substituters do not start a build: downloading from a cache needs no sandbox. Hence a simple sign: exactly what must be built locally is what fails

A store hash is the address of a particular build, not a constant

/nix/store/<hash>-<name> is the path of a particular result. It lives as long as a profile, a gcroot or another derivation references it. A hardcoded string does not hold the path itself: the path is deleted once no profile or gcroot references remain

Fix

Build: sandbox = false and an empty build-users-group

# $HOME/.nix/etc/nix/nix.conf
sandbox = false
build-users-group =

build-users-group = is left empty: a rootless install has no nixbld group, and the build runs as the current user. The warning about a missing build users group disappears after this

The price: local builds are not isolated. Code from flake.nix runs with your user's privileges, so untrusted flakes cannot be built this way

Certificates: a glob instead of a constant

CACERT=""
for _c in /nix/store/*-nss-cacert-*/etc/ssl/certs/ca-bundle.crt; do
    [ -r "$_c" ] && CACERT="$_c"
done
[ -n "$CACERT" ] || CACERT=/etc/ssl/certs/ca-certificates.crt
export NIX_SSL_CERT_FILE="$CACERT"

The loop walks every nss-cacert in the store and remembers the last readable file. If none is left, the system bundle is used. The logic is the same in a fish config: walk the glob, then fall back. The found value cannot be cached in the config: the next cleanup may make it dead again, while the glob runs on every shell start and picks the current path

For paths that must live long, a gcroot remains: a separate reference that keeps a store path from being deleted. It requires a step after every rebuild, but does not depend on the current store contents

Check

We check three things: the sandbox mode, that the build runs to the end, and that the certificates are readable

nix config show sandbox          # expect false
nix build .#default --no-link --print-out-paths
test -r "$NIX_SSL_CERT_FILE" && echo "certs ok"

nix config show sandbox must return false, otherwise the local build hits the sandbox again. In nix build, the --no-link flag leaves the result in the store without touching the profile, and --print-out-paths prints the path: if the command ran to the end and returned a path, the build succeeded. test -r checks that NIX_SSL_CERT_FILE points to a readable file, not to a deleted path. After a cleanup the path does not have to match the old one: the glob picks up another nss-cacert instance, or the system bundle if none is left, so test -r matters more than comparing with the old line

Before nix-collect-garbage -d keep in mind: -d deletes old profile generations and you can no longer roll back to them. First look at the plan with nix-store --gc --print-dead and at the history with nix profile history, and when in doubt make a copy: cp -a ~/.nix ~/.nix.bak. The copy takes space, but it restores the store and the profile to the state before the cleanup

Checklist after nix-collect-garbage -d

  • ls ~/.nix/store/*-nix-direnv-*/share/nix-direnv/direnvrc — if empty, restore the package with nix profile install nixpkgs#nix-direnv
  • test -r "$NIX_SSL_CERT_FILE" — certificates are in place, TLS is not broken
  • a new fish in the project: echo $APP_ENV is not empty, jq --version shows the version from the store

The profile is not a concern after the cleanup: the wrapper that raises the namespace takes Nix from the store, not from the profile. What that wrapper is: Nix devShell in fish: PATH is silently ignored

Pitfalls

  • sandbox = true does not block downloads: substituters work with any sandbox setting, so only local build failures point at it
  • A hardcoded store hash does not hold the path itself: /nix/store/<hash>-<name> is deleted once no profile or gcroot references remain. Either a glob or a gcroot
  • nix-collect-garbage -d is irreversible for old profile generations: rolling back to a deleted generation is impossible, so look at the plan before running it
  • Check that a path is dead before the cleanup: --print-dead shows the deletion plan without changing the store
  • Disabling the sandbox widens trust: local builds of untrusted code run with your privileges
  • The same class of bug is hardcoded store paths inside the nix-direnv wrapper: nix-direnv: "minimum required bash version is 4.4"

Takeaway

The two breakages look different but grow from one root: a rootless install lives without a daemon and without the right to isolate. sandbox = false brings local builds back, a glob over the store removes the dependency on a particular hash, and nix-store --gc --print-dead before a cleanup shows what you are about to lose. The wrapper that raises the namespace is covered separately: Nix on CachyOS: install impossible — run it rootless

Need this solved on your project?