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
What happened?
Running
sh/cli/install.shon a machine where~/.local/binand~/.bun/binwere already on PATH (both exported in~/.zshrc) and bun 1.4.2 was already installed:So the installer (1) decided neither directory was on PATH, (2) fell through to the
sudo ln -sfpath inensure_in_path()and prompted for a password, and (3) appended two redundant blocks to~/.zshrc:Root cause
sh/cli/install.shsetsset -eo pipefail(line 12) and then tests PATH membership with a pipeline intogrep -q(lines 172 and 175):grep -qexits as soon as it finds a match. If PATH is longer than the pipe buffer,tr(orecho) is still writing whengrepexits, receives SIGPIPE, and exits 141. Withpipefailthe whole pipeline is then reported as failed, so theiftakes 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: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 beforegrepexits. 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 (
grepinside a pipeline underset -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:An rc file that already exports the directory as
$HOME/.local/binor$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:For
_patch_rc, check for the marker comment (grep -qF "$marker_start") and/or accept the$HOME/...and~/...spellings before appending.Environment
/bin/bash= bash 3.2.57(1)-release, zsh login shell~/.bun/bin/buncli-latestasset, sha25621d6cc49…)install.shas served from https://openrouter.ai/labs/spawn/cli/install.sh, byte-identical tosh/cli/install.shat 4b62e87