// notes
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
direnv exec . in a project with use flake fails with unshare: unshare failed: Operation not permitted, while variables from .env are picked up. unshare creates namespaces (isolated process environments), and the failure happens on the second one. Below: how to find the cause and fix the nix wrapper
Context: rootless Nix, direnv and nix-direnv in a project whose .envrc contains use flake
Symptom
Reproduces inside a project with an .envrc that contains use flake
direnv: loading $HOME/projects/demo/.envrc
direnv: using flake
direnv: nix-direnv: cache invalidated: profile (…/.direnv/flake-profile-…) does not exist
unshare: unshare failed: Operation not permitted
direnv: nix-direnv: Evaluating current devShell failed. Falling back to previous environment!The dotenv part still works: simple variables (APP_ENV and the like) are set, and only the flake branch fails
Diagnosis
The first thing that confuses you: unshare failed does not say which process called unshare (the utility that creates a namespace). Searching the direnv configs and $HOME/.local/bin narrows it down quickly, but the exact answer comes from intercepting the binary: replace unshare with a logging script, put its directory first in PATH, and the caller shows up in the log. The technique is covered separately: How to find out who calls unshare
The interception showed thatunsharewas called by my own wrapper aroundnix. It raises a namespace and then, inside the one already raised, tries to create a second
Outside, the wrapper raises namespace #1. Inside it, nix-direnv calls nix, the wrapper raises a namespace again, and creating a nested namespace fails
The host where this reproduced blocks nested user namespaces as a matter of policy. In Linux such namespaces are generally allowed, so this is specific to that system
Cause
Let us define the terms. A user namespace isolates users and their privileges; a mount namespace isolates mount points. Rootless Nix uses them to mount /nix without root. A nested user namespace is created inside an existing one
The first unshare --mount --map-root-user succeeds: user and mount namespaces are created and the user is mapped to root. The second one, from inside namespace #1, is rejected by the kernel with EPERM. You can check the host policy like this:
sysctl user.max_user_namespaces
sysctl kernel.apparmor_restrict_unprivileged_userns
sysctl kernel.unprivileged_userns_cloneValues that forbid nested user namespaces: 0 for user.max_user_namespaces or kernel.unprivileged_userns_clone, and 1 for kernel.apparmor_restrict_unprivileged_userns. The set and presence of the keys depend on the distribution and kernel: kernel.unprivileged_userns_clone exists in Debian and derivatives, kernel.apparmor_restrict_unprivileged_userns only on systems with AppArmor, and user.max_user_namespaces is a general kernel counter. On the machine checked, for example, kernel.apparmor_restrict_unprivileged_userns was absent from /proc/sys and sysctl returned a cannot stat ... No such file or directory error
Any wrapper that raises a namespace unconditionally breaks inside one. That is the root cause
A similar but different problem: when the devShell PATH is silently not picked up in fish. Write-up: Nix devShell silently loses packages in fish
Fix
The wrapper around nix must be namespace-aware: if /nix/store is already mounted, call nix directly, without unshare
# $HOME/.local/bin/direnv-shims/nix
if [ -d /nix/store ]; then
# We are already inside the namespace, so nix can be called directly
for _d in "$HOME"/.nix/store/*-nix-[0-9]*; do
case "$_d" in *.drv) continue;; esac
[ -x "$_d/bin/nix" ] && exec "$_d/bin/nix" "$@"
done
fi
# Outside, raise the namespace
exec "$HOME/.local/bin/nix-enter" nix "$@"What happens here. If the store is mounted ([ -d /nix/store ]), the wrapper looks for an installed nix under $HOME/.nix/store and starts the first binary it finds via exec. Directories ending in *.drv are skipped: those are derivation descriptions, not installed packages. If the store is not mounted, the wrapper calls nix-enter and raises the namespace
The names nix-enter and direnv-shims/nix are examples; your wrapper may be named differently. What matters is the logic: check /nix/store before trying to create a namespace
Second step: run direnv itself entirely inside the namespace ($HOME/.local/bin/nix-enter-direnv). That is needed because nix-direnv uses hard-coded absolute paths under /nix/store/..., which do not exist outside. For the hard-coded paths and the bogus bash version error: nix-direnv: "minimum required bash version is 4.4"
Verify
There is only one reliable marker of "we are inside the namespace": a mounted store
test -d /nix/store && echo inside || echo outsideDo not rely on the $NIX_STORE_DIR variable: it can be set from outside, and then the check lies
Then rebuild the environment from scratch:
rm -rf .direnv && direnv allowIf the flake loads without unshare failed, the wrapper no longer creates a nested namespace
Pitfalls
- The symptom blames the wrong culprit.
unshare faileddoes not name the process; without intercepting the binary the search is blind - `/nix/store/...` paths in `PATH` silently do not work outside:
command -v jqreturns/usr/bin/jq. That is why it is worth keeping the whole interactive shell inside the namespace, not justdirenv
Takeaway
unshare failed with direnv and nix-direnv means the nix wrapper is trying to create a second user namespace. Checking /nix/store before raising a namespace and running direnv inside it remove both the error and the bogus version diagnostics
If the error persists, check that the wrapper is not overridden elsewhere in PATH: for example, a second copy or a shell function with the same name in $HOME/.local/bin
Related notes
nix-direnv: "minimum required bash version is 4.4"
nix-direnv fails with "minimum required bash version is 4.4" even though bash is newer. I break down the hard-coded /nix/store paths, the cause, and the fix
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
Who calls unshare: intercepting a binary through PATH
A command fails without naming the culprit. Here is the trick: replace the binary with a logging script via PATH and find the caller without tracing or root