Two orthogonal axes
Every shell has two flags:
- Login or not — login shells are the first shell of a session: Terminal.app on open, ssh login,
su -. - Interactive or not — interactive shells have a prompt; non-interactive shells run scripts.
That gives 2x2 = four shell types, each loading a different set of config files.
The matrix (zsh)
| Interactive | Non-interactive | |
|---|---|---|
| Login | .zshenv → .zprofile → .zshrc → .zlogin | .zshenv → .zprofile → .zlogin |
| Non-login | .zshenv → .zshrc | .zshenv only |
Real-world cases
- Open Terminal.app → login + interactive (full reload).
ssh user@host→ login + interactive.ssh user@host 'echo hi'→ login + non-interactive.- Run
./script.shfrom a session → non-login + non-interactive. - Type
zshinside an existing zsh → non-login + interactive. - cron / launchd jobs → non-login + non-interactive (
.zshenvonly).
Why this trips people up
You add a required value to ~/.zshrc and it works in your terminal. A scheduled job starts with a different environment and may not run zsh at all. Declare its interpreter, PATH, working directory, and required non-secret values in the job contract; obtain secrets through the job's credential boundary instead of moving them into another shell startup file.
Confirm at runtime
[[ -o login ]] && echo login. [[ -o interactive ]] && echo interactive. The flags are introspectable from inside the shell.
Scheduled jobs do not inherit an interactive shell
Cron and launchd start from their own environment and may not run zsh at all. Declare the interpreter, PATH, working directory, and required non-secret values in the job contract; keep secrets in a credential store with narrowly scoped access rather than startup files.
The caller predicts which files can be read
Start with the process creator: Terminal, an SSH server, a scheduler, or a shebang. Inspect the actual executable, arguments, and environment before deciding whether login or interactive startup rules apply.