Understanding Environment Inheritance in tmux
A new tmux session is not necessarily a clean environment.
That statement sounds strange at first. If I open a new terminal, remove a variable, and create a new tmux session, I might reasonably expect the new session to inherit the environment of the shell from which I ran tmux new-session.
That is not always what happens.
tmux is built around a long-running server process. The shell that runs a tmux command is a client of that server. If the server already exists, creating a new session does not create a new tmux server, and it does not simply copy the complete environment of the current client shell.
To understand what a new tmux process inherits, we need to distinguish several environments that are easy to conflate.
The inheritance modelβ
The basic inheritance path is:
shell that initially starts the tmux server
β
βΌ
tmux global environment
β
β merged with
βΌ
tmux session environment
β
βΌ
newly created window or pane
β
βΌ
shell startup files and commands
β
βΌ
environment of the running shell process
When the tmux server first starts, it copies the environment of the client process that started it into tmux's global environment.
Each tmux session can also have its own environment entries. When tmux creates a new process in a window or pane, it merges the global environment with the session environment. If the same variable exists in both places, the session value takes precedence.
After the process starts, it has its own process environment. Changing tmux's stored environment later does not retroactively modify shells or programs that are already running.
That gives us at least four distinct perspectives:
- The environment of the shell outside tmux.
- The global environment stored by the tmux server.
- The environment associated with a particular tmux session.
- The environment of a shell already running inside a pane.
Those environments can contain different values for the same variable.
A simple exampleβ
Start with no tmux server running:
tmux kill-server 2>/dev/null
Now set a variable and start tmux:
export APP_ENV=staging
tmux new-session -s first
Inside the tmux session:
printf '%s\n' "$APP_ENV"
The result should be:
staging
That part is unsurprising. The tmux server started from a shell containing APP_ENV=staging, so the value entered tmux's global environment and was passed to the new shell.
Detach from the session:
Ctrl-b d
Now, in the outside shell, remove the variable:
unset APP_ENV
Confirm that the outside shell no longer has it:
printenv APP_ENV
No output should be produced.
Create another tmux session:
tmux new-session -s second
Inside the new session, inspect the variable:
printf '%s\n' "$APP_ENV"
You may still see:
staging
The second session was new, but the tmux server was not. The server was still holding the global environment it captured earlier.
The relevant boundary was the lifetime of the server, not merely the lifetime of the session.
Inspecting the current shell environmentβ
Inside or outside tmux, ordinary shell commands inspect the environment of the current process:
env
or:
printenv
To inspect one variable:
printenv APP_ENV
Shell expansion can also display a value:
printf '%s\n' "$APP_ENV"
There is a small but useful distinction here.
printenv APP_ENV indicates whether APP_ENV exists in the process environment. By contrast:
printf '%s\n' "$APP_ENV"
prints an empty line both when the variable is unset and when it is set to an empty string.
To distinguish those cases in Bash:
if [[ -v APP_ENV ]]; then
printf 'APP_ENV is set to: %q\n' "$APP_ENV"
else
printf 'APP_ENV is unset\n'
fi
These commands describe the environment of the shell in which they run. They do not necessarily describe what tmux currently has stored.
A shell may have changed a variable after it started:
export APP_ENV=development
That updates the shell and the commands it subsequently launches. It does not automatically update tmux's global or session environment.
Inspecting tmux's global environmentβ
To see the global environment stored by the tmux server:
tmux show-environment -g
The shorter command alias also works:
tmux showenv -g
To inspect one variable:
tmux show-environment -g APP_ENV
For example:
APP_ENV=staging
This command can be run from inside or outside tmux. In both cases, it communicates with the tmux server and asks the server to display its stored global value.
That makes it possible to compare the current shell with the server:
printf 'current shell: '
printenv APP_ENV || printf '<unset>\n'
printf 'tmux global: '
tmux show-environment -g APP_ENV 2>/dev/null || printf '<unset>\n'
The result might be:
current shell: development
tmux global: APP_ENV=staging
There is no contradiction. The two commands are inspecting different environments.
Inspecting a session environmentβ
To inspect the environment associated with a particular session:
tmux show-environment -t development
To inspect one variable:
tmux show-environment -t development APP_ENV
The target does not have to be the session to which the current client is attached. You can inspect another session by name without attaching to it:
tmux show-environment -t staging APP_ENV
tmux show-environment -t production APP_ENV
To find the name of the current session from inside tmux:
tmux display-message -p '#S'
That allows a small diagnostic:
session_name=$(tmux display-message -p '#S')
printf 'shell process:\n'
printenv APP_ENV || printf '<unset>\n'
printf '\ntmux global environment:\n'
tmux show-environment -g APP_ENV 2>/dev/null || printf '<unset>\n'
printf '\ntmux session environment:\n'
tmux show-environment -t "$session_name" APP_ENV 2>/dev/null \
|| printf '<no session-specific entry>\n'
The session environment should not be thought of as a second, unrelated complete process environment.
tmux maintains a global environment and a session environment. When it creates a process, it combines them. A session entry overrides a global entry with the same name.
Conceptually:
global:
APP_ENV=staging
LOG_LEVEL=info
session:
APP_ENV=development
merged environment for a new process:
APP_ENV=development
LOG_LEVEL=info
The session-specific APP_ENV wins. LOG_LEVEL still comes from the global environment.
The environment of an existing paneβ
Once tmux has launched a shell in a pane, that shell is an ordinary running process with its own environment.
Suppose a pane starts with:
printenv APP_ENV
and prints:
staging
From another shell, change tmux's global value:
tmux set-environment -g APP_ENV production
Confirm that tmux now holds the new value:
tmux show-environment -g APP_ENV
The output is:
APP_ENV=production
Return to the shell that was already running and inspect its value again:
printenv APP_ENV
It will still report:
staging
The existing process was not modified.
Now create a new window in that session:
tmux new-window
In the new shell:
printenv APP_ENV
The new process may report:
production
tmux provides an initial environment when it starts a process. It does not continuously synchronize the environments of processes that are already running.
New sessions, windows, and panesβ
The distinction between tmux objects matters:
server
βββ session
βββ window
β βββ pane
β βββ pane
βββ window
βββ pane
The server owns the global environment.
A session can carry session-specific environment state.
Windows and panes contain processes. When tmux starts one of those processes, it constructs an initial environment from the global and session environments.
Therefore:
- Starting a new session does not necessarily start a new server.
- Starting a new window creates a new process environment.
- Splitting a pane creates another new process environment.
- Existing pane processes keep the environments they already have.
A command issued by a shell inside a pane is a separate matter. A child process launched by that shell normally inherits the shell's current environment, not a freshly reconstructed environment directly from tmux.
For example:
export APP_ENV=local
./run-server
./run-server inherits APP_ENV=local from the shell.
But creating a new tmux pane:
tmux split-window
causes tmux to launch the new pane's process using tmux's stored environment model. It does not simply clone every environment change made interactively in the current pane.
Changing the global environmentβ
Set a global variable:
tmux set-environment -g APP_ENV staging
Inspect it:
tmux show-environment -g APP_ENV
Change it:
tmux set-environment -g APP_ENV production
Remove the global entry:
tmux set-environment -gu APP_ENV
The long form is:
tmux set-environment -g -u APP_ENV
The -g flag selects the global environment. The -u flag unsets the entry.
These changes affect the environment tmux constructs for future processes. They do not rewrite the environments of already-running processes.
Changing a session environmentβ
Set a value for a particular session:
tmux set-environment -t development APP_ENV development
Inspect it:
tmux show-environment -t development APP_ENV
Remove the session entry:
tmux set-environment -u -t development APP_ENV
Without -g, set-environment operates on a session environment.
A session-specific value can override a global value:
tmux set-environment -g APP_ENV staging
tmux set-environment -t development APP_ENV development
Now the stored values are:
tmux show-environment -g APP_ENV
tmux show-environment -t development APP_ENV
Conceptually:
global APP_ENV: staging
development session: development
A new process in the development session receives the session-specific value.
A new process in another session, without its own APP_ENV entry, receives the global value.
Unset variables and removal markersβ
tmux can represent more than a simple NAME=value entry.
Its environment output may include a line such as:
-APP_ENV
That does not mean the variable has a value beginning with a hyphen.
The leading - indicates that the variable is marked for removal from the environment of a newly created process.
This distinction matters when a variable exists globally but a session needs to ensure that it is absent.
For recent versions of tmux, set-environment provides both -u and -r:
tmux set-environment -u APP_ENV
unsets an entry from the selected tmux environment, while:
tmux set-environment -r APP_ENV
marks the variable to be removed before tmux starts a new process.
To mark a global variable for removal:
tmux set-environment -gr APP_ENV
To mark it for removal in one session:
tmux set-environment -r -t development APP_ENV
Inspecting the environment may then show:
-APP_ENV
Because tmux command flags can differ across older versions, check the installed manual before depending on -r:
man tmux
Then search for:
set-environment
Shell-formatted outputβ
show-environment also supports shell-formatted output:
tmux show-environment -s
For one variable:
tmux show-environment -s APP_ENV
Instead of producing:
APP_ENV=staging
it produces output suitable for a Bourne-style shell, such as an export or unset command.
That makes this possible:
eval "$(tmux show-environment -s APP_ENV)"
This explicitly imports the value currently stored by tmux into the current shell.
That is fundamentally different from tmux modifying an existing shell automatically. The shell is reading tmux's output and executing the corresponding shell command itself.
Because eval executes generated shell text, it should be used deliberately. Inspect the output first when experimenting:
tmux show-environment -s APP_ENV
What update-environment doesβ
tmux has a session option named update-environment.
Inspect it with:
tmux show-options -g update-environment
Depending on the version and configuration, the result may contain variables such as:
DISPLAY
SSH_AUTH_SOCK
SSH_ASKPASS
WINDOWID
XAUTHORITY
The exact list is configurable.
The important point is that update-environment is a list of selected variable names. It is not an instruction to copy the entire environment of every client into tmux.
When a new session is created or an existing session is attached, tmux can update those selected variables in the session environment from the relevant client environment.
That behavior is useful for variables whose values commonly change between terminal connections, particularly display or authentication-agent variables.
It does not mean that an arbitrary application variable will be synchronized:
export APP_ENV=production
tmux attach-session -t development
Unless APP_ENV is included in update-environment, attaching from that shell does not imply that tmux will update its stored APP_ENV.
To add a variable to the option:
tmux set-option -ga update-environment ' APP_ENV'
The leading space separates the new item from the existing list.
Inspect the result:
tmux show-options -g update-environment
This should be done carefully. Automatically updating project-specific variables during attachment can make a session's behavior depend on which client attached most recently.
The client environmentβ
The shell that invokes a tmux command is the environment of a tmux client process.
You can inspect that shell directly before invoking tmux:
env | sort
or capture it:
env | sort > /tmp/outside-tmux.env
Inside a pane:
env | sort > /tmp/inside-tmux.env
Compare them:
diff -u /tmp/outside-tmux.env /tmp/inside-tmux.env
Some differences are expected. tmux sets or changes variables used to identify and configure the terminal environment, including variables such as TMUX, TMUX_PANE, and TERM.
The comparison is still useful for finding application-specific variables that entered the pane unexpectedly.
For a narrower comparison:
comm -3 \
<(env | sort) \
<(tmux show-environment -g | sort)
This compares the current shell environment with tmux's global environment.
The output should be interpreted carefully because:
- tmux's output uses its own representation for removed variables;
- the current shell may have added or changed variables after startup;
- shell startup files may alter variables after tmux launches the shell;
- tmux may set terminal-specific variables itself.
Identifying the serverβ
The command:
tmux list-sessions
shows sessions on the default tmux server.
If it succeeds, a server already exists. Running:
tmux new-session -s another
normally creates another session on that existing server.
If no server exists, tmux reports an error similar to:
no server running
You can use a quiet check:
if tmux list-sessions >/dev/null 2>&1; then
printf 'a tmux server is running\n'
else
printf 'no tmux server is running\n'
fi
The server's persistence explains why environment state can survive after every client has detached.
A detached session is still owned by the running server. Even if no terminal is currently displaying it, the server and its stored state remain alive.
Named servers and socketsβ
tmux can run multiple independent servers.
The -L option selects a socket name:
tmux -L development new-session -s main
Commands using the default server do not address that server:
tmux list-sessions
To communicate with the named server, use the same socket name:
tmux -L development list-sessions
Its global environment is separate:
tmux -L development show-environment -g
Another server can have another environment:
tmux -L production new-session -s main
tmux -L production show-environment -g
This gives us another boundary:
default tmux server
global environment A
development tmux server
global environment B
production tmux server
global environment C
A named server is useful when a group of sessions should not share the environment or lifecycle of the default server.
Starting a server from a controlled environmentβ
Removing variables from the shell before running tmux new-session is sufficient only when that command will start a new server.
For example:
env -u APP_ENV tmux new-session -s clean
If no tmux server exists, the new server starts without APP_ENV.
If a server already exists, however, the client connects to that server. Removing APP_ENV from the client does not automatically remove it from the server's stored global environment.
A named server makes the boundary explicit:
tmux -L isolated kill-server 2>/dev/null
env -u APP_ENV \
tmux -L isolated new-session -s clean
For a more aggressively controlled environment:
tmux -L isolated kill-server 2>/dev/null
env -i \
HOME="$HOME" \
USER="$USER" \
PATH="$PATH" \
TERM="$TERM" \
SHELL="$SHELL" \
tmux -L isolated new-session -s clean
env -i begins with an empty environment. The command then supplies only the variables explicitly listed.
This is useful for experimentation, but it can also remove variables required for locale handling, desktop integration, SSH agents, graphical applications, or other system features.
The point is not that every tmux session should be started with env -i. The point is that a genuinely controlled tmux environment requires control over the creation of the server, not merely the creation of a session on an existing server.
A diagnostic scriptβ
The following script compares one variable from several perspectives:
#!/usr/bin/env bash
set -u
variable=${1:-APP_ENV}
printf 'Variable: %s\n\n' "$variable"
printf 'Current process environment:\n'
if printenv "$variable"; then
:
else
printf '<unset>\n'
fi
printf '\nTmux global environment:\n'
if tmux show-environment -g "$variable" 2>/dev/null; then
:
else
printf '<unset or no tmux server>\n'
fi
if [[ -n ${TMUX:-} ]]; then
session_name=$(tmux display-message -p '#S')
printf '\nCurrent session: %s\n' "$session_name"
if tmux show-environment -t "$session_name" "$variable" 2>/dev/null; then
:
else
printf '<no session-specific entry>\n'
fi
else
printf '\nCurrent session:\n'
printf '<not running inside tmux>\n'
fi
Save it as:
inspect-tmux-environment
Make it executable:
chmod +x inspect-tmux-environment
Then run:
./inspect-tmux-environment APP_ENV
A result might look like:
Variable: APP_ENV
Current process environment:
development
Tmux global environment:
APP_ENV=staging
Current session: application
APP_ENV=production
That result is possible because each line describes a different layer.
The current shell has development.
The tmux server's global environment has staging.
The session has a session-specific override of production.
A new process created by tmux in that session should therefore initially receive production. A command launched directly by the current shell receives development.
A reproducible experimentβ
The entire inheritance model can be demonstrated with an isolated tmux server.
First, destroy any previous experimental server:
tmux -L environment-test kill-server 2>/dev/null
Start the server with a global value:
APP_ENV=global \
tmux -L environment-test new-session \
-d \
-s example
Inspect the global environment:
tmux -L environment-test show-environment -g APP_ENV
Expected result:
APP_ENV=global
Set a session-specific override:
tmux -L environment-test set-environment \
-t example \
APP_ENV session
Inspect the session entry:
tmux -L environment-test show-environment \
-t example \
APP_ENV
Expected result:
APP_ENV=session
Create a new window that writes its environment to a file:
tmux -L environment-test new-window \
-t example \
'printenv APP_ENV > /tmp/tmux-app-env'
Read the result:
cat /tmp/tmux-app-env
Expected result:
session
The session value overrode the global value.
Now remove the session-specific entry:
tmux -L environment-test set-environment \
-u \
-t example \
APP_ENV
Create another process:
tmux -L environment-test new-window \
-t example \
'printenv APP_ENV > /tmp/tmux-app-env-global'
Read it:
cat /tmp/tmux-app-env-global
Expected result:
global
Without the session override, the process inherited the global value.
Finally, destroy the experimental server:
tmux -L environment-test kill-server
Because the experiment uses its own named server, it does not require destroying ordinary tmux sessions on the default server.
The practical mental modelβ
When a value inside tmux is surprising, ask four separate questions.
What does the current shell contain?β
printenv APP_ENV
What does the tmux server contain globally?β
tmux show-environment -g APP_ENV
Does the session contain its own entry?β
tmux show-environment -t session-name APP_ENV
Was the pane created before or after the value changed?β
An existing shell retains the process environment it already inherited. A new pane or window causes tmux to construct a new initial environment.
These questions are more useful than asking simply:
What is the tmux environment?
There is no single environment shared continuously by the client, server, sessions, and running pane processes.
There is an inheritance path.
Conclusionβ
A new tmux session is not necessarily an environmental reset.
The tmux server is long-lived. When it starts, it captures a global environment. Sessions can contain their own environment entries. When tmux creates a new window or pane process, it merges those stored environments, with session values taking precedence.
After that process begins, its environment belongs to the process. Changes to the shell do not automatically update tmux, and changes to tmux do not retroactively update the shell.
The essential inspection commands are:
# Current shell process
printenv APP_ENV
# tmux server's global environment
tmux show-environment -g APP_ENV
# A particular session's environment
tmux show-environment -t development APP_ENV
# Variables tmux is configured to update from clients
tmux show-options -g update-environment
The essential model is:
server startup environment
β
tmux global environment
+
tmux session environment
β
new pane or window process
β
independent running process environment
Once those layers are visible, tmux environment behavior stops looking mysterious. The unexpected value is not appearing from nowhere. It is being inherited from a specific layer whose lifetime is longer than it first appears.
Comments
No comments yet. Be the first!