Skip to content

[Bug]: install.sh PATH check false-negatives on long PATH (grep -q under pipefail), triggering sudo prompt and duplicate rc entries #3460

Description

@Mjparker-toolate

What happened?

Running sh/cli/install.sh on a machine where ~/.local/bin and ~/.bun/bin were already on PATH (both exported in ~/.zshrc) and bun 1.4.2 was already installed:

[spawn] Installing spawn via bun...
[spawn] Downloading pre-built CLI binary...
[spawn] Installed spawn to /Users/me/.local/bin/spawn
[spawn] Adding spawn to /usr/local/bin (may require your password)...
sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper
sudo: a password is required

spawn v1.1.1
...
[spawn] To start using spawn, run:

    exec $SHELL

So the installer (1) decided neither directory was on PATH, (2) fell through to the sudo ln -sf path in ensure_in_path() and prompted for a password, and (3) appended two redundant blocks to ~/.zshrc:

# >>> spawn >>>
export PATH="/Users/me/.local/bin:$PATH"
# <<< spawn <<<

# >>> spawn >>>
export PATH="/Users/me/.bun/bin:$PATH"
# <<< spawn <<<

Root cause

sh/cli/install.sh sets set -eo pipefail (line 12) and then tests PATH membership with a pipeline into grep -q (lines 172 and 175):

if echo "${_SPAWN_ORIG_PATH}" | tr ':' '\n' | grep -qxF "${install_dir}"; then

grep -q exits as soon as it finds a match. If PATH is longer than the pipe buffer, tr (or echo) is still writing when grep exits, receives SIGPIPE, and exits 141. With pipefail the whole pipeline is then reported as failed, so the if takes the false branch even though the directory is on PATH. The outcome is a race, so it is nondeterministic.

Reproduction with the exact lines from the script, under /bin/bash (bash 3.2.57(1)-release), PATH of 131 entries / ~20 KB, directory present at position 4:

bash -c '
set -eo pipefail
install_dir="${HOME}/.local/bin"
_SPAWN_ORIG_PATH="${PATH}"
ok=0; fail=0
for i in $(seq 1 20); do
  spawn_in_path=false
  if echo "${_SPAWN_ORIG_PATH}" | tr ":" "\n" | grep -qxF "${install_dir}"; then spawn_in_path=true; fi
  [ "$spawn_in_path" = true ] && ok=$((ok+1)) || fail=$((fail+1))
done
echo "true: $ok  false: $fail"
'
# → true: 2  false: 18

With a short PATH (a few entries) the same loop is 20/20 true, because the whole of tr's output fits in the pipe buffer before grep exits. Long PATHs are common for people running the installer from IDE-spawned shells, dev containers, or agent harnesses that prepend many tool directories.

This is the same bug class as #2786 (grep inside a pipeline under set -eo pipefail), just triggered by SIGPIPE on early exit rather than by exit 1 on no match.

Secondary issue that turns the false negative into rc-file pollution

Once the PATH check fails, _patch_rc (line 239) decides whether to append by grepping the rc file for the expanded absolute path:

if ! grep -qF "${dir}" "$rc_file" 2>/dev/null; then

An rc file that already exports the directory as $HOME/.local/bin or $BUN_INSTALL/bin (the form bun's own installer writes) does not contain the literal /Users/me/.local/bin, so the guard passes and a duplicate block is appended. (After that first append the literal path is present and later runs do not add more, so the damage is one redundant block per directory.)

Suggested fix

Drop the pipeline and test membership with a bash pattern match, which is exact, spawns nothing, and cannot interact with pipefail:

case ":${_SPAWN_ORIG_PATH}:" in
    *":${install_dir}:"*) spawn_in_path=true ;;
esac
case ":${_SPAWN_ORIG_PATH}:" in
    *":${bun_bin_dir}:"*) bun_in_path=true ;;
esac

For _patch_rc, check for the marker comment (grep -qF "$marker_start") and/or accept the $HOME/... and ~/... spellings before appending.

Environment

  • macOS 26.6.2 (arm64), /bin/bash = bash 3.2.57(1)-release, zsh login shell
  • bun 1.4.2 already installed at ~/.bun/bin/bun
  • spawn CLI 1.1.1 (cli-latest asset, sha256 21d6cc49…)
  • install.sh as served from https://openrouter.ai/labs/spawn/cli/install.sh, byte-identical to sh/cli/install.sh at 4b62e87

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions