Skip to content
Dev Tools Article

Zsh Has Been Silently Eating Shell Histories Since 2015

A stray Ctrl+C at shell exit could truncate your history file; the fix ships in 5.9.2, which macOS doesn't have.

Lenn Voss
Lenn Voss
Cloud & Infrastructure Writer · Aug 16, 2026 · 4 min read
Zsh Has Been Silently Eating Shell Histories Since 2015

For roughly a decade, Zsh has carried a bug that can silently throw away most of your shell history. The trigger is mundane: a Ctrl+C that lands while a shell is exiting. No error, no corruption marker, no .zsh_history.bak — the file is simply shorter than it was, and you find out weeks later when that one openssl incantation you always recall with Ctrl+R is gone.

Michael Stapelberg root-caused it, reported it to the zsh-workers list in March 2025 with a reproducer that shrank a 55,075-line history file to 12,962 lines, and Bart Schaefer wrote the fix. It shipped in zsh 5.9.2 on July 12, 2026 — which means the clock on your exposure is still running, because almost nothing you're typing into today runs 5.9.2 yet.

How an atomic write loses your data atomically

Zsh doesn't just append to ~/.zsh_history. When a shell exits, savehistfile() rewrites the whole file: it re-reads the on-disk history (to merge entries from other sessions), trims to SAVEHIST, writes the result to a temporary file, and renames it over the original. Write-then-rename is the textbook crash-safe pattern, and that's the dark joke here — the rename works perfectly.

The hole is upstream of it. A commit from March 2015 made readhistfile() interruptible: if SIGINT arrives mid-read, the loop breaks early and the in-memory history is whatever fraction got read. savehistfile() never got the matching check. So if you're the kind of person who mashes Ctrl+C followed by Ctrl+D to close a terminal — a common reflex — a signal can land during that exit-time re-read, and zsh will faithfully, atomically install a truncated history over your real one. A lasthist.interrupted flag added in 2019 was supposed to make interrupted reads safer, but the exit path ignored it. Schaefer's patch finally makes the save path check for interruption before replacing the file; it missed the 5.9.1 release in May 2026 and landed in 5.9.2 six weeks later.

The window is proportional to your history size. Stapelberg runs SAVEHIST=10000000 with a multi-megabyte history file, so the exit-time read takes long enough to catch stray signals. Small histories make the race hard to hit — which is exactly why this survived ten years as scattered, unreproducible "zsh ate my history" reports.

The debugging ladder is the real tutorial

The methodology is worth stealing for any "something is eating my file" mystery, because it escalates cleanly:

  1. inotifywait established the shape: the file wasn't truncated in place, it was replaced via temp-file-and-rename — implicating zsh's own save logic rather than some rogue process.
  2. fatrace added process attribution, but not data volumes.
  3. bpftrace produced the smoking gun at the syscall level: in the bad case, reads on the history file stopped around the 11.5 MB mark without ever hitting EOF, then the write side proceeded anyway.
  4. Finally, a patched zsh that deliberately crashed when about to write a suspiciously short history, plus systemd-coredump, froze the crime scene: the core dump showed the interrupt flag set and lasthist.interrupted = 1.

Watch the file, name the process, count the bytes, then trap the program state. Most filesystem heisenbugs fall to some prefix of that ladder.

Your terminal probably still has this bug

Check with zsh --version. Arch already ships 5.9.2. Most other distros, and every macOS box, do not: macOS Tahoe still bundles zsh 5.9 from 2022 as the default shell, and Apple historically updates the bundled shell only when it feels like it. If you live in zsh on a Mac, install a current one via Homebrew and chsh to it, or accept the race and adjust your habits.

Until you're on 5.9.2, three cheap mitigations:

  • Stop interrupting exiting shells. The Ctrl+C-then-Ctrl+D logout reflex is the exact trigger. Let the shell die on its own.
  • Back up the file. It's one flat file; there is no excuse. A daily cp ~/.zsh_history ~/backup/zsh_history.$(date +%F) from cron or launchd turns a catastrophic loss into a one-day gap.
  • Don't export HISTFILE. Stapelberg documents a separate footgun: an exported HISTFILE means child shells — including ones spawned by tools like Emacs TRAMP — open your history with their default SAVEHIST, which can be tiny, and trim your file accordingly. printenv HISTFILE should print nothing.

Shell history is a database nobody treats like one

Step back and the design looks strange: every zsh exit runs what is effectively a full database compaction — read everything, merge, trim, rewrite — in-process, interruptible, with no journal and no post-write sanity check. For a file that many developers treat as institutional memory and some treat as an audit trail, that's a thin durability story. And the pipeline for fixing it is slow even when it works: sixteen months from a root-caused report with a reproducer to a shipped release, on top of a four-year gap between zsh 5.9 and 5.9.1. That's not a knock on volunteer maintainers; it's a fact about infrastructure we bet daily workflow on, and it should inform how much you trust the exit path of any shell.

It's also the strongest argument I've seen for moving history out of the shell's hands entirely. Tools like Atuin store history in SQLite — real transactions, sync across machines, and your shell process crashing or catching a signal can't vaporize the store. You don't need to go that far. But do one of two things this week: get on 5.9.2, or put ~/.zsh_history under backup. The next hand-rolled file-rewrite bug won't announce itself either.

Sources & further reading

  1. Tracking down a Zsh history data loss bug — michael.stapelberg.ch
  2. BUG: Zsh loses history entries since 2015 — zsh.org
  3. News about ZSH — zsh.sourceforge.io
  4. Arch Linux zsh 5.9.2-1 package — archlinux.org
Lenn Voss
Written by
Lenn Voss · Cloud & Infrastructure Writer

Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading