// 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
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 processSubstituters (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 ~/.nixnix config show sandboxreturnstrue, so Nix tries to isolate the build--print-deadonly 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-contentschecks store integrity if you suspect damaged pathsdu -sh ~/.nixis 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 withnix profile install nixpkgs#nix-direnvtest -r "$NIX_SSL_CERT_FILE"— certificates are in place, TLS is not broken- a new
fishin the project:echo $APP_ENVis not empty,jq --versionshows 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 = truedoes 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 -dis 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-deadshows 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
Related notes
Nix on CachyOS: install impossible — run it rootless
Nix will not install when the root is read-only, sudo is broken and /nix is baked into ELF. Three limits and a rootless run with mount namespace and chroot
Nix devShell in fish: PATH is silently ignored
devShell packages are not picked up in fish because PATH points inside the mount namespace. How to diagnose the silent fallback and re-exec the shell
nix-direnv: "unshare failed: Operation not permitted"
direnv with use flake fails on unshare failed while dotenv works. I trace the cause (a nested user namespace) and show the fix for the nix wrapper