Skip to content
Shell & TerminalDeep Dive Published Updated 7 min readViews unavailable

Bash Command Hashing: Why PATH Changes Can Execute an Unexpected Binary

Inspect and control Bash's command-location cache with hash, PATH changes, checkhash, and reproducible diagnostics for stale executables.

When Bash finds an external command by searching PATH, it can remember the full pathname in an internal hash table. Later invocations may use that remembered path instead of searching every directory again. This is normally a performance optimization, but it can surprise an operator who installs a new binary earlier in PATH, removes a cached executable, or expects a running shell to behave like a newly started one. The table stores a pathname, not a copy of the program or its inode: replacing the file at that same pathname normally means the next execution opens the replacement, while adding a higher-precedence executable at a different pathname can leave the old location selected until Bash searches again.

Command hashing is distinct from shell aliases, functions, and builtins. Bash resolves a command name through functions and builtins before searching PATH; only external command locations are relevant to the path cache. A shell’s hash output is therefore a diagnostic view of its current command resolution, not a complete inventory of everything that might execute for a name.

Inspect the cached path

Use the hash builtin to show remembered locations or ask about one command:

hash
hash -t python
type -a python

hash -t name prints the remembered pathname for that name. type -a is useful alongside it because it reports functions, builtins, and PATH candidates. command -v reports how the current shell interprets the command name; it can identify a function or builtin rather than an external file. If the resolved path differs from the executable you intended, inspect the current shell and its PATH before changing system-wide symlinks.

Clear one entry or the entire table

hash -d name forgets a specific remembered location. hash -r clears the table. Assigning a new value to Bash’s PATH variable also clears remembered filenames, according to the Bash Reference Manual. A clean diagnostic sequence is:

printf 'PATH=%s\n' "$PATH"
hash -t tool 2>/dev/null || true
hash -d tool 2>/dev/null || true
command -v tool
type -a tool

The command -v result identifies what the current shell would invoke for the name, while type -a can expose shadowing functions or aliases. In scripts, prefer an explicit absolute path when the executable identity is part of a security or reproducibility requirement. An explicit command name containing a slash is not resolved by searching PATH, so it bypasses this cache; it does not, by itself, prove that the file or its parent directories are trustworthy.

Handle removed or replaced programs

If an executable that Bash has cached is deleted, a later invocation may still try the remembered pathname. The checkhash shell option makes Bash check that a cached command still exists before using it; if it does not, Bash performs a normal PATH search. That option addresses missing cached files, not every possible change to executable contents or trustworthiness.

For a controlled test, create two directories with different versions of a harmless command, put both in PATH, resolve the name from the later directory, then add the command to the earlier directory without changing PATH. The cached path remains the later directory until the cache is cleared. This models a package installation into an already-listed directory: the PATH string did not change, so its automatic invalidation rule did not run. Use a disposable shell so the experiment does not alter an interactive environment. Never test command resolution by placing a malicious or untrusted executable earlier in an administrator’s PATH.

The following fixture creates only a temporary directory and a pair of tiny scripts. Its output should show late before the cache reset and early after it. Review the temporary path and cleanup before adapting it to another environment.

bash --noprofile --norc <<'BASH'
set -eu
tmp=$(mktemp -d "${TMPDIR:-/tmp}/bash-hash.XXXXXX")
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
mkdir "$tmp/early" "$tmp/late"
cat >"$tmp/late/probe" <<'SH'
#!/bin/sh
printf '%s\n' late
SH
chmod +x "$tmp/late/probe"
PATH="$tmp/early:$tmp/late:$PATH"
export PATH
hash -r
printf 'first lookup: '
probe
hash -t probe
cat >"$tmp/early/probe" <<'SH'
#!/bin/sh
printf '%s\n' early
SH
chmod +x "$tmp/early/probe"
printf 'cached lookup: '
probe
hash -t probe
hash -r
printf 'after hash -r: '
probe
BASH

The test intentionally does not assign to PATH after creating the early executable. Doing so would also clear the Bash hash table and hide the condition being demonstrated. The fixture is not a test of ownership, ACLs, mount behavior, or executable integrity; it only makes command-location caching observable.

hash -p filename name can bind a command name to a specified path without a PATH search, and hash -r removes remembered locations. The table is state in a Bash process, not a system-wide executable registry and not a setting written into startup files or exported as an environment variable. There is one important Bash-specific inheritance detail: when Bash starts another Bash instance to execute a shell script found through PATH, the child Bash retains command locations remembered by its parent. Do not assume that a child Bash script starts with an empty table. Unrelated programs and shells do not receive the parent’s table through the environment; they implement their own command lookup.

Shell startup and automation pitfalls

Interactive shells may load startup files that define functions, aliases, or change PATH; non-interactive shells may read a different set of configuration. A command that resolves one way in a terminal can therefore resolve differently under cron, sudo, a service manager, or CI. Log PATH, type -a, hash -t, and the relevant shell invocation when reproducing the issue, and avoid assuming that clearing one interactive hash table fixes other processes. Assigning a new value to PATH clears Bash’s remembered command filenames, even if the new text happens to describe the same directories; by contrast, a package manager creating a new file in a directory already present in the unchanged value need not trigger that reset.

The common failure cases are different and should not be collapsed into “stale binary.” A cached path whose file disappears can fail at execution time; with checkhash enabled, Bash checks for existence and performs a new search when the path is missing. A cached pathname whose file is replaced still names that pathname, so Bash may execute the replacement. A new command installed in an earlier PATH directory is a precedence change that may be invisible to the current cache. Finally, a function, alias, or builtin can shadow every external candidate, in which case the hash table is not the cause at all. type -a, hash -t, file inspection, and a fresh shell separate these conditions.

For security-sensitive automation, use a known environment and absolute executable paths, validate file ownership and permissions, and avoid inheriting a developer’s shell state. Hashing is a cache, not a trust mechanism: a remembered path can still point to a replaced executable if an attacker can modify that file or its directory.

A repeatable resolution check

When a command appears to run the wrong program, check the current shell’s resolution in this order:

  1. Identify the shell and whether a function or alias shadows the command.
  2. Print the effective PATH and inspect directory order and permissions.
  3. Examine hash -t command for a cached full pathname.
  4. Clear the individual cache entry or run hash -r, then resolve again.
  5. Compare with a fresh shell launched under the same environment.

This separates stale command-location state from startup-file drift and PATH shadowing. Bash’s hash table is usually invisible when everything is stable; understanding it makes binary upgrades, shell tests, and incident diagnosis much less mysterious.

For incident records, capture the shell version (bash --version), whether the shell is interactive, the exact PATH, whether checkhash is enabled (shopt -p checkhash), and the output of type -a plus hash -t for the affected name. Do not include secrets that may have been embedded in environment values. If the issue occurs in a service or build runner, reproduce that exact launcher and user instead of comparing it only with a login terminal. This makes the cache one testable variable among the environment, shell startup, filesystem permissions, and executable provenance.

Related:

Sources:

Comments