Skip to content
← All notes

// notes

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

Published: 2026-10-084 min read
LinuxdirenvNixdebugging

A command fails, but the message does not name the process that called it. In that situation a simple trick helps: replace the binary with a logging script and put it first in PATH (the list of directories where the shell looks for commands). I use unshare as the example, but the method works for any command

Problem: a message with no culprit

A classic scenario: direnv exec . in a project with use flake fails with unshare: unshare failed: Operation not permitted. The error is printed by unshare, but who called it is not stated. It could be nix-direnv (direnv's plugin for Nix environments), a wrapper around nix, a direnv hook, or something else

Searching configs by hand can take a while. You need a way to catch the call at the moment it happens

The error names the tool, not the caller

Why strace is not always a good fit

strace (a system call tracer) with -e trace=execve will show every process launch, including unshare. The downside is the volume: the call you want drowns in output, and ptrace settings (the mechanism a tracer uses to attach to a process) do not always allow it. For a quick search, intercepting one command is enough

Cause: interception through PATH

The shell searches the PATH directories left to right and runs the first executable file with a matching name. So a directory with your own script at the front of PATH gets the call before the real binary. The trick was needed because the error is printed by the tool (unshare), not by whoever called it. In the case I traced the culprit was a wrapper around nix that raised the namespace a second time: nix-direnv: "unshare failed: Operation not permitted"

The trick: replace the binary

PATH resolves a command by name: the shell takes the first executable file with a matching name. So you only need to put your own unshare script in a separate directory and put that directory at the front of PATH

All commands below run in the same terminal

HOOKDIR="$(mktemp -d)"
cat > "$HOOKDIR/unshare" <<EOF
#!/bin/sh
echo "UNSHARE CALLED: \$@" >> "$HOOKDIR/unshare.log"
exit 1
EOF
chmod +x "$HOOKDIR/unshare"
export PATH="$HOOKDIR:$PATH"
direnv exec . true
cat "$HOOKDIR/unshare.log"

We do not quote EOF: that way $HOOKDIR is expanded when the script is created, and the log gets an absolute path. \$@ is escaped so it stays in the script and is expanded when the script runs

The script writes the arguments to the log and exits with exit 1. This imitates a failure rather than reproducing the original EPERM: the call does not continue down the chain, and the log records who made it and with which arguments

After debugging, restore the environment:

rm -rf "$HOOKDIR"

Verify: reading the log

One caveat up front: the log shows the call arguments, not the process name. The caller is what we look for next, in your own directories

grep -rn "unshare" "$HOME"/.local/bin/ "$HOME"/.config/direnv/ 2>/dev/null

The log will contain a line like UNSHARE CALLED: --mount --map-root-user .... The arguments show how the namespace is raised (an isolated environment at the kernel level). The call itself narrows the search: next, find who could have called unshare

In my case the log showed my own wrapper around nix, which called unshare a second time. Another similar case, the silent PATH substitution from the devShell in fish, is covered in Nix devShell silently loses packages in fish

You can confirm the culprit by re-running: remove the interception, fix the place you found, and repeat the failing scenario. If the unshare failed error is gone, that was the caller

Pitfalls

  • It only catches calls by name through PATH. A command invoked by an absolute path (/usr/bin/unshare) bypasses the substitution
  • It does not work if the name is taken by a shell function or alias: the shell runs those instead of the binary from PATH
  • It does not see calls with a cleared environment (env -i) or launches through sudo: by default sudo resets PATH (env_reset)
  • It does not show already running processes and is no replacement for strace or system auditing

Takeaway

The trick needs no root: replace the command with a logging script, put its directory first in PATH, reproduce the failure, and read the log. Once the culprit is found, you fix one specific place instead of the whole environment

Need this solved on your project?