About Feeds

A (relatively) deep dive into (zsh) shell login process (without a display manager)

Recently, I have uninstalled my display manager for various reasons. One of the reasons for that was to have a better understanding of my login process. Display managers generally do arbitrary stuff, they have their own way/order of sourcing files, sometimes they skip important files and sometimes they source unnecessary stuff etc. So I thought, using no display manager would result in a simpler login model. Oh boy, I was wrong.

After quite a bit of fiddling around I believe I have a clear mental model of in which order my files gets sourced. Before that, here is a quick summary of some terminology:

[non-]Login shell
It's first thing that is executed when you log in to your system. It generally spawns your DE/WM.
[non-]Interactive shell
It's the shell that you use interactively, shells that you type into.

When you log into a TTY, you get a interactive login shell. When you spawn a new terminal in your DE/WM, you get a non-login interactive shell. When you log in to your DE directly, that is done through a non-interactive login shell. A script that you run is run under a non-login non-interactive shell. These are important because this information will guide you why your PATH variable is not working as intended in some cases and why sometimes it works.

I use zsh as my main shell and I also set it to be my login shell. zsh has a much simpler sourcing hierarchy, I'm not even going to bother myself explaining how bash does it. So here is how it goes:

A use case

A problem I was having was related to unison. It's a simple and powerful file synchronization program that I use. One problem with that is, it's quite picky about it's version. It requires absolute same version on both client and server, and not only that, it also wants both binaries to be compiled with same version of the OCaml compiler. To solve this kind of version issue between my rigs, (among other reasons) I use nix package manager. This gets me same binary on every computer I have. But there is one problem, when I install unison through nix, it installs it under ~/.nix-profile/bin/unison, and when I run unison to synchronize my files, it fails to find unison on the other computer. But I can ssh into other computer and run unison without a problem. Hmm, what is going on here?

  • I also gave this example above, when nix gets installed it also installs the following files: /etc/profile.d/nix{,-daemon}.sh
  • These files update $PATH variable, so ~/.nix-profile/bin gets added into $PATH.
  • This files are sourced through /etc/profile, which is sourced by /etc/zsh/zprofile.
  • /etc/zsh/zprofile gets sourced when a login shell is spawned.

So when I do ssh my-machine, it spawns an interactive login shell and drops me into it. By this time, /etc/zsh/zprofile is already sourced and thanks to that I can simply run unison binary. When I run unison on client, it connects the server through ssh but it uses a non-login non-interactive shell while doing that. Same goes for this command ssh my-machine which unison. This command runs in a non-login non-interactive shell. Because /etc/zsh/zprofile requires a login shell to get sourced, ~/.nix-profile/bin never gets added to the $PATH variable.

So what's the solution? You know it, only ~/.zshenv gets sourced when non-login non-interactive shell is spawned. So I just update the $PATH there and everything works as expected now.

Additional resources

Shell startup scripts
I found this great article that summarizes what is going on at startup for different shells with beautiful graphics. It proves me right about using zsh instead of bash.


That's it. This was just some kind of a braindump. From now on, I'll just try to drop my notes as blog posts like this.