You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Some tools read the login-shell entry from the passwd database directly (sshd for git/rsync/sftp, cron, su) rather than consulting $SHELL, so that entry must always name a shell that is guaranteed to exist on the host.
bash is the only shell present on every supported host: macOS ships /bin/bash 3.2, Fedora/Bazzite ship bash 5, containers and rescue environments have bash.
zsh is not guaranteed: preinstalled on macOS, optional on Fedora.
Changing the passwd entry on Fedora normally means chsh, which lives in the util-linux-user package. Installing it is rejected: it adds a hard requirement for a one-time operation. (sudo usermod -s needs no new packages but still requires root, which is not always available.)
$SHELL must never be set from .zshenv/.zshrc/.bashrc; it is normally set by login(1) from the passwd entry. The handoff below is the single sanctioned exception.
Decision
The passwd login-shell entry stays bash on every host: the guaranteed-valid baseline for any tool that reads it directly.
Interactive login bash sessions hand off to zsh, when zsh is available, via exec at the start of dot_bash_profile.tmpl.
Restructure dot_bash_profile.tmpl so the handoff runs first; the existing .bashrc source moves below it and only executes when the session stays in bash:
# Hand off to zsh for interactive sessions when available.# The passwd entry stays bash (guaranteed present on all hosts);# $SHELL is updated to reflect the actual session shell.if [[ $-==*i* ]] &&command -v zsh >/dev/null 2>&1;thenexport SHELL="$(command -v zsh)"exec zsh -l
fi# Staying in bash (no zsh installed, or non-interactive): load interactive config.if [ -f~/.bashrc ];then.~/.bashrc
fi
Design notes:
command -v zsh, not a hardcoded path: zsh lives at /bin/zsh (macOS), /usr/bin/zsh (Fedora), or /opt/homebrew/bin/zsh (Homebrew). Runtime lookup keeps one template valid on all hosts with no per-host chezmoi flag.
[[ $- == *i* ]] restricts the handoff to interactive shells. Non-interactive login shells (bash -l -c ..., some ssh-based tooling) must stay in bash.
dot_bashrc.tmpl must NOT hand off: running bash deliberately from zsh, or tools spawning bash -i, should yield bash.
dot_zshrc.tmpl is untouched.
Handoff-first ordering: exec replaces the process, so only exported environment variables survive it. Everything .bashrc sets up (aliases, starship init, thefuck, bash-completion) is non-exported shell state that is discarded at exec; sourcing it before the handoff would pay full bash interactive startup on every login just to throw it away.
PATH prerequisite: the $HOME/.local/bin:$HOME/bin bootstrap currently lives in dot_bashrc.tmpl, and dot_zshrc.tmpl never adds $HOME/bin (it adds $HOME/.local/bin only conditionally, when Factory is installed). With handoff-first ordering .bashrc never runs on the zsh path, so move that PATH bootstrap into dot_shellrc.d/10-env.sh (sourced by both shells) to keep zsh sessions self-sufficient.
Escape hatch for deliberate bash: bash --noprofile -i skips .bash_profile entirely, yielding a real interactive bash with no handoff. This is the sanctioned way to debug the login stack, verify installer profile edits, or test bash 3.2 behavior on macOS — the cases where bash -l would otherwise hand you zsh against your intent.
Platform notes
macOS: /bin/bash is already in /etc/shells, so chsh -s /bin/bash is permitted where a passwd change is desired. Add export BASH_SILENCE_DEPRECATION_WARNING=1 to dot_bash_profile.tmpl to suppress Apple's "default interactive shell is now zsh" login nag.
Fedora/Bazzite: no new packages required. /etc is mutable on Atomic, so sudo usermod -s /bin/bash "$USER" remains an option where root exists, but this spec works with zero privileges.
$SHELL reports zsh when zsh exists, bash otherwise: always honest about the running session.
Nothing in the stack ever points at a shell that is not installed.
Known edge case: bash -l typed inside a zsh session hands back off to zsh. Acceptable; consistent with "login means zsh when available". Use bash --noprofile -i when a real interactive bash is intended.
Acceptance
Login shell with zsh installed results in a zsh session with $SHELL set to the resolved zsh path
Login shell without zsh installed results in a bash session with no errors printed
bash -l -c 'echo hi' stays in bash (no handoff)
Handoff path skips ~/.bashrc (verify with bash -xli; no .bashrc lines in the trace)
bash --noprofile -i yields an interactive bash session (no handoff)
PATH bootstrap moved from dot_bashrc.tmpl to dot_shellrc.d/10-env.sh; post-handoff zsh sessions include $HOME/bin and $HOME/.local/bin in PATH
Verified on bash 3.2 (macOS /bin/bash) and bash 5.x (Fedora)
Problem
Some tools read the login-shell entry from the passwd database directly (sshd for git/rsync/sftp, cron,
su) rather than consulting$SHELL, so that entry must always name a shell that is guaranteed to exist on the host.bashis the only shell present on every supported host: macOS ships/bin/bash3.2, Fedora/Bazzite ship bash 5, containers and rescue environments have bash.zshis not guaranteed: preinstalled on macOS, optional on Fedora.chsh, which lives in theutil-linux-userpackage. Installing it is rejected: it adds a hard requirement for a one-time operation. (sudo usermod -sneeds no new packages but still requires root, which is not always available.)$SHELLmust never be set from.zshenv/.zshrc/.bashrc; it is normally set bylogin(1)from the passwd entry. The handoff below is the single sanctioned exception.Decision
bashon every host: the guaranteed-valid baseline for any tool that reads it directly.execat the start ofdot_bash_profile.tmpl.Proposed fix
Restructure
dot_bash_profile.tmplso the handoff runs first; the existing.bashrcsource moves below it and only executes when the session stays in bash:Design notes:
command -v zsh, not a hardcoded path: zsh lives at/bin/zsh(macOS),/usr/bin/zsh(Fedora), or/opt/homebrew/bin/zsh(Homebrew). Runtime lookup keeps one template valid on all hosts with no per-host chezmoi flag.[[ $- == *i* ]]restricts the handoff to interactive shells. Non-interactive login shells (bash -l -c ..., some ssh-based tooling) must stay in bash.[[ ]]: valid on bash 3.2 and bash 5.x alike. No associative arrays (see fix(bash): 10-droid.sh fails on bash 3.2 (associative arrays unsupported) #83).dot_bashrc.tmplmust NOT hand off: runningbashdeliberately from zsh, or tools spawningbash -i, should yield bash.dot_zshrc.tmplis untouched.execreplaces the process, so only exported environment variables survive it. Everything.bashrcsets up (aliases, starship init, thefuck, bash-completion) is non-exported shell state that is discarded atexec; sourcing it before the handoff would pay full bash interactive startup on every login just to throw it away.$HOME/.local/bin:$HOME/binbootstrap currently lives indot_bashrc.tmpl, anddot_zshrc.tmplnever adds$HOME/bin(it adds$HOME/.local/binonly conditionally, when Factory is installed). With handoff-first ordering.bashrcnever runs on the zsh path, so move that PATH bootstrap intodot_shellrc.d/10-env.sh(sourced by both shells) to keep zsh sessions self-sufficient.bash --noprofile -iskips.bash_profileentirely, yielding a real interactive bash with no handoff. This is the sanctioned way to debug the login stack, verify installer profile edits, or test bash 3.2 behavior on macOS — the cases wherebash -lwould otherwise hand you zsh against your intent.Platform notes
/bin/bashis already in/etc/shells, sochsh -s /bin/bashis permitted where a passwd change is desired. Addexport BASH_SILENCE_DEPRECATION_WARNING=1todot_bash_profile.tmplto suppress Apple's "default interactive shell is now zsh" login nag./etcis mutable on Atomic, sosudo usermod -s /bin/bash "$USER"remains an option where root exists, but this spec works with zero privileges.Consequences
getent passwd "$USER"reports bash: always true, always safe for non-interactive tooling.$SHELLreports zsh when zsh exists, bash otherwise: always honest about the running session.bash -ltyped inside a zsh session hands back off to zsh. Acceptable; consistent with "login means zsh when available". Usebash --noprofile -iwhen a real interactive bash is intended.Acceptance
$SHELLset to the resolved zsh pathbash -l -c 'echo hi'stays in bash (no handoff)~/.bashrc(verify withbash -xli; no.bashrclines in the trace)bash --noprofile -iyields an interactive bash session (no handoff)dot_bashrc.tmpltodot_shellrc.d/10-env.sh; post-handoff zsh sessions include$HOME/binand$HOME/.local/binin PATH/bin/bash) and bash 5.x (Fedora)bash -npasses on the rendered templateOut of scope
10-droid.shbash 3.2 / zsh bugs (fix(bash): 10-droid.sh fails on bash 3.2 (associative arrays unsupported) #83, fix(zsh): 10-droid.sh subcommand passthrough and trusted-folder detection broken #84); independent workRelated: #80, #81. This issue is the canonical home of this spec.