🚀 New: chi (χ) — an open-source autoresearch harness for fleets of LLM coding agents. Read the announcement.

A Quoted ~ in PATH Works in Bash and Silently Becomes a Relative Path Everywhere Else

A literal ~ in PATH resolves to your home directory in interactive Bash but to ./~ in env, sh, make, xargs and Python subprocess. I reproduced it on Bash 5.2 and dash, and here is how to find and fix it.

Contents

Put export PATH="$PATH:~/.local/bin" in your .bashrc and everything appears to work. kek resolves, which agrees, you move on. The environment variable now contains a literal tilde character, though, and that is the thing every child process inherits.

Disconnect3d described the problem and framed it as a quoted tilde not being expanded. I reproduced it, and the Bash result is more interesting than “it doesn’t work”: in Bash it does work, which is exactly why the bug survives.

What I ran

Setup, on Linux with Bash 5.2.21 and dash from Ubuntu. A fake HOME contains an executable kek that prints REAL-home. A working directory contains ./~/.local/bin/kek that prints PLANTED-cwd. Then I set PATH with a literal tilde and ran kek through different launchers:

export PATH="/usr/bin:/bin:~/.local/bin"
kek                 # run from Bash
env kek
sh -c kek
echo | xargs kek
python3 -c 'import subprocess; subprocess.run(["kek"])'
printf 't:\n\tkek\n' | make -f -
LauncherWhat ran
kek typed in BashREAL-home
env kekPLANTED-cwd
sh -c kek (dash)PLANTED-cwd
xargs kekPLANTED-cwd
Python subprocess.run(["kek"])PLANTED-cwd
make recipePLANTED-cwd

In every case echo "$PATH" printed ~/.local/bin with the tilde intact. The variable never changed. What differed is who did the lookup.

Why Bash hides it

Assignment is the first place a tilde can be expanded, and the quotes prevent that. Unquoted, export PATH=$PATH:~/.local/bin is expanded at assignment time in both Bash and dash, because the Bash manual says tilde-prefixes are checked after the first = and after any : in an assignment. I confirmed that: the printed PATH contained the absolute home path and everything resolved correctly.

Quoting turns that off, so the value stays ~/.local/bin. A program launching a child normally hands PATH to execvp or an equivalent, which treats each colon-separated entry as a plain directory. ~ is a valid directory name, so the entry means ./~/.local/bin, relative to the current directory. That is what env, dash, xargs, Python and make did above.

Bash is the exception because its own command lookup expands a leading tilde in each PATH element before searching. My run is consistent with that: type kek in Bash reported the absolute home path, while env kek found the planted file. I did not read Bash source for this, so treat the mechanism as the explanation that fits the output, not a cited guarantee. The behavior, on 5.2.21, is what I measured.

One more datapoint: the system which binary on this machine, run under the same PATH, printed ~/.local/bin/kek. That is a relative path resolved from the current directory, so even the tool you use to debug the problem can mislead you in the opposite direction from type.

Why this is a security problem, not only a nuisance

A relative PATH entry means the directory you are standing in influences which binary runs. Anyone who can write ./~/.local/bin/<name> in a directory you work in, such as a cloned repository or an unpacked archive, can have it run when a tool resolves <name> there. The source post does not spell out an attack, so this is my reading of the demo, and I only tested it with files I created myself.

It also produces the more ordinary failure: a script that works from your terminal and fails in cron, a Makefile, a CI step or a Python tool that shells out, with “command not found” and no hint that the interactive shell was covering for a bad PATH.

Finding it

Check the environment, not the shell’s lookup:

echo "$PATH" | tr ':' '\n' | grep -n '~'

Any output is a literal tilde. Relative entries more generally, such as an empty element or ., are the same class of problem. Test with env rather than a bare command, since env does not get Bash’s help:

env sh -c 'command -v kek'

Fix

Use $HOME and keep it quoted:

export PATH="$PATH:$HOME/.local/bin"

$HOME is expanded even inside double quotes, so a space in the path cannot break the assignment, which is the other fragility the original post notes about the unquoted tilde form. Check files other than .bashrc too: .profile, Dockerfile ENV PATH= lines and CI config are all places the same string can be copied into.

I did not test zsh, since it was not installed here. The source post’s demo, which appears to be from macOS judging by its ls output, shows a quoted tilde failing to find the binary. That fits a shell without Bash’s lookup expansion, but the post does not say which shell it used, and I have not checked.

Sources