How it works
The explanation, with examples you can run.
Shell variables versus environment variables
GREETING=hi; bash -c 'echo "child sees: [$GREETING]"'export GREETING; bash -c 'echo "child sees: [$GREETING]"'GREETING=once bash -c 'echo "child sees: [$GREETING]"'env -i /usr/bin/envchild sees: []child sees: [hi]child sees: [once]A plain assignment creates a shell variable that stays in the shell. export marks it for the environment of every later child. NAME=value command sets it for that one command only. env -i starts a program with an empty environment, so env prints nothing at all. A running process's environment, as it was at exec, is visible in /proc/<pid>/environ (NUL-separated, readable by the owner and root), which is one reason not to put secrets in environment variables on shared machines.
How PATH lookup works
echo "$PATH"type -a ls cdmkdir -p /tmp/bin && printf '#!/bin/sh\necho fake ls\n' > /tmp/bin/ls && chmod +x /tmp/bin/lsPATH=/tmp/bin:$PATH ls/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binls is /usr/bin/lsls is /bin/lscd is a shell builtinfake lsWhen a command name has no slash, the shell first checks its own aliases, functions and builtins (cd must be a builtin because a child process can't change its parent's directory), then tries each PATH directory in order and runs the first executable it finds. ls appears twice because on Ubuntu /bin is a symlink to /usr/bin. Putting /tmp/bin first made our fake ls win, which is exactly why a writable directory early in PATH, or an empty entry (::, which means the current directory), is a security hole.
Bash remembers where it found each command (hash lists them). If you install a program earlier in PATH and the shell still runs the old one, hash -r clears that cache.
Why the environment changes under services
Interactive shells build their environment from startup files (/etc/profile, ~/.bashrc, ~/.zshrc), which is where version managers and export PATH=... lines live. Non-interactive starters don't run them:
- cron uses a short default PATH (often just
/usr/bin:/bin) and few other variables. - systemd gives services a clean environment; set variables with
Environment=orEnvironmentFile=in the unit. - Containers get the image's
ENVvalues plus whateverdocker run -eor Kubernetes adds. - sudo resets most of the environment by default (
env_reset) and uses its ownsecure_path.
The fix is the same everywhere: use absolute paths in scripts, or set PATH and the variables the program needs where the service is defined.
On macOS
The mechanics are identical. The default PATH comes from /etc/paths and /etc/paths.d/ via path_helper, Homebrew adds /opt/homebrew/bin on Apple silicon, and the default shell is zsh, which reads ~/.zshrc rather than ~/.bashrc. GUI apps started by launchd do not see variables exported in your shell files.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.