Skip to content

Installer Shell Behavior

Most developer tools ship a curl | sh or curl | bash installer that runs in a subprocess disconnected from your interactive zsh session. These installers read their own shell's profile files, modify PATH entries, and write configuration to locations determined by the environment at the time the script runs — not the environment you see in your terminal. Understanding which shell each installer spawns, and what environment it inherits, is the difference between a clean install and an hour of debugging PATH confusion.

Installer audit

The following table documents every installer in this framework's toolchain, its shell requirements, and the profile files it modifies:

Tool Shell Invocation Modifies Behavior
mise — (not used) vendored bin/mise Nothing (binary only) This stack does not run mise's curl \| sh installer at all: bin/mise in the repo is generated by mise generate bootstrap, version-pinned, and fetches its own checksummed binary into the cache on first run. Activation is wired in conf.d/70-tools.zsh.
rv — (not used) mise github: backend Nothing (binary only) Installed declaratively ("github:spinel-coop/rv" in [tools]), so its curl \| sh installer is gone too. Activation via eval "$(rv shell zsh)" wired in conf.d/70-tools.zsh.
Homebrew /bin/bash /bin/bash -c ... Nothing (prints instructions) Requires bash explicitly. Prints eval "$(brew shellenv)", but you do not need it: conf.d/10-path.zsh adds /opt/homebrew/{bin,sbin} (Apple Silicon) directly, and /usr/local/bin (Intel) is in /etc/paths already.
rustup sh curl \| sh ~/.profile, ~/.bashrc, ~/.zshenv Writes CARGO_HOME/bin to PATH in all detected profiles. Reads CARGO_HOME, RUSTUP_HOME.
nvm bash curl \| bash ~/.bashrc, ~/.bash_profile, ~/.zshrc, ~/.profile Appends NVM_DIR and source lines to the first profile found (selection overridable via $PROFILE).
uv sh curl \| sh Nothing (binary only) mise manages uv in this stack; standalone installs via astral.sh/uv/install.sh.
bun bash curl \| bash ~/.bashrc, ~/.zshrc mise manages bun in this stack; standalone adds BUN_INSTALL to PATH in detected profiles.

Safe invocation patterns

The goal is to ensure that when an installer subprocess spawns, it inherits the correct XDG_* variables, CARGO_HOME, and PATH — so tools are installed to the right locations and the installer's profile modifications target the correct files. The POSIX profile handles the common case. These patterns address edge cases and override situations.

mise

This framework never runs mise's network installer. The repository vendors bin/mise — a script produced by mise generate bootstrap that pins an exact mise version, downloads that release with checksum verification into the cache on first invocation, and then executes it:

# The only mise "install" step in this stack:
~/development/personal/repos/dotfiles/bin/mise bootstrap

For reference, the upstream installer (curl https://mise.run | sh) downloads a binary to ~/.local/bin/mise and does not modify profile files, but its shell-specific endpoints (curl https://mise.run/zsh | sh) do add an activation line to the detected shell config. If you ever use it outside this framework, use the base form only — here, shell activation is managed in conf.d/70-tools.zsh as the single source of truth.

rv

rv is declared in [tools] as "github:spinel-coop/rv", so mise installs it from GitHub releases like any other tool — its curl | sh installer is not used either. The rv shell zsh activation block is evaled from conf.d/70-tools.zsh, already wired in by this framework.

# Verify (after mise bootstrap / mise install)
rv --version

Homebrew

Homebrew's installer is hardcoded to /bin/bash -c "$(curl ...)". It does not modify any profile files — it prints post-install instructions and expects you to add eval "$(brew shellenv)" to your profile manually.

You do not need the eval line on either architecture — the framework's conf.d/10-path.zsh puts Homebrew on PATH for you:

  • Intel: Homebrew's /usr/local/bin is in the default /etc/paths (and 10-path.zsh also appends it), so brew is on PATH via path_helper regardless.
  • Apple Silicon: /opt/homebrew/bin is NOT in /etc/paths, so path_helper would miss it — but 10-path.zsh prepends /opt/homebrew/bin and /opt/homebrew/sbin directly (guarded by a directory test, so it is a no-op on machines without them). It sits below the version-managed tool dirs (mise shims win) and above the bare system bins.

The framework adds the prefix directly rather than running eval "$(brew shellenv)", to keep PATH construction deterministic and avoid a subprocess on every shell start. Verify with which brew after installation.

On Linux, use apt (Debian/Ubuntu) or dnf (Fedora/RHEL) for system tools. The distribution package manager is the natural choice — better integrated, faster, and one fewer dependency to maintain. Homebrew on Linux exists but adds complexity without meaningful benefit when the same packages are available natively.

# Standard Homebrew installation (macOS only; no special flags needed).
# This spawns /bin/bash regardless of your default shell.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Verify brew is on PATH (should be via /etc/paths on macOS)
which brew

# If `which brew` fails (non-standard prefix on macOS):
# Add to conf.d/70-tools.zsh:
#   if [[ -d /opt/homebrew ]]; then
#     eval "$(/opt/homebrew/bin/brew shellenv)"
#   fi

rustup (not used — core:rust replaces it)

This stack declares rust = "stable" in mise's [tools], which uses mise's core Rust backend and never invokes rustup — precisely because of the behavior documented below. If you must use rustup outside this framework:

rustup is the most aggressive profile modifier in this list. Its installer writes PATH entries to ~/.profile, ~/.bashrc, and ~/.zshenv simultaneously — every one it finds. It also respects CARGO_HOME and RUSTUP_HOME environment variables. Since ~/.profile already exports these to the XDG location, rustup will install to the correct directories. Use --no-modify-path to prevent it from appending redundant PATH entries to your shell profiles.

# Safe rustup installation: respects CARGO_HOME/RUSTUP_HOME from
# ~/.profile, skips all profile modification.
curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | \
  sh -s -- --no-modify-path -y

# If environment variables are not inherited, export explicitly:
CARGO_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/cargo" \
RUSTUP_HOME="${XDG_DATA_HOME:-$HOME/.local/share}/rustup" \
curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | \
  sh -s -- --no-modify-path -y

nvm is not used in this framework — mise replaces it entirely. If you encounter a project that requires nvm for team compatibility, install it with --no-use to prevent it from modifying your profile and source it from a dedicated conf.d/ fragment guarded by a conditional.

Post-install cleanup

After installing any tool that modifies shell profiles, run the audit:

# Scan for rogue injections (either form)
mise run dotfiles:audit
sh bootstrap.sh --audit-only

The audit checks ~/.profile, ~/.bash_profile, ~/.bashrc, ~/.zshenv, and ~/.zshrc for lines injected by common installers (NVM_DIR, VOLTA_HOME, BUN_INSTALL, cargo/env, pyenv init, asdf.sh, conda init, etc.). Remove any matches and rely on the framework's conf.d/10-path.zsh as the single source of truth.