Zsh doesn’t actually store history and environment variables per session in the way you might intuitively expect; instead, it orchestrates a shared state that feels like it’s per-session.
Let’s see it in action. Open two separate terminal windows. In the first, type export MY_VAR="session1" and then echo $MY_VAR. You’ll see session1. Now, in the second terminal, type echo $MY_VAR. If you’re expecting session1, you’ll be surprised to see nothing, or perhaps a default value if you had it set elsewhere. This is because environment variables are inherited from the parent process and not automatically shared between independent terminal sessions.
# Terminal 1
> export MY_VAR="session1"
> echo $MY_VAR
session1
# Terminal 2
> echo $MY_VAR
# (likely empty or a system default)
Now, let’s look at history. In Terminal 1, type ls -l. Then, in Terminal 2, type history 1. You’ll see ls -l listed. This is shared, but not in real-time. Zsh, by default, appends history to a file on exit and reads from it on startup.
# Terminal 1
> ls -l
# Terminal 2
> history 1
1 ls -l
The core problem Zsh’s session management (or lack thereof, in the way some expect) solves is maintaining a consistent, yet flexible, shell environment across multiple interactive sessions and even non-interactive commands. It aims to provide a rich history for recall and a robust mechanism for setting up your environment.
Internally, Zsh uses a history file (by default, ~/.zsh_history) to persist commands across sessions. When a new Zsh session starts, it reads this file. When it exits, it appends the commands from that session to the file. This is why commands entered in one terminal appear in another’s history after the first terminal is closed. Environment variables, on the other hand, are typically set in startup files like ~/.zshenv, ~/.zprofile, ~/.zshrc, and ~/.zlogin. When you launch a new terminal session, the parent process (often your desktop environment or terminal emulator) inherits its environment. Zsh then sources these configuration files, setting up its own environment. Variables exported in one session (export MY_VAR=...) only exist within that session and its child processes, not in parallel, independent sessions.
The setopt command is your primary lever here. setopt SHARE_HISTORY (which is often on by default) means that history is written to the history file incrementally, not just on exit. This allows commands from one session to appear in another’s history command much faster, though still not instantaneously. The setopt INC_APPEND_HISTORY option also contributes to this, appending each command as it’s executed. For environment variables, the solution is to define them in your Zsh startup files. For example, in ~/.zshrc:
# ~/.zshrc
export MY_GLOBAL_VAR="always_set"
This ensures MY_GLOBAL_VAR is set in every new Zsh session you start. If you need variables specific to a particular task or context that you want to isolate, you’d typically set them in a temporary shell or use a tool like direnv which can load/unload environment variables based on the current directory.
The most surprising thing about Zsh’s history management is how it handles duplicate commands and the "unlimited" history. By default, Zsh doesn’t store consecutive duplicate commands if setopt HIST_IGNORE_DUPS is set (which is common). If setopt HIST_IGNORE_ALL_DUPS is also set, it will ignore a command if it has appeared anywhere in the history, not just consecutively. This prevents your history from becoming bloated with repeated commands, making it more efficient to search.
The next concept you’ll likely encounter is managing Zsh plugins and themes using frameworks like Oh My Zsh or Prezto, which build upon these session management fundamentals.