Skip to content

Two terminals, one history file

Level: 201 · you keep two terminal windows open and have lost commands from one of them

One line: Two bash windows on one ~/.bash_history both keep their commands, added in the order the windows close, until one window has run more commands than HISTSIZE. Then the last window to close writes its own list over the file. histappend stops that, and PROMPT_COMMAND='history -a' writes each command at the next prompt. zsh appends in the same way unless APPEND_HISTORY is off, and fish writes every command to the file as it runs.

Measured

A and B are two real interactive bashes open at the same time on one HISTFILE, the way two terminal windows share ~/.bash_history. The script types into each through a fifo and reads back what each prints. It never sleeps: PROMPT_COMMAND=echo makes a shell print an empty line whenever it is ready for its next command, and the script waits for that line before typing anything else. So the events happen in the order they are printed, on every run. A shell exits when the script closes its input, as at Ctrl-D. Each scenario starts from a file holding one line, echo old.

Verified output of histtwo_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

## Nothing set. B exits first, then A
A$ echo from-A
from-A
B$ echo from-B
from-B
(B exits)
(A exits)
$ cat hist
echo old
echo from-B
echo from-A

## HISTSIZE=3 HISTFILESIZE=100, and A runs four commands
A$ echo a1
a1
A$ echo a2
a2
A$ echo a3
a3
A$ echo a4
a4
B$ echo from-B
from-B
(B exits)
(A exits)
$ cat hist
echo a2
echo a3
echo a4

## The same, with histappend (bash -O histappend)
A$ echo a1
a1
A$ echo a2
a2
A$ echo a3
a3
A$ echo a4
a4
B$ echo from-B
from-B
(B exits)
(A exits)
$ cat hist
echo old
echo from-B
echo a2
echo a3
echo a4

## PROMPT_COMMAND='history -a; echo'
A$ echo from-A
from-A
B$ echo from-B
from-B
A$ echo again-A
again-A
(both still open)
$ cat hist
echo old
echo from-A
echo from-B
echo again-A

B$ history -n; history
    1  echo old
    2  echo from-B
    3  history -n; history
    4  echo from-B
    5  echo again-A
(B exits)
(A exits)
  • With nothing set, nothing was lost. Both commands are in the file, B's first because B exited first. Each shell appended the commands it had run to what was already there. The saying that "the last window to close wins" is not what happened here.
  • It is what happens once a window outgrows HISTSIZE. With HISTSIZE=3, A ran four commands, more than its list can hold. At exit it wrote its list over the file, so echo old and B's echo from-B are gone. So is A's own echo a1, which had already dropped out of A's list. This is the rule from The history is a list in memory, and HISTFILESIZE=100 did not prevent it.
  • histappend makes the exit an append, always. echo old and echo from-B survived. echo a1 is still missing, because a shell can only append what its list still holds.
  • PROMPT_COMMAND='history -a' writes as you go. (The script's version adds ; echo for its own ready signal.) With both shells still open, the file already held every command in the order they were typed, whichever window typed them.
  • history -n is less reliable than it looks. It reads "the lines not already read" into B's list. B got its own echo from-B a second time and never got A's echo from-A. That fits bash counting by line number. B had read one line at startup and appended one, so it treated the first two lines as read. echo from-A had become line 2 and was skipped, and reading started at line 3, echo from-B.

On a Mac

The key is shared: /bin/bash 3.2 on the Mac printed the same bytes as bash 5.2 on Ubuntu. The one caveat is from the previous lesson: in bash 3.2, history -a, and so PROMPT_COMMAND='history -a', wrote nothing in a session that had started with no history file or an empty one.

Terminal.app changes the picture, for zsh and for bash. None of the examples run inside Terminal.app, so what follows is read from the two files it relies on, /etc/zshrc_Apple_Terminal and /etc/bashrc_Apple_Terminal on macOS 26. They are not measured output. macOS's /etc/zshrc and /etc/bashrc load them when TERM_PROGRAM is Apple_Terminal. There is no such file for any other terminal on this Mac, and none on Linux.

  • Terminal gives each window a TERM_SESSION_ID. A zsh in that window saves the window's new commands to ~/.zsh_sessions/<id>.historynew, and at exit appends them both to the shared HISTFILE and to a per-window <id>.history. That per-window file is what a window restored by Terminal's Resume reads back, so a reopened window gets its own history rather than everyone's. bash does the same under ~/.bash_sessions.
  • So in Terminal.app, two windows' histories are appended to the shared file at exit, never written over it, and the file's comments recommend raising HISTSIZE and SAVEHIST because of it.
  • The zsh version turns itself off when INC_APPEND_HISTORY, INC_APPEND_HISTORY_TIME or SHARE_HISTORY is set, or SHELL_SESSION_HISTORY=0. SHELL_SESSIONS_DISABLE=1 turns off the whole session mechanism. The bash version turns itself off when histappend is set or HISTTIMEFORMAT is set, and entirely when the file ~/.bash_sessions_disable exists.
  • The Saving session... and ...saving history...truncating history files... lines a Terminal window prints as it closes come from these scripts.

In zsh and fish

zsh appends at exit by default, has an option that makes the last exit win, and two options that write as you go. A and B here are zsh -d -i with a scratch .zshrc that sets HISTFILE and SAVEHIST and defines precmd() { echo }, the zsh spelling of the empty-line ready signal:

Verified output of histtwo_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

## Nothing set. B exits first, then A
A% echo from-A
from-A
B% echo from-B
from-B
(B exits)
(A exits)
$ cat hist
echo old
echo from-B
echo from-A

## unsetopt appendhistory (zsh +o appendhistory)
A% echo from-A
from-A
B% echo from-B
from-B
(B exits)
(A exits)
$ cat hist
echo old
echo from-A

## setopt incappendhistory (zsh -o incappendhistory)
A% echo from-A
from-A
B% echo from-B
from-B
A% echo again-A
again-A
(both still open)
$ cat hist
echo old
echo from-A
echo from-B
echo again-A

A% fc -ln 1
echo old
echo from-A
echo again-A
(B exits)
(A exits)
$ cat hist
echo old
echo from-A
echo from-B
echo again-A
fc -ln 1

## setopt sharehistory (zsh -o sharehistory)
A% echo from-A
from-A
B% echo from-B
from-B
A% echo again-A
again-A
(both still open)
$ cat hist
echo old
: <epoch>:0;echo from-A
: <epoch>:0;echo from-B
: <epoch>:0;echo again-A

A% fc -ln 1
echo old
echo from-A
echo from-B
echo again-A
(B exits)
(A exits)
$ cat hist
echo old
echo from-A
echo from-B
echo again-A
fc -ln 1
  • Nothing set: the same as bash. Both commands survive, in the order the shells exited. APPEND_HISTORY is on by default.
  • unsetopt appendhistory: the last shell to exit wins. A wrote its list over the file, and B's echo from-B is gone.
  • setopt incappendhistory: each command reaches the file as it is typed, so with both shells open the file holds all three, in typed order. A's own list, fc -ln 1, still has only A's commands.
  • setopt sharehistory: the file is written as you go, and A reads it back. A's fc -ln 1 includes B's echo from-B. While both shells run, the lines it writes carry the : <epoch>:0; prefix of zsh's extended format, which Three history file formats takes apart.

fish writes each command into the file as it runs. A here is an interactive fish at a terminal, typed into by fish_at_a_terminal.py. Halfway through, A's second command starts B, another interactive fish at another terminal, through the same helper:

Verified output of histtwo_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ python3 fish_at_a_terminal.py "echo from-A" "python3 fish_at_a_terminal.py 'echo from-B'" "echo again-A"

$ string replace -r "when: \d+" "when: <epoch>" < ~/.local/share/fish/fish_history
- cmd: echo from-A
  when: <epoch>
- cmd: python3 fish_at_a_terminal.py 'echo from-B'
  when: <epoch>
  paths:
    - fish_at_a_terminal.py
- cmd: echo from-B
  when: <epoch>
- cmd: echo again-A
  when: <epoch>

The file holds all four commands in the order they ran: A's, then the command that started B, then B's, then A's again. There is no exit step at which one fish could write over another. What a running fish shows of another session's commands is a separate question. fish's history merge is the command for reading them into a running session, and this page does not measure it. The key is shared, so this is also what fish 4.3.2 does on a Mac.

If you are coming from another library

See also