Bash Startup Files: Login, Interactive, and Non-Interactive Shells
Map Bash startup-file selection to invocation mode so shell configuration, scripts, SSH sessions, and environment hooks behave predictably.
Bash does not read one universal profile on every launch. Startup behavior depends on whether the shell is a login shell, whether it is interactive, whether it is invoked as sh, and in some cases how a remote shell starts it. Misdiagnosing that matrix leads to familiar problems: a PATH that exists in Terminal but not in a script, duplicated prompt setup, or a slow command caused by interactive plugins loading in automation.
Know which file a shell actually reads
An interactive login Bash reads /etc/profile and then the first readable file among ~/.bash_profile, ~/.bash_login, and ~/.profile. An interactive non-login Bash reads ~/.bashrc. A common pattern is for ~/.bash_profile to source ~/.bashrc so interactive settings are shared, while login-only environment setup stays separate.
A non-interactive Bash started to run a script checks BASH_ENV and sources the file named by that variable after expansion. It does not search PATH for that file. Exporting BASH_ENV globally can therefore run shell code in every non-interactive Bash process, including scripts and tools that were not designed to load interactive customizations.
When invoked as sh, Bash follows a different startup path to mimic historical and POSIX behavior. Remote-shell daemon detection is another special case: Bash may read ~/.bashrc for a non-interactive network shell. Do not infer startup behavior solely from the shell’s visible prompt.
Keep interactive customization out of automation
Put prompt themes, terminal title changes, completion systems, and interactive aliases behind an interactive check in ~/.bashrc:
case $- in
*i*) ;;
*) return ;;
esac
# Interactive-only prompt, completion, and key bindings follow.
Use explicit exports and small environment-only setup for login shells. A setting needed by scripts should be declared in the script, a controlled environment, or a startup file that the invoking mode is guaranteed to read; relying on ~/.bashrc from a non-interactive script is usually a bug.
Diagnose the mode instead of guessing
Inspect the current mode with shopt -q login_shell and the interactive flag in $-. Reproduce the failing invocation with bash -lc, bash -ic, or bash -c as appropriate, and print the executable path and PATH in the same environment. Then use –noprofile or –norc to isolate which file or plugin adds delay or side effects.
Startup files execute code with the user’s privileges. Keep downloaded snippets, conditional PATH edits, and environment hooks reviewable, and avoid expensive network access or output in files that non-interactive tools may source.
Use the invocation matrix as the debugging model
The terms “login” and “interactive” describe different properties. A login shell is normally the first shell of a session; an interactive shell reads commands from a terminal. Bash may be interactive without being a login shell, as in a new terminal tab, or non-interactive with login files enabled by an explicit --login option. Check both independently:
case $- in
*i*) printf 'interactive=yes\n' ;;
*) printf 'interactive=no\n' ;;
esac
if shopt -q login_shell; then
printf 'login=yes\n'
else
printf 'login=no\n'
fi
printf 'argv0=%s\n' "$0"
That inspection reports the current process, not necessarily the shell that launched it. A script executed directly uses its shebang; bash script.sh selects Bash explicitly; sh script.sh selects the sh program and ignores the shebang for that invocation. A service may also launch a fixed executable with an environment that bypasses the terminal’s usual profile chain.
Treat each startup file as a separate interface
The login chain is often used for environment variables such as PATH, locale, and editor settings. Bash reads /etc/profile, then the first readable file from ~/.bash_profile, ~/.bash_login, and ~/.profile. It does not automatically read all three. Interactive non-login shells instead read ~/.bashrc. If a user’s login file sources .bashrc, that is an explicit convention in the file, not an implicit Bash rule.
Some Linux distributions also arrange for system-wide interactive configuration such as /etc/bash.bashrc; this is distribution packaging behavior and should not be assumed on every Unix host. Inspect the files installed by the target operating system. Likewise, do not treat .profile as a universal shell file: whether Bash reads it depends on invocation mode, and other shells have their own startup rules.
For non-interactive Bash, BASH_ENV is the deliberate hook for a file to be sourced before a script runs. Bash expands the variable and reads the resulting path; it does not search PATH for the filename. This can be useful for a controlled test harness, but exporting BASH_ENV globally can make unrelated scripts source arbitrary shell code, change functions and options, or emit unexpected output. Prefer setting it only for a process whose caller controls the file.
Account for sh, POSIX mode, and remote sessions
When Bash is invoked under the name sh, it uses a different startup policy and enters POSIX mode after the relevant startup handling. An interactive login shell invoked as sh reads /etc/profile and ~/.profile; an interactive non-login shell can use ENV; a non-interactive shell invoked as sh reads no other startup files. Do not expect .bashrc or BASH_ENV to behave identically in every invocation mode.
Bash has a special compatibility path for non-interactive shells it identifies as connected to a remote shell daemon, including sshd in documented configurations: it may read ~/.bashrc. That is not a promise that every SSH command uses .bashrc; distributions, server configuration, forced commands, and the invoked program matter. Keep .bashrc safe for non-interactive use if your remote command path depends on this behavior, or initialize the command’s environment explicitly.
When effective and real user or group IDs differ, Bash also restricts startup-file and inherited-environment behavior unless invoked with privileged-mode options. This is a security boundary, not a convenient mechanism for privilege elevation; avoid relying on startup files for privileged shell scripts.
Reproduce the exact mode without editing a real profile
Start with direct, bounded probes that show how Bash was invoked:
bash -c 'printf "noninteractive flags=%s\n" "$-"'
bash --login -c 'printf "login-shell=%s\n" "$(shopt -q login_shell; echo $?)"'
bash --noprofile --norc -c 'printf "isolated PATH=%s\n" "$PATH"'
These commands answer different questions: -c creates a non-interactive shell, --login selects the login startup path even without a terminal, and the suppression flags help isolate user configuration. Do not use bash -i in a non-terminal test without accounting for job-control and input warnings; a true interactive reproduction should run under the same terminal or PTY class as the failing application.
For a startup delay, test one file or sourced component at a time. Bash’s -x tracing can reveal which commands execute, but expanded arguments may include tokens, paths, or other sensitive values. Redirect traces to a private file, restrict access, inspect only the relevant lines, and remove them when finished. Prefer a temporary .bashrc or a dedicated clean account over destructive edits to a working profile.
Put environment setup where the caller can guarantee it
A variable required by a script belongs in that script, the process manager’s environment, a controlled wrapper, or a documented file that the launching mode explicitly sources. Moving all environment setup to .bashrc is unreliable because cron, services, build tools, non-interactive bash -c, and many IDE task runners do not necessarily read it. Sourcing .bashrc from every script is not a safe universal repair either: it may assume a terminal, change shell options, print text into a protocol stream, or perform interactive actions.
Keep startup files idempotent where practical. PATH additions should avoid adding the same directory on every nested shell; completion and prompt initialization should not run twice; optional commands should be guarded when unavailable. An interactive guard should appear before prompt-only code, but only after any environment setup that the file intentionally provides to its callers.
Acceptance testing should cover a login interactive shell, interactive non-login shell, non-interactive script, bash invoked as sh, and any remote or service invocation that matters to the application. Record the executable, $-, login_shell, selected startup paths, and final environment. That turns “works in my terminal” into a testable statement about a specific Bash startup mode.
Related:
- Shell Signal Handling: trap, Cleanup, and Process Groups
- How to Build a Cross-Shell Dotfiles Repository
Sources: