This line is in a lot of dotfiles, Dockerfiles and CI configs:
export PATH="$PATH:~/.local/bin"
It looks right, and in your own terminal it usually works. But the ~ sits inside double quotes, so the shell never turns it into your home directory. PATH ends up holding the literal string ~/.local/bin, which is a relative path: a directory called ~ inside whatever directory a program happens to be running in. Anyone who can create ./~/.local/bin/ there (a cloned repo, an unpacked archive, a shared upload folder, /tmp) can put a binary in front of the one you meant to run.
Dominik Czarnota (disconnect3d) wrote this up on 2 October; below is a reproduction on two systems, the check, and the places worth grepping.
Why the tilde stays a tilde
Bash only performs tilde expansion on unquoted words. In an assignment it also expands a ~ right after the = or after a :, which is why the unquoted form works:

export PATH=$PATH:~/.local/bin # expands: /home/you/.local/bin
export PATH="$PATH:~/.local/bin" # stays literal: ~/.local/bin
export PATH="$PATH:$HOME/.local/bin" # expands, and is safe to quote
The unquoted version is fragile (a space in a path breaks the assignment), so the fix is the third line: use $HOME, which expands inside double quotes.
Why you never noticed: interactive bash covers for it
We planted an executable at ./~/.local/bin/whoru in a temp directory, set PATH with the quoted tilde, and asked different programs to run whoru. On Debian bookworm (bash 5.2.15) in Docker:
D=$(mktemp -d)
mkdir -p "$D/~/.local/bin"
printf '#!/bin/sh\necho PLANTED\n' > "$D/~/.local/bin/whoru"
chmod +x "$D/~/.local/bin/whoru"
cd "$D"
export PATH="/usr/local/bin:/usr/bin:/bin:~/.local/bin"
bash -c whoru
bash --posix -c whoru
sh -c whoru
zsh -c whoru
env whoru
python3 -c 'import subprocess; subprocess.run(["whoru"])'
bash -c whoru bash: line 1: whoru: command not found
bash --posix -c whoru PLANTED
sh -c whoru (dash) PLANTED
zsh -c whoru PLANTED
env whoru PLANTED
python3 subprocess PLANTED
Plain bash looks the command up in $HOME/.local/bin, because it expands a leading ~ in PATH entries when it searches. So the commands you type keep working and the bug stays hidden. Everything else takes the entry literally: /bin/sh on Debian and Ubuntu (dash), which is what make, system() and many scripts use; zsh; bash in POSIX mode; and anything that calls execvp directly, such as env, Python's subprocess and most language runtimes. On macOS with the stock bash 3.2 we saw the same split: bash and /bin/sh found nothing, while zsh, env and Python ran the planted file. Python's shutil.which("whoru") even returned ~/.local/bin/whoru.
The practical result: a tool you start from your shell can run a different binary than the one you get when you type the same name yourself.
The one-line check
From the original write-up:

echo "$PATH" | tr ':' '\n' | grep '~'
No output means no literal tilde. While you are there, also look for empty entries and ., which have the same effect (the current directory is searched):
echo "$PATH" | tr ':' '\n' | grep -n -v '^/'
That prints every entry that does not start with /: empty entries, ., a literal ~ and any other relative directory. Each line it shows is searched relative to wherever a program runs.



