Which Environment Variables Does an SSH Session Actually Load?

Summary

Understanding which environment variables load in an SSH session depends on whether it's a login/non-login and interactive/non-interactive shell. A standard `ssh user@host` command typically initiates a login interactive session, loading `/etc/environment`, `/etc/profile` (and `/etc/profile.d`), and then the first found among `~/.bash_profile`, `~/.bash_login`, or `~/.profile`, often sourcing `~/.bashrc`. In contrast, executing a command directly via `ssh user@host -- command` results in a non-login non-interactive session, which only loads `/etc/environment`, `/etc/bash.bashrc`, and `~/.bashrc`. Tools like Warpify can alter the default shell invocation, changing the session type and thus the loaded environment files. Developers should be aware of these distinctions to troubleshoot missing environment variables in different SSH contexts.

Many development colleagues regularly need to log in to machines (physical or virtual). After reading this article, you will certainly gain something. At the beginning, I raise three questions that both summarize the content and spark your interest:

  • What kinds of SSH sessions are there?
  • When you run ssh your-name@your-host, once you’re on the target machine, from which files are the environment variables loaded when you execute env?
  • When you run ssh your-name@your-host -- env, from which files are the environment variables loaded when you execute env?

Background

A large portion of my work involves maintaining a job platform based on SSH. Don’t be fooled by the “primitive” or “simple” sound of it — this platform supports core low-level business across the company: server operations, Kubernetes node maintenance, big data components, database operations, mixed deployments, distributing configuration for 7-layer load balancers, etc.

It would not be an exaggeration to describe it with the following diagram:

In 2025, many platform users don’t understand Linux, SSH, or Shell well; they don’t search online, and often ask me directly:

“My script works fine when I log in to the machine through a bastion host, but when using your platform it errors out — none of the environment variables are found. Why?”

@flowblok’s Conclusion

Thanks to [Julia Evans]’ comic zine The Secret Rules of the Terminal, which refers to a blog Shell startup scripts as supporting material to describe the difference between .bashrc and .bash_profile.

The diagram drawn by @flowblok in Shell startup scripts is very complicated. He says he read all kinds of sources and was both impressed and puzzled!

Summarizing from that diagram: when launching Bash via SSH login, what scripts are loaded depends on several factors:

  • Whether it is a login shell? This can be checked via Bash running shopt login_shell.
  • Whether it is an interactive shell? This can be checked via Bash running tty.

Verification Experiment

Let’s verify @flowblok’s conclusion.

Experiment environment:

  • Machine: Ubuntu 25.04 VM on OrbStack
  • Terminal Emulator: Warp

Different Linux distros may behave differently; you’re welcome to share differences you find.

It’s confusing to distinguish “login vs non-login” and “interactive vs non-interactive”

From reading documentation, we know that login, interactive, non-login, non-interactive combine to form 4 possible SSH session types.

  • login + interactive

    This is the most common session type. After ssh $username@$hostname, running shopt login_shell and tty shows login_shell on and something like /dev/pts/X.

  • login + non-interactive

    This usually doesn’t happen by default. You can force it, using something like:

    ssh -i ~/.ssh/id_rsa user@host -- 'bash --login -c "shopt login_shell; tty"'

    That command shows login_shell on and not a tty.

  • non-login + interactive

    Also rare by default. After ssh user@host, then manually starting Bash again (without --login flag), you get shopt login_shell = off, and tty = /dev/pts/X.

    Using Warp’s Warpify SSH Sessions feature also yields this kind of session. The reason will be explained later.

  • non-login + non-interactive

    This is the type used by the job platform mentioned in the background: users are confused because this session type behaves differently. Executing ssh user@host -- shopt login_shell; tty in this case outputs login_shell off and not a tty.

Expansion: How SSH Session Is Created

Use strace on sshd to observe how SSH creates the session, in order to explain why Warp’s Warpify SSH Sessions feature affects the outcome of shopt login_shell.

$ ps -aux | grep sshd
root         284  0.0  0.0   9836  3144 ?        Ss   01:55   0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
$ sudo strace -f -e trace=execve -p 284

Behavior of Warpify SSH Sessions

One can see that Warpify actually replaces the SSH login command with a long script chain to implement enhanced functionality.

When Warpify SSH Sessions is enabled, and you log in, the strace output is huge; large-model analysis reveals:

Process tree structure:

sshd (PID 284) — main SSH daemon
├ sshd-session (PID …) — handles a single connection
├ sh (…) — runs MOTD scripts, etc.
└ bash (…) — user’s actual shell process
└ many child processes related to Warp Terminal features

Inspecting strace, you find that after Warpify SSH Sessions, the user’s Bash process is started with something like "/usr/bin/bash", ["bash", "--rcfile", "/dev/fd/63"].

Original Behavior

With Warpify disabled, using ssh user@host to log in, based on the earlier observations, we can see how the Bash process is started:

Bash is launched via "/bin/bash", ["-bash"]. Consulting man bash, we see:

INVOCATION
A login shell is one whose first character of the argument zero is a -, or one started with the --login option.

Thus -bash is equivalent to --login. So ssh user@host is effectively the same as ssh user@host -- bash --login. By default, SSH opens a login shell. However, Warpify SSH Sessions changes that behavior, removing the --login flag, so shopt login_shell returns off.

How Environment Variable Scripts Are Loaded

Add an environment variable like export ENV_XXXX="this is XXXX" in each of these files:

  • /etc/environment
  • /etc/profile
  • /etc/bash.bashrc
  • /root/.profile
  • /root/.bash_login
  • /root/.bash_profile
  • /root/.bashrc
  • /etc/profile.d/custom.sh

Then run the two commands from the start:

$ ssh $username@$hostname
$ env | grep ENV
$ ssh $username@$hostname -- 'env | grep ENV'

The results closely match what @flowblok described.

Login + Interactive Session

First, @flowblok didn’t mention /etc/environment, but both types of sessions load it.

In login + interactive sessions, you can see that the scripts along what the diagram shows (purple line) are loaded. Looking at /etc/profile:

if [ -d /etc/profile.d ]; then
  for i in $(run-parts --list --regex '^[a-zA-Z0-9_][a-zA-Z0-9._-]*\.sh$' /etc/profile.d); do
    if [ -r $i ]; then
      . $i
    fi
  done
  unset i
fi

It loads scripts in /etc/profile.d.

If ~/.bash_profile, ~/.bash_login, and ~/.profile all exist, only ~/.bash_profile is loaded. They operate in a certain order, which we can further verify experimentally.

When ~/.bash_profile is removed, ~/.bash_login is used instead.

Similarly, if ~/.bash_login is also removed, then ~/.profile is used. In my distro, ~/.bash_profile includes loading ~/.bashrc:

# ~/.profile: executed by Bourne-compatible login shells.

export ENV_PROFILE="this is ~/.profile"
echo "this is ~/.profile"

if [ "$BASH" ]; then
  if [ -f ~/.bashrc ]; then
    . ~/.bashrc
  fi
fi

This behavior is mentioned in man bash, in the INVOCATION section. 

When bash is invoked as an interactive login shell, or as a non-interactive shell with the --login option, it first reads and executes commands from the file /etc/profile, if that file exists. After reading that file, it looks for ~/.bash_profile, ~/.bash_login, and ~/.profile, in that order, and reads and executes commands from the first one that exists and is readable. The --noprofile option may be used when the shell is started to inhibit this behavior.

Non-login Non-interactive Sessions

Skipping /etc/environment since already covered.

In these sessions, the loading logic is also mentioned in man bash under INVOCATION.

If bash determines it is being run non-interactively in this fashion, it reads and executes commands from /etc/bash.bashrc and ~/.bashrc, if these files exist and are readable.

The experiment’s observed behavior matches this.

Summary

  • SSH sessions can be divided into the four types: login + interactive, login + non-interactive, non-login + interactive, non-login + non-interactive.
  • ssh your-name@your-host loads /etc/environment, /etc/profile (and scripts under /etc/profile.d), /etc/bash.bashrc, ~/.bash_profile, or ~/.bash_login (if ~/.bash_profile does not exist), or ~/.profile (if neither ~/.bash_profile nor .bash_login exist).
  • ssh your-name@your-host -- $CMD loads /etc/environment, /etc/bash.bashrc, ~/.bashrc.

As Julia Evans said, terminal-related knowledge is very scattered. If interested, friends may read The Secret Rules of the Terminal.

This post is translated from https://spencercjh.me/blog/ssh-session-env/

LINUX SHELL LOGIN TTY

  RELATED

  COMMENTS

0

No comment for this article.