It started with a simple question: how far back does my shell history go? How much of
the years spent in a terminal is still sitting there? Answering it looked like a
single-file job.
$ wc -l ~/.zsh_history
1218 /Users/mustafaerbay/.zsh_history
One thousand two hundred eighteen lines. But how many months is that? That is where I
stalled, because the file contains not a single date — neither the oldest nor the newest
line says when it was written. Looking elsewhere turned up /etc/zshrc in the same
neighbourhood, with these three lines inside:
HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history
HISTSIZE=2000
SAVEHIST=1000
The file is owned by root, mode r--r--r--, last modified on 13 August. This is not a
setting of mine; it is a decision Apple shipped with the machine. And it says: at most
one thousand commands go into that file.
When I finally counted the file properly, that was exactly the number: 1,000. Not
one below the ceiling — precisely on it. Which means this file filled up months ago, and
every command I have typed since then has silently deleted the oldest one.
Every number below was measured on this machine on 2 October 2026: macOS 26.6.2
(25G83), zsh 5.9, arm64. The file shifted underneath me mid-measurement — a terminal
window I had forgotten about closed, and the size went from 52,982 to 53,283 bytes — so
a copy was frozen at 16:47:35, and every figure comes from that single copy.
First I counted it wrong, and the wrong way of counting was the lesson
My first attempt said 939. Not a rounding error but a thesis-inverting one: 939 means
"sixty-one commands left before the ceiling"; 1,000 means "the ceiling is full and the
losses started long ago."
Here is the mistake. zsh records events, not lines; a multi-line command is one event,
with the line breaks marked by backslashes. The correct rule is a single sentence: an
event ends at the first line that does not end in a backslash.
total lines 1218
lines ending in a backslash 218
=> event count 1000
My first script threw away blank lines as "not data." But none of the 61 blank lines in
that file is an independent entry; every one of them sits in the middle of a multi-line
command that happens to contain an empty line. Dropping them removed 61 lines and
stitched the surrounding commands together at the wrong seams.
I verified it with zsh itself rather than with arithmetic alone. Under a temporary
ZDOTDIR, the shell read the file:
$ fc -R hist-snapshot.txt
$ print ${#history}; print $HISTCMD
999
1000
The history array excludes the current event slot, so 999 + 1 = 1,000. The independent
arithmetic agrees. The lesson is cheap to state and expensive to learn: when counting a
file, use the rules of the program that writes it, not your own intuition.
What happens once the ceiling is full?
This one went to the lab, reproduced without touching my real history file, inside a
temporary ZDOTDIR. A file of 995 fake entries, then 20 more commands:
start: 995 entries, first=eski_0001 last=eski_0995
after: 1000 entries, first=eski_0016 last=yeni_20
The total should have been 1,015; the file stopped at 1,000 and the first fifteen
entries were gone. Twenty more commands, and eski_0016 through eski_0035 went the
same way. No error, no warning, no log line. My own file is in exactly this state.
The single-session version is starker. A shell took 1,500 commands into memory, fc -l
counted all of them while the session ran, and then the shell closed:
in memory (fc -l): 1500
on disk: 1000
first line: lab_komut_501
Five hundred never reached the disk. In the zsh documentation HISTSIZE is "the maximum
number of events stored in the internal history list" and SAVEHIST is "the maximum
number of history events to save in the history file." Two numbers, two memories: one is
what you remember during the session, the other is what you still have the next morning.
No entry has a time — but the shell will show you one
How many of the thousand entries carry a timestamp? Zero. zsh writes timestamps in the
: <beginning time>:<elapsed seconds>;<command> form, and only when EXTENDED_HISTORY
is on. The documented default is off, and so is the state on this machine. There is not
a single line starting with HIST in my ~/.zshrc, so this too is an inherited default.
So far this is annoying but honest: if the information is absent, it is absent. The sly
part begins when you ask the shell. I listed a timestamp-free file from two separate
shells four seconds apart, using fc -l -t '%Y-%m-%d %H:%M:%S' so the seconds show:
read 1 — wall clock 16:40:46
1 2026-10-02 16:40:46 eski_komut_bir
2 2026-10-02 16:40:46 eski_komut_iki
read 2 — wall clock 16:40:50
1 2026-10-02 16:40:50 eski_komut_bir
2 2026-10-02 16:40:50 eski_komut_iki
The same two commands, two different times. The value shown is not when the command ran
but when the file was read — not even the file's modification time. I went looking for
this in the documentation and could not find it: fc only defines the format, not what
gets printed for an undated file. So the sentence below rests on measurement, not on the
manual: it fills an empty field and shows it to you.
The rule that falls out has now caught me twice on this blog: a tool showing you a number
does not mean it knows that number. While
counting my laptop's sleep records
the uptime counter offered a reassuring figure, but what it measured was not "how long
I stayed awake," it was "how long since the last reboot."
The order is not chronological either
If there are no timestamps, is the ordering at least right? Two shells. A started first,
wrote three commands, waited three seconds. B started half a second later, wrote three
commands and exited immediately.
[t=0.5] B exited -> [t=3.5] A exited too ->
kabukB_1 kabukB_1
kabukB_2 kabukB_2
kabukB_3 kabukB_3
kabukA_1
kabukA_2
kabukA_3
A's commands were typed first and sit last in the file. Because the definition of
HISTFILE is literally this: "the file to save the history in when an interactive shell
exits." The moment of writing is not when you type the command, it is when you close the
window. Thanks to the default APPEND_HISTORY no session overwrites another; but the
file's order is the order in which I closed terminal tabs.
For anyone working across three screens that means the bottom line is not "what did I do
last," it is "which window did I close last."
A crashed shell remembers nothing
If the definition says "when an interactive shell exits," what does a shell that never
exits do? Five commands, then kill -9:
exit code: 137
ls: .../.zsh_history: No such file or directory
The file was never created. Closing the same shell cleanly wrote three lines properly.
So every time you force-restart the machine without closing your terminal, whatever you
typed in that session is gone — and since the loss leaves no record, I cannot even count
how often it has happened.
The remedy is in the documentation: INC_APPEND_HISTORY adds new lines to $HISTFILE
"incrementally (as soon as they are entered)." Default: off.
Commands the agent runs never arrive at all
One number nagged at me. The claude command appears in only 12 of the thousand
entries, while most of the work on this machine happens inside those twelve sessions.
$ zsh -c 'echo etkilesimsiz_komut_calisti'
etkilesimsiz_komut_calisti
$ zsh s.zsh
betik_icinden
--- history file ---
0 lines
Zero. The definition is explicit again: the file is written only when an interactive
shell exits. Nothing an agent, a script or a cron job runs ever lands in this ledger.
Twelve lines, twelve sessions, and no way to learn from this file how many commands ran
inside them. Shell history is a record not of "what I did" but of "what I typed with my
own hands." Those used to be the same thing.
What do the surviving thousand commands say?
After accounting for the losses, the survivors describe a narrower landscape than I
expected:
| Measurement | Value |
|---|---|
| Event count | 1,000 |
| Multi-line commands | 90 |
| Distinct command names | 90 |
| Share of the top 10 commands | 70.6% |
| Unique command texts | 373 |
| Entries eaten by repetition | 627 (62.7%) |
| Command names used exactly once | 39 |
The ranking: ssh 158, clear 126, bstart 82, cd 80, fstart 71, python 66,
yes 36, git 31, npm 30, sudo 26, seray 22, ping 22, ls 20, scp 20.
Ninety different tools, and a tenth of them cover roughly three quarters of the work.
Thirty-nine commands appear exactly once. All 36 yes lines are copies of a single text
— one load test's invocation — so even the list flatters the variety.
Three of them stand out: bstart 82, fstart 71, seray 22, all shortcuts from my
.zshrc. bstart enters a project directory, activates a virtual environment and starts
a development server; fstart only enters the front-end directory and calls
npm run dev; seray starts nothing at all, it just enters the directory and activates
the virtual environment. Together 175 entries, 17.5% of my history. Three words.
Those 175 lines teach me nothing. Six months from now the history cannot tell me which
command bstart ran, because it is not written there; and if the definition changed, it
will not recall the old one either. A shortcut gives back at reading time what it saved
at typing time.
clear appearing 126 times — exactly an eighth of the ledger — is its own joke: the
command for clearing the screen was recorded as the very noise it was clearing.
Repetition eats two thirds of the ceiling
Of the thousand entries only 373 are distinct text; the remaining 627 lines are
repeats. Think of it as a memory budget: the ceiling is fixed, and 62.7% of it goes to
rewriting something already there. A rough derivation — not measured, but derived from
two measured numbers: with the same distribution, dropping repeats would fit about
2.7 times today's variety into those thousand lines.
zsh offers four switches for this, and all four ship off:
-
HIST_IGNORE_DUPS— removes only an exact repeat of the previous event ("duplicates of the previous event," in the manual's phrasing). The weakest one. -
HIST_IGNORE_ALL_DUPS— "if a new command line being added to the history list duplicates an older one, the older command is removed from the list (even if it is not the previous event)." -
HIST_SAVE_NO_DUPS— "when writing out the history file, older commands that duplicate newer ones are omitted." Memory stays as is; only the disk copy is tidied. -
HIST_EXPIRE_DUPS_FIRST— when room is needed, it causes "the oldest history event that has a duplicate to be lost before losing a unique event from the list." The documentation pairs this with keepingHISTSIZElarger thanSAVEHIST; on macOS that cushion already exists, 2,000 against 1,000. Apple left the pillow and never turned on the switch that lies on it.
The ledger next door has been silent for five months
There is a second ledger in the same directory: ~/.bash_history. 7,207 bytes, 208
lines, 207 entries, 109 of them distinct. Last modified 8 May 2026; not a single line
added in five months.
Its contents belong to a different era: python 52, clear 38, cd 27, rm 11. And 28
lines begin directly with # — instead of deleting a command I had put a hash in front
of it and said "not right now."
That file records nothing any more, because zsh has been the default shell for new
accounts since macOS 10.15, as Apple's own documentation states. But it was never
deleted. Two hundred and seven lines of a frozen period sit there, and unlike their
neighbour they will never be trimmed. The condition for a complete memory, it seems, is
that the memory is no longer in use.
Before enlarging the memory: what is in it?
This file is plain text and it holds everything typed on the command line. Mine is
rw-------, readable only by me. Still, a crude scan was worth running: 23 of the
thousand entries contain one of the words token, secret, password or api key.
Not all 23 carry a real secret — matching a word is enough to be counted — but until the
scan I did not know that either, and asking the question correctly is the point. The good
news: four of them use read -rs, so the secret went into a silent prompt rather than the
command line.
Raising the ceiling also extends the half-life of that content. Three tools help:
-
HIST_IGNORE_SPACEremoves lines whose first character is a space — but the manual adds an important caveat: "the command lingers in the internal history until the next command is entered before it vanishes," so the disappearance is not instant. -
HISTORY_IGNOREtakes a pattern and, "at the time history files are written… any potential history entry that matches the pattern is skipped." Far stronger than the leading-space trick for secret hygiene. - And a one-line rule: shell history is not an audit trail. Who ran what, and when, is not answered here; that needs a command-auditing pipeline such as auditd.
Choosing the default on purpose
There is a list of settings to change at the end of this, but the list is not the point.
Until today shell history looked like a record to me; what I had was a cache, and
someone else had decided its capacity, its ordering and its expiry.
Four questions for your own setup:
-
What is the ceiling?
echo $SAVEHIST. If you see 1,000, that is your system's choice and not yours — roughly a month of memory for someone typing 30-40 commands a day. -
Are timestamps kept? Without
setopt EXTENDED_HISTORYthere is no answer to "when did I do this," and the shell will show you today's clock instead of saying so. -
When does the write happen? On exit, by default.
INC_APPEND_HISTORYwrites as soon as the command is entered;INC_APPEND_HISTORY_TIMEwrites after it finishes and records the duration;SHARE_HISTORYadditionally imports other sessions' lines. The manual puts a clear warning here: "The three options should be considered mutually exclusive." Pick one; do not turn on all three. -
Parallel shells?
APPEND_HISTORYis on by default in zsh, so sessions do not overwrite each other. If the file lives on a network share,HIST_FCNTL_LOCKhands locking to the system'sfcntlcall — "where this method is available" — and the manual says this avoids history corruption on NFS.
There is a more radical route too: atuin, mcfly and hstr keep history in a database
instead of a flat file and remove all four complaints above — no timestamps, tight
ceiling, wrong order, crashed sessions lost — in one move. The price is one more
dependency in your shell.
As this article goes out, the defaults are still running on this machine, because
changing a setting is the job of a decision, not of a measurement. But I now know what
the setting is and what it costs, which I did not before the measurement.
Remembering Is Not a Default
Back to the original question: how far back does this file go? I cannot say. There are no
timestamps, so the age of the oldest line is unknown. The ceiling is full, so anything
before it is already deleted. Crashed sessions were never written. Commands run by the
agent and by scripts never entered. Four separate reasons, and none of them leaves a
record.
The real lesson is not about shell history. Most of the records in our lives work this
way: there is a ceiling, there is a moment of writing, and there is a default handed to
us without being asked. Until we open the ledger we assume it is boundless. Opening it
shows that the ledger reflects not what it holds, but what it was permitted to hold.
Every command typed while writing this article took one of the oldest lines of my history
with it. I will never know which ones.
Official Sources
- zsh — Parameters Used By The Shell (HISTFILE, HISTSIZE, SAVEHIST, HISTORY_IGNORE)
- zsh — History options (APPEND_HISTORY, EXTENDED_HISTORY, INC_APPEND_HISTORY, SHARE_HISTORY)
- zsh — Src/hist.c, the savehistfile implementation
- zsh release tags (5.9.2, 12 July 2026)
- Use zsh as the default shell on your Mac — Apple Support
Top comments (0)