Engineering 29 min read

The Atomic Rename Was Perfect. The File It Wrote Was Half Your Notes.

MMNMNOTE
atomic renamePOSIXdurabilityfile I/Odata losslocal-firstzsh

rename() guarantees that a name always points to a complete file — the old one or the new one. It guarantees nothing about whether the new file holds all your data. A Zsh bug lost 42,113 of 55,075 history lines through a flawless rename, and the shell exited with status 0.1

Write to a temp file, fsync it, rename it over the original. It is the correct recipe. It is what the man pages say, what the durability posts say, and what you get back when you ask anything — human or machine — how to save a file safely. It is also only half a rule, and the missing half is the half that protects your data rather than your reader.

This post is the second half of that rule. The first half — the append-only log that compacts by rewriting and renaming, never by editing in place — is correct, and this post quotes it back approvingly.2 What neither that post nor the standard ever claims is that the file you are swapping in contains everything it should. That claim has to be earned separately, by checking the read.


What POSIX actually promises about a rename

Read the guarantee precisely and it is a statement about a name — about which file that name refers to while the swap is happening. POSIX.1-2017 promises that a link named new stays visible throughout the operation and points either to the old file or the new one. It makes no claim whatsoever about what is inside either file.

The clause itself: "If the link named by the new argument exists, it shall be removed and old renamed to new. In this case, a link named new shall remain visible to other threads throughout the renaming operation and refer either to the file referred to by new or old before the operation began."3

Every word there is about which file a name points to. Not one word is about the contents of either file.

That is not a defect in the standard. It is the standard doing exactly its job: guaranteeing that no observer can catch the filesystem mid-swap. The recipe built on it — write elsewhere, then swap — inherits precisely that guarantee and no more. A reader never sees a torn file. A reader may very well see a short one.

The distinction matters because the two failures look identical from the outside and have opposite causes. Corruption is a commit problem. Truncation is a payload problem. The recipe fixes the first.

Note also that POSIX.1-2017 says threads, not processes. It is the kind of word that gets upgraded when the clause is quoted from memory.

flowchart TD
  A[Read the old file<br/>into memory] --> B{Read reached<br/>end of file?}
  B -->|Nothing asks| C[Write records<br/>to a temp file]
  B -->|Guard added| G[Decline to write<br/>keep the old file]
  C --> D[Flush and fsync<br/>the temp file]
  D --> E[Rename temp over<br/>the original]
  E --> F[Commit succeeds<br/>exit status 0]

Figure: The save-by-rewrite pipeline. A program reads the old file into memory, writes the records it collected to a temp file, flushes and fsyncs that file, and renames it over the original. The rename is the only step POSIX makes any promise about. The diamond is the question most implementations never ask — whether the read that filled memory actually reached the end of the file. Without it, an incomplete read flows straight through three correct durability steps and commits successfully. With a guard on that branch, the program declines to write and keeps the old file intact.

The diamond is where this post lives. Everything below it is already right.


The bug: an interrupted read feeding an uninterrupted write

Zsh rewrites your history file on exit by reading the existing file, merging it with the session, and writing the result. In March 2025, Michael Stapelberg traced a history-loss bug to that pipeline and described the seam in one sentence: the read learned how to stop early, and the write never learned to ask whether it had.

His words, from the investigation: "readhistfile could be interrupted when a signal fires (it checks errflag & ERRFLAG_INT and short-circuits its read loop), but savehistfile did not check for interruption when writing the shell history when exiting. Therefore, savehistfile wrote the (incomplete) history, truncating the actual history."4

Nobody wrote a data-loss bug here. The interruptible read arrived in commit f1c702f2a4159409b27b9576999614a69a51987d on 29 March 2015, authored by Peter Stephenson under the subject "34817: Catch some errors earlier when reading history."5 That commit was a bug fix. It made a read loop respond to a signal instead of grinding on — which is what you want a read loop to do.

The defect lives in the seam between two functions, not in either one. One side gained the ability to stop early. The other side kept assuming a complete result. Neither change was wrong; the pair was.

Stapelberg filed the report on 25 March 2025, which puts 9.99 years between the commit and the report.6 His own summary of that interval: "It's remarkable that a bug like this one, which causes data loss, can remain unfixed for 10 years in a popular shell".7


The syscall traces: one missing line

The cheapest evidence in the whole investigation is a diff between two syscall traces. Stapelberg ran a bpftrace program permanently in the background, capturing every open, read, write, and rename against his history file. A healthy logout and a lossy logout then produced traces whose one meaningful difference is a line that is missing.8

Here is the healthy shutdown:

zsh(231222) openat: /home/michael/.zsh_history flags 0 mode 0
zsh(231222) lseek fd 3 offset 0 whence 1
zsh(231222) read = 0
zsh(231222) close 3 (reads: 52895744, writes: 0)
zsh(231222) unlink /home/michael/.zsh_history.new
zsh(231222) openat: /home/michael/.zsh_history.new flags c1 mode 180
zsh(231222) close 3 (reads: 0, writes: 52888907)
zsh(231222) rename:/home/michael/.zsh_history.new -> /home/michael/.zsh_history

And the lossy one:

zsh(231233) openat: /home/michael/.zsh_history flags 0 mode 0
zsh(231233) lseek fd 3 offset 0 whence 1
zsh(231233) lseek fd 3 offset 0 whence 1
zsh(231233) lseek fd 3 offset 11572944 whence 0
zsh(231233) close 3 (reads: 11575296, writes: 0)
zsh(231233) unlink /home/michael/.zsh_history.new
zsh(231233) openat: /home/michael/.zsh_history.new flags c1 mode 180
zsh(231233) close 3 (reads: 0, writes: 11572944)
zsh(231233) rename:/home/michael/.zsh_history.new -> /home/michael/.zsh_history

Stapelberg's annotation is the whole diagnosis: "Note how there is no read = 0 line, i.e. Zsh does not read until EOF".9

A read returning 0 is how a program learns it has reached the end of a file. In the healthy trace it is there. In the lossy trace it is missing, and the process wrote 11,572,944 bytes where the healthy run wrote 52,888,907 — 21.9% of the payload, derived from those two figures.10

Then both traces end the same way: rename over the original. Same call, same success, same guarantee honored in full.

That stray lseek fd 3 offset 11572944 whence 0 in the lossy trace is not noise, either. POSIX requires it: "If the file is not already at EOF, and the file is one capable of seeking, the file offset of the underlying open file description shall be set to the file position of the stream if the stream is the active handle to the underlying file description."11

The C library, closing a stream that never reached EOF, dutifully repositions a file descriptor that is about to be discarded. That lseek is the system saying — in the one place nobody reads — that the read stopped early. The information existed. Nothing above it asked.


The core dump: two frames that disagree by 5,637 entries

The trace shows the byte counts. A core dump shows the intent. Stapelberg patched Zsh to crash deliberately whenever it was about to commit a history file with fewer than 50,000 lines, installed systemd-coredump, and waited for the failure to happen naturally.12

The backtrace that came back is worth reading as a comparison across two stack frames, not one. Frame #0 is the inner call to savehistfile, at hist.c:3086:

#0  0x000056040d781e19 in savehistfile (fn=0x56040f7a76b0 "/home/michael/.zsh_history", err=1, writeflags=0) at hist.c:3086
        lines_written = 45546
        tmpfile = 0x5604100ec210 "/home/michael/.zsh_history.new"
        xcurhist = 45546
        ret = 10

Frame #1 is the outer call that invoked it, at hist.c:3121:

#1  0x000056040d781f72 in savehistfile (fn=0x56040f7a76b0 "/home/michael/.zsh_history", err=1, writeflags=32771) at hist.c:3121
        lines_written = 0
        tmpfile = 0x0
        xcurhist = 51183
        ret = 0

The two frames disagree. The outer frame knew about 51,183 history entries. The inner frame — the one holding the temp file that is one rename away from becoming your history — had written 45,546. That is 5,637 entries short, and nothing in the program is in a position to notice, because the number that would prove the shortfall lives in a different stack frame from the number that gets committed.

State a comparison like that carelessly and it reads as a contradiction inside one function. It is not. It is two frames, and that is precisely what makes it invisible from the inside.


The reproducer: 42,113 lines lost, exit status 0

The sharpest number in this story is not in any trace. It is in the bug report, where Stapelberg reduced the failure to a deterministic recipe: patch Zsh to slow the history read, start a shell, exit it, and press Ctrl+C while the read is running.13

His transcript, condensed to the three lines that matter:

% wc -l ~/.zsh_history
55075 /home/stapelberg/.zsh_history
# ... start zsh, press Ctrl+D to exit, press Ctrl+C during the read ...
~/src/zsh-repro % echo $?
0
~/src/zsh-repro % wc -l ~/.zsh_history
12962 /home/stapelberg/.zsh_history

His next line: "Oh no! We just lost most of our history :-("14

Read the middle command again. echo $? printed 0. The shell reported a clean exit over a loss of 42,113 lines — 76.5% of the file, derived from those two counts. Every layer told the truth as it understood it. The write succeeded. The rename succeeded. The process exited normally. The only false statement in the entire sequence was the implicit one: that the file now on disk is your history.

This is why the symptom is so hard to spot. As Stapelberg described it, "there was no visible corruption in the .zsh_history (no non-printable characters or incomplete lines of text), and that the number of lines in the file was not always the same."15

Nothing is malformed. Every line that survived is a real line. Recovery tools have nothing to flag, because there is nothing wrong with the file — there is only something missing from it, and only you know what.

Root cause, stated by the reporter: "Fundamentally, I think the savehistfile() function, when called because Zsh is exiting, is not handling an interrupted readhistfile() call correctly."16


Reproducing the shape in thirty-five lines of Python

The failure is not a Zsh failure. It is a shape, and the shape survives translation into any language with a read loop and a save routine. Here it is in Python, with a textbook-correct atomic commit — flush, fsync, os.replace — sitting downstream of a read that gets interrupted.

# nn332_demo.py -- an interrupted READ, then a flawless COMMIT.
import os, sys, signal

def build(path, n=50000):
    with open(path, "w") as f:
        for i in range(1, n + 1):
            f.write("line %d\n" % i)

interrupted = False
def on_sigint(signum, frame):
    global interrupted
    interrupted = True
signal.signal(signal.SIGINT, on_sigint)

def compact(path, guard):
    records = []
    with open(path) as src:                  # <- the READ
        for line in src:
            records.append(line)
            if len(records) == 12962:        # stand-in for the signal arriving mid-read
                os.kill(os.getpid(), signal.SIGINT)
            if interrupted:
                break                        # short-circuit, exactly like readhistfile()
    if guard and interrupted:
        return -1                            # the guard: refuse to commit
    tmp = path + ".new"
    with open(tmp, "w") as dst:
        dst.writelines(records)
        dst.flush()
        os.fsync(dst.fileno())               # durable...
    os.replace(tmp, path)                    # ...and atomic. Both correct. Both useless here.
    return 0

path, guard = sys.argv[1], sys.argv[2] == "guard"
build(path); before = sum(1 for _ in open(path))
ret = compact(path, guard); after = sum(1 for _ in open(path))
print("guard=%-5s before=%d after=%d lost=%d ret=%d"
      % (guard, before, after, before - after, ret))

Run on Python 3.12.3, Linux 6.8.0-124-generic, on 2026-08-17:

$ python3 nn332_demo.py hist.txt noguard
guard=False before=50000 after=12962 lost=37038 ret=0
$ echo $?
0
$ python3 nn332_demo.py hist.txt guard
guard=True  before=50000 after=50000 lost=0 ret=-1
$ echo $?
0

The unguarded run destroyed 37,038 of 50,000 lines while fsync returned successfully, os.replace returned successfully, compact() returned 0, and the process exited 0. Every durability primitive worked exactly as documented.

The guarded run lost nothing. The difference between the two is three lines of logic placed above the write, not below it.


The fix: three lines that decline to commit

The upstream patch is small enough to read in full. Bart Schaefer authored it on 15 April 2025, changing Src/hist.c by eight added and five removed lines, plus three lines of ChangeLog.17 The second hunk is the entire thesis of this post:

@@ -3092,7 +3093,9 @@ savehistfile(char *fn, int err, int writeflags)
 		hist_ignore_all_dups |= isset(HISTSAVENODUPS);
 		readhistfile(fn, err, 0);
 		hist_ignore_all_dups = isset(HISTIGNOREALLDUPS);
-		if (histlinect)
+		if (errflag & ERRFLAG_INT)
+		    ret = -1;
+		else if (histlinect)
 		    savehistfile(fn, err, 0);
 
 		pophiststack();

Before: if we have lines, write them. After: if the read was interrupted, return an error and write nothing; otherwise, if we have lines, write them. The write path finally asks the read path a question.

Schaefer's diagnosis on the mailing list names the assumption that had gone unexamined: "When savehistfile() is doing straightforward write-back, it only stops if fprintf() or fputc() returns -1."18

A write loop that stops only on a write error is a write loop that cannot conceive of a bad payload. He widened the blast radius in the same message — the interrupt is not the only signal that gets you here: "a SIGHUP here certainly appears as if it would leave a truncated history file if HIST_SAVE_BY_COPY is not set, and a SIGINT probably so."19

Ten years of intermittent data loss, closed by three lines that decline to act on an incomplete read.


Why a working patch took 412 days to reach a release branch

A fix that exists is not a fix that ships. The commit was authored on 15 April 2025 and landed on the release branch on 1 June 2026 — 412 days later, derived from the two commit timestamps.17 The obvious reading is that somebody sat on it. The evidence says otherwise, and the honest version is more interesting.

Zsh 5.9 was announced on 16 May 2022. The next release of any kind, 5.9.1, was announced on 31 May 2026.20 21 That is 1,476 days — just over four years — with no Zsh release at all. A patch written in April 2025 had no release train to catch until May 2026.

The release engineer, dana, said as much in the 5.9.2 announcement of 12 July 2026: "this is a stable maintenance release consisting mainly of bug fixes that were missed in 5.9.1".22 Counted to that announcement rather than to the branch, the interval a user actually waited was 453 days.17 22

This matters beyond Zsh. The gap between fixed upstream and fixed on your machine is a real interval with its own failure modes, and it is usually longer than the fix itself took to write. Zsh is not an obscure dependency, either — Apple's own documentation states that "Starting with macOS 10.15, your Mac uses zsh as the default login shell and interactive shell."23


How often does this actually happen?

Rarely, and the honest answer is worth more than the alarming one. Rename atomicity is not where most applications break. Thanumalayan Sankaranarayana Pillai and colleagues at the University of Wisconsin–Madison studied eleven widely-used systems for crash-consistency bugs and reported the opposite of the intuitive result.

Their §4.4.2 finding, verbatim: "Databases and key-value stores do not employ atomic renames extensively; consequently, we observe non-atomic renames affecting only three of these applications (GDBM, HSQLDB, LevelDB)."24 The rename is not the usual culprit. It was not the culprit here either — here it worked.

The broader finding is the one that generalizes: across those eleven mature systems the study found a total of 60 vulnerabilities.25 The abstract states the reason plainly: "We find that applications use complex update protocols to persist state, and that the correctness of these protocols is highly dependent on subtle behaviors of the underlying file system, which we term persistence properties."26

The incidence of this particular Zsh bug was low, and Stapelberg says so himself immediately after calling it remarkable: most users, he writes, probably do not share his habit of killing shell sessions in a way that makes a SIGINT likely — though he adds that he has to imagine some users have lost parts of their history.7

Anyone using Zsh normally could go a decade without losing a line. The class of bug is the general lesson; the individual bug was rare.

The guard is also not free. Returning -1 and declining to write means that on an interrupt you keep the old file and lose the session's new entries instead of the whole history. That is a trade, not a free win — a small, bounded, recent loss in exchange for never taking an unbounded, silent, historical one. It is the right trade, and it is still a trade.

And the layer below has its own version of the problem. Anthony Rebello and colleagues characterized three Linux file systems and five applications under fsync failure and reported: "Our findings show that although applications use many failure-handling strategies, none are sufficient: fsync failures can cause catastrophic outcomes such as data loss and corruption."27 28 There, the error is reported and nobody handles it correctly. Here, no error is reported at all.


The clause to add to write-temp-then-rename

Keep the recipe. Add one clause: never publish a rewrite whose read you did not verify reached the end of the file. The commit is not the risky step — it is the only step with a standard behind it. The risky step is the one that filled memory, and almost nothing checks it.

Concretely, three habits close the gap:

  1. Verify the read, not just the commit. Track whether the read loop ended at EOF or at an early exit — a signal, a short read, a timeout, a cancelled task. Carry that fact forward as data. The zsh fix is exactly this: a flag from the read path consulted by the write path.
  2. Check the return value of the close, not just the rename. The Linux close(2) manual is blunt about why: "A careful programmer will check the return value of close(), since it is quite possible that errors on a previous write(2) operation are reported only on the final close() that releases the open file description. Failing to check the return value when closing a file may lead to silent loss of data."29 POSIX puts the mechanism behind that warning in fclose(): "Any unwritten buffered data for the stream shall be written to the file; any unread buffered data shall be discarded."30 The last write in your program is often one you never wrote.
  3. Treat a short transfer as a normal event. write(2) says so normatively: "Note that a successful write() may transfer fewer than count bytes. Such partial writes can occur for various reasons; for example, because there was insufficient space on the disk device to write all of the requested bytes, or because a blocked write() to a socket, pipe, or similar was interrupted by a signal handler after it had transferred some, but before it had transferred all of the requested bytes."31 A partial write is a documented success. Loop until the count is satisfied.

None of this contradicts the existing advice. An append-only log that compacts by writing a new file and renaming it is still the right design, and its guarantee — a reader always sees a complete file — is still true.2

A separate post on agents writing to notes they read seconds earlier concedes the one limit atomic rename has against concurrency: it guarantees a clean commit, not a conflict-free one.32 The Zsh case has a single writer and zero contention — which is exactly what makes it a different failure. Both posts are right about what they claim. This is the clause that sits beside them.

The generalization is not that renames are unsafe. It is that a note editor, a shell, a config tool, and a database exporter all run the same three-stage pipeline — read the old state, build the new state, commit it — and only the third stage has a specification defending it.

Anything that rewrites a whole file on save inherits this seam. Related failures in the same family: a database that can lose a committed write and a 40-byte note that costs 55 kilobytes of disk writes.

Nobody has solved this class of bug. Pillai's 60 vulnerabilities were found in eleven mature, widely-used, expert-maintained systems, and that number is the right posture to hold about your own code, including ours.25 The discipline is knowable and checkable. It is not the same thing as being immune.


Frequently Asked Questions

These are the questions developers actually type when a file comes back shorter than it went in. Each answer is scoped to what the specifications and the upstream record state, because the gap between "the commit was atomic" and "the data is all there" is exactly where the confusion lives.

Can an atomic rename still lose data?

Yes. The rename guarantees the swap, not the payload. POSIX defines it as a promise about which file a name refers to during the operation — the old one or the new one, never a torn one.3 If the file you are swapping in was built from an incomplete read, the rename commits that incomplete file perfectly and reports success.

Is writing to a temp file and renaming it safe?

It is correct and insufficient. It reliably prevents a reader from seeing a half-written file, which is what it was designed for. It says nothing about whether the temp file contains everything it should. Add one check above it: confirm the read that produced the contents reached end of file before you commit.

Why do I lose my ZSH history?

One documented mechanism is an interrupted read during the exit rewrite. Zsh reads the existing history, merges, and writes the result; a signal could short-circuit the read while the write proceeded anyway.4 The report was filed in March 2025 and the fix shipped in Zsh 5.9.2, announced 12 July 2026.22 Other causes exist, including history options and multiple shells.

How can I check if a file is completely written to disk?

Check the return value of close(), not only the write calls. The Linux manual warns that errors from an earlier write(2) are often reported only at the final close(), and that skipping that check "may lead to silent loss of data."29 Then verify the source side too: a commit cannot detect an incomplete read.

Is mv atomic on my fs?

For a rename within a single filesystem, yes — that is the guarantee POSIX defines.3 Across filesystems it is a copy-then-delete and no such guarantee applies. Either way it answers a narrower question than most people are asking: it protects the swap, never the completeness of what you are swapping in.

Does fsync protect me from this?

No. fsync makes bytes durable; it cannot know which bytes were supposed to be there. In the Python reproduction above, fsync returned successfully over a 74% loss. Rebello and colleagues also found that when fsync genuinely fails, no studied failure-handling strategy is sufficient.27

How do I know my read reached the end of the file?

Instrument the exit condition. A read loop can end because it saw EOF, or because a signal, timeout, cancellation, or error stopped it early — and only the first justifies a rewrite. Record which one happened and consult it before committing. In the syscall trace, the presence or absence of read = 0 was the entire difference between a healthy save and a lossy one.9


The atomicity you were promised is real, and it is a promise about a name. What lands under that name is your problem, and the only place to defend it is above the write, at the moment you decide the thing in memory is worth committing.

This post builds on Michael Stapelberg's investigation and his upstream bug report, which did the hard work of turning an intermittent symptom into a reproducible failure.


If you keep notes for years, the same discipline applies to whatever holds them: MNMNOTE is a local-first markdown editor that keeps your notes on your own device as open Markdown files.

Footnotes

  1. Stapelberg, M. "BUG: Zsh loses history entries since 2015." zsh-workers 53412, Tue, 25 Mar 2025 11:14:53 +0100. Reproducer step 4: wc -l ~/.zsh_history reports 55075 before, echo $? reports 0, and wc -l reports 12962 after. https://www.zsh.org/mla/workers/2025/msg00114.html. Published 2025-03-25. Accessed 2026-08-17.

  2. MNMNOTE. "The Append-Only Log File That Never Rewrites History." https://blog.mnmnote.com/posts/the-append-only-log-file-that-never-rewrites-history. Published 2026-07-22. Accessed 2026-08-17. 2

  3. The Open Group. "rename — rename a file," DESCRIPTION. IEEE Std 1003.1-2017 (POSIX.1-2017), The Open Group Base Specifications Issue 7, 2018 edition. https://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html. Accessed 2026-08-17. 2 3

  4. Stapelberg, M. "Tracking down a Zsh history data loss bug," §"What was the bug?". https://michael.stapelberg.ch/posts/2026-08-09-zsh-history-truncation-bug/. Published 2026-08-09. Accessed 2026-08-17. 2

  5. zsh-users/zsh, commit f1c702f2a4159409b27b9576999614a69a51987d, "34817: Catch some errors earlier when reading history." Author and committer: Peter Stephenson. https://github.com/zsh-users/zsh/commit/f1c702f2a4159409b27b9576999614a69a51987d. Committed 2015-03-29. Accessed 2026-08-17.

  6. Derived interval: 2015-03-29 (commit date, 5) to 2025-03-25 (report date, 1) is 3,649 days, or 9.99 years.

  7. Stapelberg, M. "Tracking down a Zsh history data loss bug," §"Conclusion". https://michael.stapelberg.ch/posts/2026-08-09-zsh-history-truncation-bug/. Published 2026-08-09. Accessed 2026-08-17. 2

  8. Stapelberg, M. "Tracking down a Zsh history data loss bug," §"bpftrace" — the two journalctl -fu zshhisttrace traces. Every line is reproduced verbatim and in source order; the surrounding history-lock lines (symlink …LOCK, the flags 541 open and its zero-byte close, the trailing unlink …LOCK) are elided — they are present in both runs and differ only in the process id. https://michael.stapelberg.ch/posts/2026-08-09-zsh-history-truncation-bug/. Published 2026-08-09. Accessed 2026-08-17.

  9. Stapelberg, M. "Tracking down a Zsh history data loss bug," annotation on the truncated trace. https://michael.stapelberg.ch/posts/2026-08-09-zsh-history-truncation-bug/. Published 2026-08-09. Accessed 2026-08-17. 2

  10. Derived figure, both operands from 8: 11,572,944 ÷ 52,888,907 = 21.88%.

  11. The Open Group. "fclose — close a stream," DESCRIPTION. IEEE Std 1003.1-2017. https://pubs.opengroup.org/onlinepubs/9699919799/functions/fclose.html. Accessed 2026-08-17.

  12. Stapelberg, M. "Tracking down a Zsh history data loss bug," §"Making it crash!" — the deliberate-crash patch, systemd-coredump, and the gdb bt full backtrace showing frame #0 at hist.c:3086 and frame #1 at hist.c:3121. https://michael.stapelberg.ch/posts/2026-08-09-zsh-history-truncation-bug/. Published 2026-08-09. Accessed 2026-08-17.

  13. Stapelberg, M. "BUG: Zsh loses history entries since 2015," zsh-workers 53412, reproducer steps 1–4. https://www.zsh.org/mla/workers/2025/msg00114.html. Published 2025-03-25. Accessed 2026-08-17.

  14. Stapelberg, M. zsh-workers 53412, closing line of reproducer step 4. https://www.zsh.org/mla/workers/2025/msg00114.html. Published 2025-03-25. Accessed 2026-08-17.

  15. Stapelberg, M. "Tracking down a Zsh history data loss bug," §"The symptom". https://michael.stapelberg.ch/posts/2026-08-09-zsh-history-truncation-bug/. Published 2026-08-09. Accessed 2026-08-17.

  16. Stapelberg, M. zsh-workers 53412, root-cause paragraph. https://www.zsh.org/mla/workers/2025/msg00114.html. Published 2025-03-25. Accessed 2026-08-17.

  17. zsh-users/zsh, commit a6760226c75c8a13e78f8b4c7163f1256322531a, "53454: fix interrupt handling in savehistfile()". Authored by Bart Schaefer 2025-04-15; committed to the release branch by dana 2026-06-01 (412 days, derived). Diff: Src/hist.c +8 −5, ChangeLog +3 −0. https://github.com/zsh-users/zsh/commit/a6760226c75c8a13e78f8b4c7163f1256322531a. Accessed 2026-08-17. 2 3

  18. Schaefer, B. Reply carrying the patch, zsh-workers 53454, Sun, 6 Apr 2025 14:42:14 -0700. https://www.zsh.org/mla/workers/2025/msg00156.html. Published 2025-04-06. Accessed 2026-08-17.

  19. Schaefer, B. zsh-workers 53454, same message, on SIGHUP and HIST_SAVE_BY_COPY. https://www.zsh.org/mla/workers/2025/msg00156.html. Published 2025-04-06. Accessed 2026-08-17.

  20. zsh-announce archive index, entry "2022/05/16 zsh 5.9 released — dana." https://www.zsh.org/mla/announce/. Accessed 2026-08-17.

  21. dana. "zsh 5.9.1 released," zsh-announce, message header Date: Sun, 31 May 2026 16:16:14 -0500. https://www.zsh.org/mla/announce/msg00135.html. Published 2026-05-31. Accessed 2026-08-17. Derived: 2022-05-16 to 2026-05-31 is 1,476 days, or 4.04 years.

  22. dana. "zsh 5.9.2 released," zsh-announce 145, message header Date: Sun, 12 Jul 2026 14:47:37 -0500. https://www.zsh.org/mla/announce/msg00136.html. Published 2026-07-12. Accessed 2026-08-17. 2 3

  23. Apple Support. "Use zsh as the default shell on Mac," article 102360. https://support.apple.com/en-us/102360. Published 2026-06-15. Accessed 2026-08-17.

  24. Pillai et al., "All File Systems Are Not Created Equal," §4.4.2 "Atomicity within System Calls," paragraph "Directory operation atomicity." https://www.usenix.org/system/files/conference/osdi14/osdi14-paper-pillai.pdf. Published October 2014. Accessed 2026-08-17.

  25. Pillai, T. S., Chidambaram, V., Alagappan, R., Al-Kiswany, S., Arpaci-Dusseau, A. C., & Arpaci-Dusseau, R. H. "All File Systems Are Not Created Equal: On the Complexity of Crafting Crash-Consistent Applications." 11th USENIX Symposium on Operating Systems Design and Implementation (OSDI '14), pp. 433–448. https://www.usenix.org/conference/osdi14/technical-sessions/presentation/pillai. Published October 2014. Accessed 2026-08-17. 2

  26. Pillai et al., "All File Systems Are Not Created Equal," abstract. https://www.usenix.org/conference/osdi14/technical-sessions/presentation/pillai. Published October 2014. Accessed 2026-08-17.

  27. Rebello, A., Patel, Y., Alagappan, R., Arpaci-Dusseau, A. C., & Arpaci-Dusseau, R. H. "Can Applications Recover from fsync Failures?" 2020 USENIX Annual Technical Conference (ATC '20), abstract. https://www.usenix.org/conference/atc20/presentation/rebello. Published July 2020. Accessed 2026-08-17. 2

  28. Rebello et al., "Can Applications Recover from fsync Failures?" — study scope: three Linux file systems (ext4, XFS, Btrfs) and five applications (PostgreSQL, LMDB, LevelDB, SQLite, Redis). https://www.usenix.org/conference/atc20/presentation/rebello. Published July 2020. Accessed 2026-08-17.

  29. The Linux man-pages project. close(2), NOTES, "Dealing with error returns from close()." Linux man-pages 6.18, page dated 2026-02-08. https://man7.org/linux/man-pages/man2/close.2.html. Accessed 2026-08-17. 2

  30. The Open Group. "fclose — close a stream," DESCRIPTION, on unwritten buffered data. IEEE Std 1003.1-2017. https://pubs.opengroup.org/onlinepubs/9699919799/functions/fclose.html. Accessed 2026-08-17.

  31. The Linux man-pages project. write(2), RETURN VALUE, on partial writes. Linux man-pages 6.18, page dated 2026-02-08. https://man7.org/linux/man-pages/man2/write.2.html. Accessed 2026-08-17.

  32. MNMNOTE. "The Agent Read Your Note 40 Seconds Ago. It's About to Write What It Read." https://blog.mnmnote.com/posts/the-agent-read-your-note-its-about-to-write-what-it-read. Published 2026-08-09. Accessed 2026-08-17.