Shell History

Every command you type into Bash gets recorded in a history list so you can recall it later without retyping it. This isn’t just a scrollback buffer — it’s a numbered list kept in memory during your session and normally saved to a file on disk when the shell exits, which is why the commands you ran yesterday are still available today. Understanding how that list is built, filtered, and persisted lets you search your past work instantly, safely avoid leaking secrets into a plaintext file, and keep history in sync across multiple open terminals.

Overview / How it works

When you start an interactive Bash session, Bash reads the file named by the HISTFILE variable (by default ~/.bash_history) into an in-memory list called the history list. As you type commands, each one is appended to that in-memory list and given a sequence number. This is purely a Bash/readline feature — the Linux kernel has no idea what “history” is; it’s the shell keeping its own bookkeeping.

Two variables control size, and they control different things:

  • HISTSIZE — how many commands are kept in the in-memory list for the running session.
  • HISTFILESIZE — how many lines are kept in the history file on disk.

When an interactive shell exits normally, Bash writes the in-memory list back out to $HISTFILE. By default this overwrites the file with the shell’s own list. If you enable the histappend shell option (shopt -s histappend), Bash instead appends only the new lines from this session, which matters a lot once you regularly have more than one terminal open (see Common Mistakes).

The history builtin lets you inspect and manipulate both the in-memory list and the file directly, with flags like -a (append new lines to the file), -r (read the file into memory), -w (write memory to the file), and -c (clear the in-memory list). Combining these is how you build real-time history sharing between terminals.

Bash also supports history expansion — the classic !!, !n, !string notation. This is handled by the history library before your command line is parsed and executed, and only when the shell is interactive and the histexpand option is on (it is by default in interactive shells). That’s why bang-history never works inside a script file: non-interactive shells don’t run the expansion pass at all.

Finally, because ~/.bash_history is a plain text file (Bash creates it with mode 600, readable/writable only by its owner), anything you type on the command line — including passwords passed as arguments — can end up sitting on disk in plaintext. HISTCONTROL, covered below, is your main defense against that.

Syntax

history [n]
history -c
history -d <offset>
history -a
history -r
history -w
!!
!n
!string
!$
Form Meaning
history List the whole in-memory history, numbered.
history n List only the last n entries.
history -c Clear the in-memory history list (does not touch the file until you also write it).
history -d offset Delete a single entry by its history number.
history -a Append history lines from this session, not yet written, to $HISTFILE.
history -r Read $HISTFILE and add its contents to the current in-memory list.
history -w Write the entire current in-memory list out to $HISTFILE, overwriting it.
!! Re-run the previous command.
!n Re-run history entry number n.
!string Re-run the most recent command that started with string.
!$ The last argument of the previous command (useful inside a new command, e.g. cat !$).

Key variables, usually set in ~/.bashrc:

Variable Purpose
HISTSIZE Number of commands kept in memory for the session.
HISTFILESIZE Number of lines kept in the history file on disk.
HISTCONTROL Colon-separated filters: ignorespace (skip lines starting with a space), ignoredups (skip an exact repeat of the previous line), ignoreboth (both), erasedups (remove all earlier duplicates when a repeat is added).
HISTIGNORE Colon-separated glob patterns of commands to never record, e.g. "ls:cd:history".
HISTTIMEFORMAT A strftime format string; when set, history prefixes each entry with a timestamp.
HISTFILE Path to the history file; default ~/.bash_history. Unset it to disable saving entirely.

Examples

Example 1: Recalling and re-running a command

tail -100 /var/log/app.log
sudo !!

Output:

Aug  4 09:01:12 web1 app[2311]: INFO request served in 42ms
Aug  4 09:01:13 web1 app[2311]: INFO request served in 39ms
$ sudo !!
sudo tail -100 /var/log/app.log
[sudo] password for alice:
Aug  4 09:01:12 web1 app[2311]: INFO request served in 42ms
Aug  4 09:01:13 web1 app[2311]: INFO request served in 39ms

The first tail works fine but the log turns out to require root to read the rest of the directory. Rather than retyping the whole line, sudo !! expands !! to the entire previous command and runs it prefixed with sudo. Bash always echoes the expanded line before executing it, so you can see exactly what will run.

Example 2: Configuring safer, timestamped history

# ~/.bashrc - history settings
HISTCONTROL=ignoreboth:erasedups
HISTSIZE=5000
HISTFILESIZE=20000
HISTTIMEFORMAT="%F %T  "
shopt -s histappend
source ~/.bashrc
history | tail -5

Output:

  601  2026-08-04 09:12:03  cd ~/projects/api
  602  2026-08-04 09:12:10  git status
  603  2026-08-04 09:14:55  npm test
  604  2026-08-04 09:15:40  git commit -m "fix: retry logic"
  605  2026-08-04 09:16:02  history | tail -5

ignoreboth skips exact duplicate lines and anything starting with a space; erasedups also removes older copies of a command when it’s repeated, so your history stays useful instead of filling up with fifty identical ls entries. HISTTIMEFORMAT makes every listed entry show when it actually ran.

Example 3: Sharing history live across multiple terminals

# ~/.bashrc - sync history across all open terminals in real time
shopt -s histappend
PROMPT_COMMAND="history -a; history -c; history -r; ${PROMPT_COMMAND}"
history | grep rsync

Output:

  742  2026-08-04 08:40:11  rsync -avz ~/projects/api/ deploy@example.com:/srv/api/
  789  2026-08-04 10:05:33  rsync -avz --delete ~/site/dist/ deploy@example.com:/var/www/site/

The PROMPT_COMMAND runs before every prompt is displayed. history -a appends this shell’s new lines to the file, history -c clears the in-memory copy, and history -r reloads the (now updated) file back into memory — including any lines other terminals just appended. The net effect: a command typed in one terminal shows up in history in another terminal almost immediately, instead of only after that terminal is closed.

How it works step by step

  1. You open a terminal. Bash starts an interactive shell and reads $HISTFILE into the in-memory history list.
  2. You type a command. Before execution, if histexpand is on (default), Bash’s history library scans the line for event designators like !! and expands them, echoing the result.
  3. The command runs. Afterward, Bash decides whether to record it, applying HISTCONTROL and HISTIGNORE filters — a command starting with a space is dropped if ignorespace/ignoreboth is set, for example.
  4. If it passes the filters, the line is appended to the in-memory list with the next sequence number (and a timestamp, internally, if HISTTIMEFORMAT is set).
  5. If your PROMPT_COMMAND calls history -a, the new line is also flushed to $HISTFILE immediately, rather than waiting for the shell to exit.
  6. When the shell exits, Bash writes the in-memory list to $HISTFILE — overwriting the whole file by default, or appending only new entries if histappend is set.

Common Mistakes

Mistake 1: Unescaped ! triggers history expansion inside double quotes

echo "Deploy successful!"

Output:

bash: !": event not found

Double quotes don’t protect ! from history expansion the way single quotes protect other special characters — Bash sees !" and tries to look up a history event named ". Escape the bang or turn expansion off for the session.

echo "Deploy successful\!"

Mistake 2: Assuming other open terminals see your commands automatically

# Terminal A: working all morning, 40 commands typed
# Terminal B: opened later, 3 commands typed, closed first
# Terminal A closes last -> overwrites ~/.bash_history with only
# what Terminal A knows about, silently dropping Terminal B's work

Without histappend, each shell overwrites the whole file on exit with its own list, so whichever terminal closes last wins and earlier terminals’ unique commands vanish. Even with histappend, terminals still don’t share history live unless you also sync it, as shown in Example 3.

shopt -s histappend
PROMPT_COMMAND="history -a; history -c; history -r; ${PROMPT_COMMAND}"

Mistake 3: Secrets end up in plaintext history

mysql -u root -p"<YOUR_PASSWORD>" -e "SHOW DATABASES;"

Anything typed as a command-line argument gets saved to ~/.bash_history in plaintext by default, readable by anyone who gets access to that file or a backup of it. Prefer prompts, environment variables loaded from a restricted file, or the ignorespace trick (a leading space skips recording, if HISTCONTROL includes it):

mysql -u root -p
 curl -H "Authorization: Bearer <YOUR_TOKEN>" https://api.example.com/status

Best Practices

  • Set shopt -s histappend and HISTCONTROL=ignoreboth (or ignoreboth:erasedups) in ~/.bashrc so history survives multiple terminals cleanly.
  • Add HISTTIMEFORMAT so you can tell when a command ran, not just what it was — invaluable when reconstructing what you did during an incident.
  • Never pass passwords or tokens as bare command-line arguments; use an interactive prompt, an environment variable, or a file with mode 600 instead.
  • If a secret does leak into history, remove it with history -d <offset> and edit or delete the matching line in ~/.bash_history as well.
  • Use Ctrl+R (reverse incremental search) to find a command from weeks ago instead of scrolling with the Up arrow.
  • Keep HISTFILESIZE generous but bounded (a few thousand to tens of thousands) — an unbounded file is slow to search and hard to reason about.
  • Treat history as a convenience for recall, not a substitute for real scripts — anything you reuse regularly belongs in a script or shell function.

Practice Exercises

  1. Edit your ~/.bashrc so that duplicate commands and commands starting with a space are never saved, entries are timestamped, and up to 10,000 commands persist across sessions. Reload your shell and confirm with history | tail.
  2. Open two terminals. In the first, run a couple of commands; in the second, run a couple more. Configure real-time history sharing (Example 3) so that history | tail -2 in either terminal shows the other terminal’s most recent command.
  3. Find the exact history line number of an earlier curl command using history | grep curl, re-run it with !n, and then delete it from history with history -d if it contained a sensitive query parameter.

Summary

  • Bash keeps a numbered in-memory history list per session and persists it to $HISTFILE (usually ~/.bash_history) on exit.
  • HISTSIZE limits entries kept in memory; HISTFILESIZE limits lines kept on disk — they’re independent.
  • HISTCONTROL (ignorespace, ignoredups, ignoreboth, erasedups) and HISTIGNORE filter what actually gets recorded.
  • Event designators like !!, !n, !string, and !$ let you recall and reuse previous commands without retyping them — interactive shells only.
  • shopt -s histappend plus a PROMPT_COMMAND using history -a; history -c; history -r keeps history synced across multiple open terminals.
  • The history file is plaintext and can contain sensitive data — avoid passing secrets as command-line arguments, and clean up if one leaks in.