Did your last agent run pick the wrong python?
I see this failure more than failed tests.
The chat looks green after a version print.
Then CI executes an entirely different local binary.
Why does that split keep happening on scratch hosts?
Because PATH is only a search story, not truth.
The agent tells one story and your laptop tells another.
A disposable host will tell a third story.
This FAQ kills five PATH myths with commands.
It is a court, not a vibe check.
Why these myths keep shipping
Agents inherit a login shell, or they do not.
They prepend a user bin directory, or they do not.
They call npm from a hashed path after you install another copy.
Does that hashed npm path sound familiar yet?
Then the next tool call starts a cold shell.
Your export then died with the previous process.
I do not treat a version line as identity.
I treat PATH as guilty until a digest says otherwise.
Myth 1: which printed a path, so we are safe
The which command is not a kernel contract.
It only walks the current PATH list today.
It ignores the shell hash table in bash.
It can miss aliases, functions, and wrapper shims.
Which process will actually exec that name?
type and command -v beat which for this.
Then you still need the real file digest.
# proposal: run on the agent host, not your laptop
set -euo pipefail
echo "PATH=$PATH"
type -a python3 || true
command -v python3
python3 -c 'import sys; print(sys.executable)'
If those three lines disagree, you must stop.
Do not ship a maybe-python into code review.
Quick checks I actually run
- Print PATH at the top of the step.
- Prefer type -a over the which command.
- Prefer command -v over a chat summary.
- Refuse to continue when those four answers diverge.
Myth 2: the README install put it on PATH
READMEs assume an interactive human at a prompt.
Your coding agent is not that interactive human.
A line like add ~/.local/bin never ran in this shell.
The installer only wrote new files under HOME.
The current shell never sourced the new profile.
Did the agent start a login shell after install?
In practice that login shell never even started.
Corrected model: mutate PATH in the same session.
Persist the export inside your actual runbook.
Prove the next command sees the new directory.
# proposal: pin the install dir, then export
export PATH="$HOME/.local/bin:$PATH"
hash -r
command -v ruff
type -a ruff
No export means no proof for later steps.
A later step will resurrect the old binary.
Myth 3: version output means we have the tool
Agents love tool --version because it looks official.
It is not identity; it is only a string.
Two binaries can print the same marketing line.
Wrappers can proxy and shims can delay the real exec.
What inode did you just execute on disk?
Resolve the path, then fingerprint the file.
# proposal: identity over slogans
bin=$(command -v node)
printf 'bin=%s\n' "$bin"
ls -l "$bin"
readlink -f "$bin" 2>/dev/null || true
sha256sum "$bin" 2>/dev/null || shasum -a 256 "$bin"
"$bin" -e 'console.log(process.execPath)'
Keep the digest next to the command log.
A version line is not a digest, period.
Myth 4: the next agent step uses the same PATH
Each tool call can be a brand new shell.
Some hosts reuse one process, but many do not.
Your export PATH then died with that process.
The next call hashed the old python again.
Have you printed PATH at the start of every call?
If not, you guessed across those process boundaries.
Corrected model: treat every call as a cold shell.
Re-export PATH, run hash -r, and resolve again.
# proposal: preamble for every agent command
export PATH="$HOME/.local/bin:/usr/bin:/bin"
hash -r
command -v python3
python3 -c 'import sys; print(sys.executable, sys.version)'
Put that preamble in the runbook file.
Do not leave it only inside the prompt text.
Myth 5: nvm or pyenv will fix the non-interactive shell
Version managers hook interactive rc files by default.
Scratch agent shells are often fully non-interactive processes.
Those hooks never ran, so the shim is missing.
Did you source the manager in this exact process?
If you did not, the default system binary wins.
# proposal: do not assume nvm is alive
if [[ -n "${NVM_DIR:-}" && -s "$NVM_DIR/nvm.sh" ]]; then
# shellcheck disable=SC1090
. "$NVM_DIR/nvm.sh"
fi
type -a node
command -v node
Better: stop using a manager on a scratch host.
Install one pinned binary and put it first on PATH.
Then fingerprint it; that is the whole trick.
How bash hash actually bites you
After the first successful lookup, bash caches the path.
A later install in ~/.local/bin does not update that cache.
You run the old binary and thank the new installer.
Is hash -r optional on an agent host?
No, hash -r is the gap between theater and a new exec.
# proposal: demonstrate the trap, then clear it
type python3
hash -t python3 2>/dev/null || true
export PATH="$HOME/.local/bin:$PATH"
# still cached?
type python3
hash -r
type python3
That sequence is the whole lesson.
Install, then forget hash -r, and you tested nothing.
The artifact: a PATH court you can rerun
Do not argue with the model about tools.
Interrogate the host with a boring court script.
I keep a court file for this, so copy it.
Treat it as a proposal until you execute it.
#!/usr/bin/env bash
# path-court.sh — proposal, run on the scratch host
set -euo pipefail
names=("python3" "node" "npm" "git" "pip")
echo "== host =="
hostname || true
echo "shell=${SHELL:-unknown}"
echo "uid=$(id -u)"
echo "PATH=${PATH:-}"
echo
echo "== hash table =="
hash 2>/dev/null || echo "(empty or unsupported)"
echo
for n in "${names[@]}"; do
echo "== name: $n =="
if type -a "$n" 2>/dev/null; then
:
else
echo "type: missing"
fi
cv=$(command -v "$n" || true)
if [[ -z "${cv}" ]]; then
echo "verdict: ABSENT"
echo
continue
fi
echo "command -v: $cv"
if [[ -e "$cv" ]]; then
ls -l "$cv"
readlink -f "$cv" 2>/dev/null || true
if command -v sha256sum >/dev/null; then
sha256sum "$cv"
else
shasum -a 256 "$cv"
fi
else
echo "note: not a filesystem path"
fi
echo "verdict: PRESENT"
echo
done
Run it twice, once before the install step.
Then run it again after the install step.
If the digest did not change, the install was theater.
If PATH changed but the digest did not, you still lost.
Decision table
| Symptom | Likely myth | What to run | Ship? |
|---|---|---|---|
| which found it, CI did not | Myth 1 | type -a and command -v | No |
| installer succeeded, next step missing | Myth 2 | echo PATH, then hash -r | No |
| --version matches, behavior does not | Myth 3 | sha256sum and readlink | No |
| step three used the old binary | Myth 4 | print PATH every call | No |
| nvm worked on your laptop only | Myth 5 | source hook or pin a binary | No |
That decision table is the actual review gate.
Plain chat text is not a review gate.
A debugging workflow I actually follow
- Freeze the command under test; do not let the model rewrite it.
- Print PATH, PWD, and id at the first line.
- Resolve the binary with type and command -v.
- Fingerprint the file before you trust --version.
- Repeat the same four prints after every install.
- Repeat them again in the next tool call.
- Only then look at tests or diffs.
The model can help you type the commands.
It cannot substitute for those seven host prints.
Where a cold box actually helps
I do not want this audit on my laptop PATH.
That PATH is already contaminated by my own tools.
I want a cold box and a model that can run the script.
I also want a log a human can read.
Then a human reads the digest; the chat does not decide.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode's free model access and free server option fit that lab loop.
The host is disposable, so laptop PATH cannot leak in.
The model can invoke path-court.sh and paste the verdict.
I will not claim quotas, hardware, or model names here.
Those change, but PATH search order does not.
Use the box as a lab, not as production.
If you need one cold path-court.sh run, that free server is enough.
What this does not prove
A matching digest is not a supply-chain review.
sha256sum only pins the file you executed today.
PATH hygiene still does not replace your lockfiles.
It also does not replace container image digests.
This audit does not replace a real SBOM.
The court script skips Windows shells on purpose.
It skips images with no sha256sum and no shasum.
It skips names that exist only as aliases.
If your agent uses a non-POSIX shell, rewrite the preamble.
If the host is a tiny busybox, drop bash arrays.
Who should not use this approach
Skip this if you already boot from a pinned image.
Your PATH is the image, and that is better.
Skip this if you cannot execute anything on the host.
Logs without an exec are still only stories.
Skip this if the task is one local script on your machine.
Your laptop PATH is then the actual product.
Do not use a public free server when secrets are in env.
PATH audits need no tokens and no credentials.
The mental model I actually keep
The agent did not find a tool for you.
It only searched a colon-separated PATH string list.
That PATH list dies with the process immediately.
The hash table then lies after new installs.
Tool version flags are only marketing copy here.
So I ask four questions on every run:
- What PATH did this process actually see?
- What file did command -v actually return here?
- What digest is that file on disk?
- Will the next process see the same PATH?
If any answer is missing, the run is not done.
Is that slower than trusting the chat window?
Yes, and that slower path is the point.
Trusting the chat is how the wrong python shipped.
Print PATH, fingerprint the binary, then ship.
Top comments (0)