Tutorials 18 min read

Line Endings in Your Notes: Why the Same File Looks Different on Windows, Mac, and Linux

MMNMNOTE
line-endingscrlf-vs-lfgitattributesnote-vaultgitmarkdowncross-platform

Two silent-corruption traps hit a cross-OS note vault when it lives on more than one laptop. The louder is line endings: Windows ends a line with two bytes (\r\n); macOS and Linux with one (\n). A git checkout without a rule turns the vault into a diff where every line changed but no word did. One .gitattributes line ends it.

The other trap is filename case, and we cover it in the companion to this piece, The Case-Sensitivity Trap That Corrupts a Note Vault Silently. Read the two together — they are the two things nobody warns you about when you start versioning notes across a Mac and a Windows laptop, and neither is anyone's fault. They are compatibility surfaces you learn to live with.

Pro Git names the byte-level version in one sentence: "If you're programming on Windows and working with people who are not (or vice-versa), you'll probably run into line-ending issues at some point" 1. It has always been true of code. It is quietly true of a plain-Markdown note vault the day the same source is read by a Windows editor that expects \r\n, a git index that stores \n, and a macOS renderer that ships whatever it opened.

What line endings actually are — two invisible bytes

A line ending is a byte, sometimes two. Windows writes carriage-return (0x0D) followed by line-feed (0x0A) — CRLF. macOS and Linux write only line-feed — LF. Pro Git states it plainly: "Windows uses both a carriage-return character and a linefeed character for newlines in its files, whereas macOS and Linux systems use only the linefeed character" 1.

The convention is not arbitrary. On the Linux and macOS side, POSIX defines it. IEEE Std 1003.1 §3.403 says a text file "contains characters organized into zero or more lines. The lines do not contain NUL characters and none can exceed {LINE_MAX} bytes in length, including the <newline> character" 2. The specified terminator is one byte. Everything downstream — sed, grep, awk, less, most Markdown parsers — is written to that contract.

On the Windows side, the two-byte convention is old enough that its own operating system does not fully honor it any more. Microsoft's Michel Lopez wrote the retirement notice on the Windows Command Line blog in May 2018: "For many years, Windows Notepad only supported text documents containing Windows End of Line (EOL) characters — Carriage Return (CR) & Line Feed (LF). This means that Notepad was unable to correctly display the contents of text files created in Unix, Linux and macOS" 3.

Notepad has read all three line-ending styles since Windows 10 v1809, shipped in October 2018 — but it still writes CRLF by default for new files.

That is the shape of the trap. The bytes are old, the systems have moved, and the vault owner is the one who has to pick a convention that survives the room.

The core.autocrlf knob has three values

Git ships with a per-machine knob that translates the bytes between your working directory and its object database. core.autocrlf has three settings, and each one is a different theory of who should own the byte. The wrong choice is not disastrous. Choosing consciously is how you stop being surprised on the day of your first cross-machine pull.

The Windows default is core.autocrlf=true. Pro Git describes it as: "this converts LF endings into CRLF when you check out code" 1 — and the reverse on commit, so the repository always stores LF. A Windows developer runs the git installer, checks the recommended box, and files land on disk with CRLF while the object database keeps LF. On paper it works.

The macOS and Linux default is core.autocrlf=input. Pro Git's exact sentence: "This setup should leave you with CRLF endings in Windows checkouts, but LF endings on macOS and Linux systems and in the repository" 1. It converts CRLF to LF on commit — but never the other way around. A file that lands as LF stays LF everywhere it goes.

The third value is false. Pro Git names it too: "you can turn off this functionality, recording the carriage returns in the repository by setting the config value to false" 1. Git records the raw byte sequence, whatever it is. A file committed on Windows keeps its CRLF; a file committed on Linux keeps its LF; the repository becomes a mixed record that reflects nothing but the last person to type the letters.

The pain isn't any single value. The pain is that every teammate — and every one of your own laptops — has to remember which value they set, and why. A repository-scoped rule doesn't require anyone to remember.

Before you set anything, know what the vault already contains. This is what the check looks like on a POSIX shell:

# Report each file's detected line-ending style
file *.md
# Any file that contains a CR (i.e. CRLF-terminated)
grep -rIl $'\r' .
# Strip all CR bytes from a single file (CRLF → LF)
tr -d '\r' < in.md > out.md

On Windows, run those inside Git-Bash or WSL. The file command names line-terminator style directly ("ASCII text, with CRLF line terminators" vs "ASCII text"), and grep for $'\r' finds every CRLF-terminated file in the tree — the second silent-fact-check most vault owners have never run.

One .gitattributes file ends the debate

The fix is a repository-scoped rule that survives the OS the vault is cloned onto. Commit a .gitattributes file at the vault's root, and every machine that clones the repo inherits the decision without configuring anything. Pope Kim named the principle on his engineering blog in August 2025: "a team's agreement is encoded in the codebase itself" 4.

Two lines are enough. Save this at the top of your vault as .gitattributes:

* text=auto
*.md text eol=lf

The first line, * text=auto, is git's own recommended safe default. gitattributes(5) documents what it guarantees: it "does ensure that text files that you introduce to the repository have their line endings normalized to LF when they are added, and that files that are already normalized in the repository stay normalized" 5. text=auto also lets git detect binaries and leave their bytes untouched. It is the low-cost floor.

The second line pins every .md file to LF regardless of whose machine checks it out. The eol attribute is what makes this deterministic: "This setting uses the same line endings in the working directory as in the index when the file is checked out" 6. Once .gitattributes is committed, eol=lf means "LF everywhere" — LF in the index, LF in the working tree, LF on every platform.

The text attribute is where the normalization happens in the first place. gitattributes(5) again: "When a matching file is added to the index, the file's line endings are normalized to LF in the index" 7. That normalization is what stops the "every line changed" diff from ever happening again for markdown files in the vault.

The rule fits on two lines because git already knows how to do the work. You are just writing down the agreement.

Three caveats the fix will not fix

The rule has three edges. Repeating the LF recommendation without them is how honest advice turns into folklore that gets contradicted in the comments. Each edge is documented. Each has a known workaround. Reading them once is how you stop rediscovering them under deadline.

Caveat one: CRLF is not universally wrong. RFC 5545 iCalendar requires CRLF between lines — a subject we spelled out in A Calendar File Is Just Plain Text. Windows .bat, .cmd, .ps1, and .sln files need CRLF to run correctly on Windows itself. The LF recommendation in this piece is scoped to *.md notes in a plain-Markdown vault; a shell script or an iCalendar file gets a different rule, and .gitattributes is the file where you write both.

Pope Kim's essay makes the same point about the scope of any LF-default rule and calls out how uncomfortable the topic still is: "sometimes I still see company repositories relying on core.autocrlf. Honestly, using this in 2025 feels frustrating" 4. Muhammad Bin Zafar frames the trade-off for a mixed-platform team on DEV Community with the same honesty — "this problem arises with Git when team members are using different operating systems, e.g., Windows vs Mac/Linux" — followed by the concession on the very next line, "Windows is alone in having CRLF. So the decision is made: LF" 8.

Caveat two: core.autocrlf conversion is not lossless. Git's own configuration reference is bracingly honest about this in the core.safecrlf section — "CRLF conversion bears a slight chance of corrupting data" 9. That is why a repository-scoped .gitattributes beats a per-machine core.autocrlf for a note vault you plan to keep for a decade. The .gitattributes file records the decision once, in the repo, and does not reach across every other project on the same laptop the way --global git config does. If a rare byte pattern trips core.safecrlf, git will refuse the conversion loudly rather than silently rewriting the file.

Caveat three: .gitattributes only affects files added after it is committed. A vault that already has a mix of CRLF and LF files does not fix itself when you commit the rule. The tell is telling — you drop the file, run git status, and it shows nothing changed. The vault still has ghost diffs on the next pull. gitattributes(5) names the recovery command. Once .gitattributes is in place, run one clean sweep from the top of the vault:

# Only from a clean working tree — commit or stash any real changes first
git add --renormalize .
git status                       # every file scheduled for normalization
git commit -m "Normalize line endings to LF via .gitattributes"

That single normalization commit is where the repository's history gets its ghost-diff reset. Every subsequent commit is written in one convention. This step is the one most tutorials leave out, and it is the one Moy — a real note-vault owner — flagged directly on the Obsidian forum after a cross-OS sync broke: "You use multiple platforms (Windows and iOS/macOS/Linux)" and "your synchronization method includes Git" together mean "you'll typically encounter numerous ineffective syncs due to line ending differences" 10. The .gitattributes file is what encodes the agreement; git add --renormalize . is what enforces it retroactively.

The other silent-corruption trap on the same surface

Line endings are one of two byte-level traps a cross-OS note vault hits without a written contract. The other is filename case — the same eight bytes resolving to one file on your Mac and two on your Linux server. We spelled it out in The Case-Sensitivity Trap; as a pair, they are the two things nobody warns you about.

A vault crossing operating systems is easier to reason about if you commit three tiny files at its root and pick a naming convention. A safe .gitignore for a note vault — what git should ignore. A safe .gitattributes — what this piece just described. A lowercase-kebab-ASCII naming convention — the intersection of what every filesystem, every git remote, and every URL will accept without transformation.

Together they are the cross-OS vault safety kit. Once they exist, the vault stops surprising you.

There is a fourth companion piece worth naming. Line endings decide the byte between lines; hard-wrap discipline decides the shape inside lines. Hard-Wrap or Soft-Wrap Your Notes? The Diff Decides is the review-side companion to this interop-side fix. Same file, two decisions, two different jobs.

How this works in MNMNOTE

The vault owner who lives across a Mac, a Windows laptop, and a Linux desktop is exactly the reader who hits this trap. A browser-based, local-first note tool opens the same plain-Markdown vault on Windows, macOS, Linux, iOS, and Android — and the discipline that keeps it legible on every one of them belongs to you.

MNMNOTE is browser-based and opens the vault where you cloned it. That is the whole promise. The .gitattributes line above is the discipline that pays for the promise. It is one file in the same folder your notes live in, committed once, and the wall-of-text failure mode never comes back for the notes that already exist in it.

Frequently asked questions

Why does my markdown note look like one giant line on Windows?

Because your file is LF-terminated and the editor you opened it in on Windows treats a CR as required to break the line. Modern editors — VS Code, Sublime, JetBrains, Notepad on Windows 10 v1809 and later — read all three line-ending styles correctly. The fix is at the file level: commit .gitattributes with *.md text eol=lf and every note stays LF across every machine.

How do I fix CRLF vs LF in a note vault?

Commit a .gitattributes at the vault root with * text=auto and *.md text eol=lf. From a clean working tree, run git add --renormalize ., review git status, then commit the normalization. One commit fixes the entire vault. Every future clone inherits the rule without configuring anything on the machine.

Why did every line of my note change in git diff?

Because your local core.autocrlf disagrees with what is in the repository — the diff is not your content, it is \r\n\n on every line. Set the platform default (Windows → true, macOS/Linux → input) or, better, commit .gitattributes so no contributor has to remember. The repository-scoped rule survives OS reinstalls; the personal --global config does not.

What is the safe .gitattributes for markdown files?

* text=auto on the first line, *.md text eol=lf on the second. Rationale: text=auto normalizes text and protects binaries, and eol=lf pins markdown to LF because Markdown is not a Windows-only format and every modern editor — including Windows Notepad since v1809 — reads LF-terminated files correctly. The rule survives every editor swap because it lives in the repository, not the app.

What does "warning: LF will be replaced by CRLF" mean?

It is a warning, not an error. Git prints it when core.autocrlf=true on Windows and the file is LF-terminated — git will do the conversion. If it annoys you, add .gitattributes with *.md text eol=lf; the file stays LF everywhere, no message, no conversion. The warning also disappears for any file whose extension the .gitattributes rule matches.

Do I need to configure line endings for Obsidian, VS Code, or any other editor?

No. Obsidian and VS Code preserve whatever line-ending style they opened a file in. The failure mode is not in the editor — it is in the git index, where a wrong core.autocrlf value rewrites LF to CRLF or the reverse on checkout. Setting .gitattributes at the vault root is the one control that survives editor swaps and OS reinstalls.

Won't core.autocrlf corrupt my file?

The risk is documented and small. Git's own configuration reference says "CRLF conversion bears a slight chance of corrupting data" 9. That is why a repository-scoped .gitattributes is the safer floor: it records the decision once, in the vault, and core.safecrlf will refuse a conversion rather than silently rewriting a file where the round-trip would not be lossless.

References


Two invisible bytes decide whether a plain-Markdown vault reads as careful prose or an 8,000-word wall the day your other laptop opens it — a discipline you commit once at the vault's root, then stop thinking about, is what mnmnote.com is built to reward.

Footnotes

  1. Chacon, S. & Straub, B. Pro Git, 2nd edition, §8.1 "Customizing Git — Git Configuration." Apress / continuously updated online. https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration 2 3 4 5

  2. IEEE Std 1003.1-2017, POSIX Base Definitions, chapter 3 §3.403 "Text File." The Open Group. https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html

  3. Lopez, M. (2018, May 8). "Introducing extended line endings support in Notepad." Windows Command Line blog, Microsoft. https://devblogs.microsoft.com/commandline/extended-eol-in-notepad/

  4. Kim, P. (2025, August 28). "Git, Still Using autocrlf in 2025? That's Frustrating." PPMC. https://blog.popekim.com/en/2025/08/28/stop-using-autocrlf.html 2

  5. gitattributes(5), * text=auto explanation — "does ensure that text files that you introduce to the repository have their line endings normalized to LF when they are added, and that files that are already normalized in the repository stay normalized." https://git-scm.com/docs/gitattributes

  6. gitattributes(5), the eol attribute — "This setting uses the same line endings in the working directory as in the index when the file is checked out." https://git-scm.com/docs/gitattributes

  7. gitattributes(5), the text attribute — "When a matching file is added to the index, the file's line endings are normalized to LF in the index." Git project, continuously updated. https://git-scm.com/docs/gitattributes

  8. Zafar, M. B. (midnqp) (2024, January 4; edited February 23). "The fuss with CRLF and LF in Git." DEV Community. https://dev.to/midnqp/the-fuss-with-crlf-and-lf-in-git-4nnf

  9. Git canonical config reference, core.safecrlf — "CRLF conversion bears a slight chance of corrupting data." https://git-scm.com/docs/git-config (source: Documentation/config/core.adoc in the git repository). 2

  10. Moy (2024, November 24). "Solving Git Sync Issues Caused by Different System Line Endings." Obsidian Forum. https://forum.obsidian.md/t/solving-git-sync-issues-caused-by-different-system-line-endings/92253